@north-light/sdk 0.3.539 → 0.3.541

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.
@@ -11,7 +11,7 @@ export const sdkErrorCodes = [
11
11
  'oauth_nonce_missing',
12
12
  /** The callback's `state` was missing or differed from the saved one. */
13
13
  'oauth_state_mismatch',
14
- /** `exchangeCode` got a sign-in-only answer, or `exchangeIdentity` got one carrying a connection. */
14
+ /** `exchangeCode` got a sign-in-only answer (no connection); finish such a sign-in with `exchangeIdentity`. */
15
15
  'oauth_exchange_mismatch',
16
16
  /** `oauth.client(userId)` or `refresh` found no stored connection for the person: they must sign in again. */
17
17
  'not_connected',
@@ -188,11 +188,12 @@ export declare class OAuth2Client {
188
188
  nonce?: string;
189
189
  }): Promise<Connection>;
190
190
  /**
191
- * Finish a sign-in-only request (`authorizeUrl` with scopes `openid` or `openid email`) and return the
192
- * person's verified identity. There is no connection, refresh token, or runtime access on this path.
193
- * `nonce` is the value `authorizeUrl` returned and is required. A response without an ID token, or with
194
- * one that fails verification, throws `id_token_invalid`; a response carrying a runtime connection (the
195
- * request asked for runtime scopes) throws `oauth_exchange_mismatch` and must be finished with `exchangeCode`.
191
+ * Finish a sign-in-only request (`authorizeUrl` with scopes `openid`, optionally with `email` and `profile`) and
192
+ * return the person's verified identity. The app gets no connection, refresh token, or runtime access on this path.
193
+ * `nonce` is the value `authorizeUrl` returned and is required. A response without an ID token, or with one that
194
+ * fails verification, throws `id_token_invalid`. The directory may still answer with a runtime connection (it gives
195
+ * one on every sign-in to an app held by a team with a runtime grant); the identity is returned all the same, and
196
+ * the unneeded refresh token is revoked best-effort. Revocation removes only that token, never the grant.
196
197
  */
197
198
  exchangeIdentity({ code, codeVerifier, state, expectedState, nonce }: {
198
199
  code: string;
@@ -271,6 +272,7 @@ export declare class OAuth2Client {
271
272
  nonce?: string;
272
273
  }): Promise<IdTokenClaims>;
273
274
  revoke(connection: Connection): Promise<boolean>;
275
+ private revokeRefreshToken;
274
276
  /** A token request. `deadlineAt` is the whole operation's deadline (discovery included); without one it starts now. */
275
277
  private tokenResponse;
276
278
  private connectionFrom;
@@ -265,23 +265,26 @@ export class OAuth2Client {
265
265
  return connection;
266
266
  }
267
267
  /**
268
- * Finish a sign-in-only request (`authorizeUrl` with scopes `openid` or `openid email`) and return the
269
- * person's verified identity. There is no connection, refresh token, or runtime access on this path.
270
- * `nonce` is the value `authorizeUrl` returned and is required. A response without an ID token, or with
271
- * one that fails verification, throws `id_token_invalid`; a response carrying a runtime connection (the
272
- * request asked for runtime scopes) throws `oauth_exchange_mismatch` and must be finished with `exchangeCode`.
268
+ * Finish a sign-in-only request (`authorizeUrl` with scopes `openid`, optionally with `email` and `profile`) and
269
+ * return the person's verified identity. The app gets no connection, refresh token, or runtime access on this path.
270
+ * `nonce` is the value `authorizeUrl` returned and is required. A response without an ID token, or with one that
271
+ * fails verification, throws `id_token_invalid`. The directory may still answer with a runtime connection (it gives
272
+ * one on every sign-in to an app held by a team with a runtime grant); the identity is returned all the same, and
273
+ * the unneeded refresh token is revoked best-effort. Revocation removes only that token, never the grant.
273
274
  */
274
275
  async exchangeIdentity({ code, codeVerifier, state, expectedState, nonce }) {
275
276
  assertState(state, expectedState);
276
277
  if (typeof nonce !== 'string' || !nonce)
277
278
  throw new CrouterError('exchangeIdentity requires the nonce authorizeUrl returned', 'oauth_nonce_missing');
278
279
  const response = await this.tokenResponse({ grant_type: 'authorization_code', code, redirect_uri: this.options.redirectUri, code_verifier: codeVerifier });
279
- if (response.refresh_token !== undefined || response.runtimeUrl !== undefined) {
280
- throw new CrouterError('The directory answered with a runtime connection, so the request asked for runtime scopes; finish it with exchangeCode(), or request only openid or openid email for a sign-in-only visitor', 'oauth_exchange_mismatch');
281
- }
282
280
  if (typeof response.id_token !== 'string' || !response.id_token)
283
281
  throw new CrouterError('Directory returned no ID token for a sign-in-only request', 'id_token_invalid');
284
282
  const claims = await this.verifyIdToken(response.id_token, { nonce });
283
+ if (typeof response.refresh_token === 'string' && response.refresh_token) {
284
+ // The directory gave a connection this sign-in does not use: drop its refresh token. Best-effort, because the
285
+ // identity is already verified and the token is unusable without the app's key.
286
+ await this.revokeRefreshToken(response.refresh_token).catch(() => undefined);
287
+ }
285
288
  return {
286
289
  sub: claims.sub,
287
290
  ...(typeof claims['email'] === 'string' ? { email: claims['email'] } : {}),
@@ -523,11 +526,14 @@ export class OAuth2Client {
523
526
  return claims;
524
527
  }
525
528
  async revoke(connection) {
529
+ return await this.revokeRefreshToken(connection.refreshToken);
530
+ }
531
+ async revokeRefreshToken(refreshToken) {
526
532
  const deadlineAt = Date.now() + this.timeoutMs;
527
533
  const metadata = await this.discoveryWithin(deadlineAt);
528
534
  if (!metadata.revocation_endpoint)
529
535
  return false;
530
- await this.post(metadata.revocation_endpoint, { token: connection.refreshToken, token_type_hint: 'refresh_token' }, true, deadlineAt);
536
+ await this.post(metadata.revocation_endpoint, { token: refreshToken, token_type_hint: 'refresh_token' }, true, deadlineAt);
531
537
  return true;
532
538
  }
533
539
  /** A token request. `deadlineAt` is the whole operation's deadline (discovery included); without one it starts now. */
@@ -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.539"
5
+ sdk-version: "0.3.541"
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.539`. 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.541`. 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.539";
2
+ export declare const SDK_VERSION = "0.3.541";
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.539';
3
+ export const SDK_VERSION = '0.3.541';
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.539",
3
+ "version": "0.3.541",
4
4
  "crouterContract": "1.28",
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.539",
48
- "@north-light/identity": "0.3.539",
47
+ "@north-light/api": "0.3.541",
48
+ "@north-light/identity": "0.3.541",
49
49
  "jose": "^6.2.1"
50
50
  },
51
51
  "peerDependencies": {