@owlmeans/auth-token 0.1.18-rc.16 → 0.1.18-rc.18

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/README.md CHANGED
@@ -6,7 +6,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
6
6
  your project's skill store (`.agents/skills/`):
7
7
 
8
8
  ```sh
9
- npx @owlmeans/agent-skills@^0.1.18-rc.28
9
+ npx @owlmeans/agent-skills@^0.1.18-rc.30
10
10
  ```
11
11
 
12
12
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/auth-token",
4
- "version": "0.1.18-rc.16",
5
- "generatedAt": "2026-09-17T08:16:11.235Z",
4
+ "version": "0.1.18-rc.18",
5
+ "generatedAt": "2026-09-19T13:42:56.261Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -8,7 +8,7 @@ user-invocable: false
8
8
  # @owlmeans/auth-token
9
9
 
10
10
  **Layer:** Auth shared
11
- **Install:** `"@owlmeans/auth-token": "^0.1.18-rc.16"` in `dependencies`
11
+ **Install:** `"@owlmeans/auth-token": "^0.1.18-rc.18"` in `dependencies`
12
12
 
13
13
  The contract half of long-lived access tokens: the record shape, the route declarations, the
14
14
  format helpers, and one client-side guard that presents a token it was handed. The server half —
@@ -20,7 +20,7 @@ UI is `@owlmeans/web-auth-token`.
20
20
  | Export | Description |
21
21
  |--------|-------------|
22
22
  | `makeAuthTokenEntrypoints(opts?)` | The three routes (`list` GET, `create` POST, `revoke` DELETE `/:id`) under a `/tokens` base. `opts`: `parent`, `path`, `guard` |
23
- | `makeTokenCarrierGuard(alias, opts)` | A client `GuardService` that presents one token. `opts.token` is a value or a thunk; `opts.scheme` is `'auth-token'` (default) or `'bearer'` |
23
+ | `makeTokenCarrierGuard(alias, opts)` | A client `GuardService` that presents one token. `opts.token` is a value or a thunk; `opts.scheme` is `'auth-token'` (default) or `'bearer'`; `opts.onRejected` hears a 401 |
24
24
  | `parseAuthorizationHeader(header)` | `{ scheme, value }` with the scheme lower-cased, or `null` |
25
25
  | `isAccessToken(value, prefix)` · `displayOf(token, prefix)` | Whether a value is one of this deployment's tokens; the half of it that may be shown again |
26
26
  | `CreateAccessTokenSchema` · `AccessTokenParamsSchema` | The ajv body/params filters |
@@ -39,6 +39,12 @@ the guard answers `match` only for a value that starts with it — so an access
39
39
  session bearer arrive under the same `Authorization` header without either guard shadowing the
40
40
  other, and two deployments never mistake each other's credentials for their own.
41
41
 
42
+ **`AccessTokenRecord.audience?: string[]`** names the resources a token was issued FOR when it came
43
+ through an OAuth grant (`@owlmeans/server-oauth`, RFC 8707 applied at OUR guard — the token is an
44
+ opaque secret, not a JWT). Absent on every hand-minted token, which is admitted everywhere its scopes
45
+ reach; a non-empty audience is admitted only by a guard whose `resources` intersect it (see
46
+ [[server-auth-token]]).
47
+
42
48
  The plaintext exists exactly once, in the create response. What is stored is its hash; what a list
43
49
  shows forever after is `display` — the prefix plus 8 characters, enough to tell two of your own
44
50
  tokens apart and far too little to replay.
@@ -66,7 +72,14 @@ context.registerMiddleware(authMiddleware)
66
72
 
67
73
  There is no session, no refresh and no storage: the token is a long-lived credential the caller was
68
74
  handed. `opts.token` may be a thunk because a long-running process reads it from an environment
69
- variable and must not cache it past a reconfiguration.
75
+ variable or a credentials file and must not cache it past a reconfiguration — or past a sign-in that
76
+ has not happened yet.
77
+
78
+ **The carrier answers `update()`.** On a 401 for a request that presented this guard's bearer,
79
+ `@owlmeans/api` calls `service('<alias>').update(undefined)` to clear a browser session; a carrier
80
+ with no `update` made that a bare `TypeError` instead of the auth failure already being reported. The
81
+ carrier's `update` is a no-op that calls `opts.onRejected` — the hook a credential holder uses to
82
+ forget a dead token and sign in again (or to report it, when an operator supplied it by hand).
70
83
 
71
84
  ## The management surface is deliberately not a CRUD
72
85
 
@@ -92,7 +105,8 @@ sending a zero.
92
105
 
93
106
  ## Related
94
107
 
95
- - [[server-auth-token]] — the store, the verifying guard, the handlers and the coguard
108
+ - [[server-auth-token]] — the store, the verifying guard (audience admission), issuance, the handlers and the coguard
109
+ - [[oauth]] — the OAuth vocabulary that mints audience-scoped tokens
96
110
  - [[web-auth-token]] — the management panel and its hook
97
111
  - [[auth-protocol]] — where long-lived tokens sit among the other authentication paths
98
112
  - [[auth-common]] — `authMiddleware`, `DEFAULT_GUARD`, `extractAuthToken`
@@ -1 +1 @@
1
- {"version":3,"file":"carrier.d.ts","sourceRoot":"","sources":["../src/carrier.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAExD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAErD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,UAAW,MAAM,QAAQ,mBAAmB,KAAG,YAsBhF,CAAA"}
1
+ {"version":3,"file":"carrier.d.ts","sourceRoot":"","sources":["../src/carrier.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAExD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAErD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,UAAW,MAAM,QAAQ,mBAAmB,KAAG,YA8BhF,CAAA"}
package/build/carrier.js CHANGED
@@ -26,6 +26,8 @@ export const makeTokenCarrierGuard = (alias, opts) => {
26
26
  return token == null ? null : `${scheme} ${token}`;
27
27
  },
28
28
  }, service => async () => { service.initialized = true; });
29
+ service.update =
30
+ async () => { await opts.onRejected?.(); };
29
31
  return service;
30
32
  };
31
33
  //# sourceMappingURL=carrier.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"carrier.js","sourceRoot":"","sources":["../src/carrier.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEjD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAG9D;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,KAAa,EAAE,IAAyB,EAAgB,EAAE;IAC9F,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,KAAK,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,iBAAiB,CAAC,WAAW,EAAE,CAAA;IAEzF,MAAM,OAAO,GAAG,KAAK,IAA4B,EAAE;QACjD,MAAM,KAAK,GAAG,OAAO,IAAI,CAAC,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAA;QAEhF,OAAO,KAAK,IAAI,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAA;IACrD,CAAC,CAAA;IAED,MAAM,OAAO,GAAiB,aAAa,CAAe,KAAK,EAAE;QAC/D,KAAK,EAAE,KAAK,IAAI,EAAE,CAAC,MAAM,OAAO,EAAE,IAAI,IAAI;QAE1C,MAAM,EAAE,KAAK,IAAO,EAAE,CAAC,KAAK,CAAM;QAElC,aAAa,EAAE,KAAK,IAAI,EAAE;YACxB,MAAM,KAAK,GAAG,MAAM,OAAO,EAAE,CAAA;YAE7B,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,KAAK,EAAE,CAAA;QACpD,CAAC;KACF,EAAE,OAAO,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAA,CAAC,CAAC,CAAC,CAAA;IAEzD,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA"}
1
+ {"version":3,"file":"carrier.js","sourceRoot":"","sources":["../src/carrier.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEjD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAG9D;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,KAAa,EAAE,IAAyB,EAAgB,EAAE;IAC9F,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,KAAK,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,iBAAiB,CAAC,WAAW,EAAE,CAAA;IAEzF,MAAM,OAAO,GAAG,KAAK,IAA4B,EAAE;QACjD,MAAM,KAAK,GAAG,OAAO,IAAI,CAAC,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAA;QAEhF,OAAO,KAAK,IAAI,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAA;IACrD,CAAC,CAAA;IAED,MAAM,OAAO,GAAiB,aAAa,CAAe,KAAK,EAAE;QAC/D,KAAK,EAAE,KAAK,IAAI,EAAE,CAAC,MAAM,OAAO,EAAE,IAAI,IAAI;QAE1C,MAAM,EAAE,KAAK,IAAO,EAAE,CAAC,KAAK,CAAM;QAElC,aAAa,EAAE,KAAK,IAAI,EAAE;YACxB,MAAM,KAAK,GAAG,MAAM,OAAO,EAAE,CAAA;YAE7B,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,KAAK,EAAE,CAAA;QACpD,CAAC;KACF,EAAE,OAAO,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAA,CAAC,CAAC,CAAC,CAOxD;IAAC,OAAmF,CAAC,MAAM;QAC1F,KAAK,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,UAAU,EAAE,EAAE,CAAA,CAAC,CAAC,CAAA;IAE3C,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA"}
package/build/types.d.ts CHANGED
@@ -31,6 +31,18 @@ export interface AccessTokenRecord extends ResourceRecord {
31
31
  expiresAt?: Date;
32
32
  /** Set once and never unset. A revoked token is kept so its display name still resolves. */
33
33
  revokedAt?: Date;
34
+ /**
35
+ * Resources this token was issued FOR, when it was issued through an OAuth grant
36
+ * (`@owlmeans/server-oauth`).
37
+ *
38
+ * Absent for every token minted by hand through `createAccessToken` — those are admitted
39
+ * everywhere the caller's scopes reach, exactly as before this field existed. A non-empty
40
+ * audience narrows that: a guard configured with `resources` admits the token only when the
41
+ * two lists intersect, so a token minted for the `/mcp` connector cannot also drive the
42
+ * ordinary REST API, and vice versa (RFC 8707's audience-restriction, applied at OUR guard
43
+ * rather than by decoding a JWT audience claim, because this token is an opaque secret).
44
+ */
45
+ audience?: string[];
34
46
  }
35
47
  /** What a caller may see. The hash never leaves the server. */
36
48
  export type AccessTokenView = Omit<AccessTokenRecord, 'hash'>;
@@ -62,6 +74,14 @@ export interface AccessTokenParams {
62
74
  export interface TokenCarrierOptions {
63
75
  token: string | (() => string | Promise<string>);
64
76
  scheme?: 'auth-token' | 'bearer';
77
+ /**
78
+ * Called when the platform rejects a request that presented this credential — `@owlmeans/api`
79
+ * reaches for `AuthService.update(undefined)` on such a 401, and a carrier with none of that
80
+ * machinery answers with a no-op unless this is given. A credential holder wires it to whatever
81
+ * it does about a dead token: sign in again if it minted the token itself, or report the
82
+ * problem rather than silently trying another identity if an operator supplied it by hand.
83
+ */
84
+ onRejected?: () => void | Promise<void>;
65
85
  }
66
86
  export interface AuthTokenEntrypointOptions {
67
87
  /** The entrypoint the token routes hang under. A parent protocol is preferred; strings remain adapter input. */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,KAAK,EAAE,kBAAkB,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AACtF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAElD;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAkB,SAAQ,cAAc;IACvD,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,6FAA6F;IAC7F,IAAI,EAAE,MAAM,CAAA;IACZ,4FAA4F;IAC5F,OAAO,EAAE,MAAM,CAAA;IACf,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,IAAI,EAAE,QAAQ,CAAA;IACd,SAAS,EAAE,IAAI,CAAA;IACf,SAAS,CAAC,EAAE,IAAI,CAAA;IAChB,mFAAmF;IACnF,UAAU,CAAC,EAAE,IAAI,CAAA;IACjB,SAAS,CAAC,EAAE,IAAI,CAAA;IAChB,4FAA4F;IAC5F,SAAS,CAAC,EAAE,IAAI,CAAA;CACjB;AAED,+DAA+D;AAC/D,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAA;AAE7D,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,+EAA+E;IAC/E,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,4EAA4E;AAC5E,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,eAAe,CAAA;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,eAAe,EAAE,CAAA;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAA;CACX;AAED;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAA;IAChD,MAAM,CAAC,EAAE,YAAY,GAAG,QAAQ,CAAA;CACjC;AAED,MAAM,WAAW,0BAA0B;IACzC,gHAAgH;IAChH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,iEAAiE;AACjE,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,kBAAkB,CAAC,WAAW,EAAE,SAAS,CAAC,CAAA;IAChD,IAAI,EAAE,kBAAkB,CAAC,WAAW,EAAE,eAAe,CAAC,CAAA;IACtD,MAAM,EAAE,kBAAkB,CAAC;QAAE,IAAI,EAAE,iBAAiB,CAAA;KAAE,EAAE,iBAAiB,CAAC,CAAA;IAC1E,MAAM,EAAE,kBAAkB,CAAC;QAAE,MAAM,EAAE,iBAAiB,CAAA;KAAE,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC1E"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,KAAK,EAAE,kBAAkB,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AACtF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAElD;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAkB,SAAQ,cAAc;IACvD,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,6FAA6F;IAC7F,IAAI,EAAE,MAAM,CAAA;IACZ,4FAA4F;IAC5F,OAAO,EAAE,MAAM,CAAA;IACf,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,IAAI,EAAE,QAAQ,CAAA;IACd,SAAS,EAAE,IAAI,CAAA;IACf,SAAS,CAAC,EAAE,IAAI,CAAA;IAChB,mFAAmF;IACnF,UAAU,CAAC,EAAE,IAAI,CAAA;IACjB,SAAS,CAAC,EAAE,IAAI,CAAA;IAChB,4FAA4F;IAC5F,SAAS,CAAC,EAAE,IAAI,CAAA;IAChB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,+DAA+D;AAC/D,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAA;AAE7D,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,+EAA+E;IAC/E,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,4EAA4E;AAC5E,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,eAAe,CAAA;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,eAAe,EAAE,CAAA;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAA;CACX;AAED;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAA;IAChD,MAAM,CAAC,EAAE,YAAY,GAAG,QAAQ,CAAA;IAChC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACxC;AAED,MAAM,WAAW,0BAA0B;IACzC,gHAAgH;IAChH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,iEAAiE;AACjE,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,kBAAkB,CAAC,WAAW,EAAE,SAAS,CAAC,CAAA;IAChD,IAAI,EAAE,kBAAkB,CAAC,WAAW,EAAE,eAAe,CAAC,CAAA;IACtD,MAAM,EAAE,kBAAkB,CAAC;QAAE,IAAI,EAAE,iBAAiB,CAAA;KAAE,EAAE,iBAAiB,CAAC,CAAA;IAC1E,MAAM,EAAE,kBAAkB,CAAC;QAAE,MAAM,EAAE,iBAAiB,CAAA;KAAE,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC1E"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/auth-token",
3
- "version": "0.1.18-rc.16",
3
+ "version": "0.1.18-rc.18",
4
4
  "license": "MIT",
5
5
  "description": "Long-lived access tokens (API keys) on the OwlMeans auth rails — the record shape, the token format, the management entrypoints and the client-side carrier guard.",
6
6
  "type": "module",
@@ -23,11 +23,11 @@
23
23
  }
24
24
  },
25
25
  "dependencies": {
26
- "@owlmeans/auth": "^0.1.18-rc.27",
27
- "@owlmeans/context": "^0.1.18-rc.25",
28
- "@owlmeans/entrypoint": "^0.1.18-rc.28",
29
- "@owlmeans/resource": "^0.1.18-rc.27",
30
- "@owlmeans/route": "^0.1.18-rc.26"
26
+ "@owlmeans/auth": "^0.1.18-rc.28",
27
+ "@owlmeans/context": "^0.1.18-rc.26",
28
+ "@owlmeans/entrypoint": "^0.1.18-rc.29",
29
+ "@owlmeans/resource": "^0.1.18-rc.28",
30
+ "@owlmeans/route": "^0.1.18-rc.27"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@owlmeans/dep-config": "workspace:*",
package/src/carrier.ts CHANGED
@@ -36,5 +36,13 @@ export const makeTokenCarrierGuard = (alias: string, opts: TokenCarrierOptions):
36
36
  },
37
37
  }, service => async () => { service.initialized = true })
38
38
 
39
+ // `@owlmeans/api` calls `context.service<AuthService>(alias).update(undefined)` on a 401 for a
40
+ // request that presented this guard's bearer — an ordinary browser session clears itself there,
41
+ // and a carrier with no `update` at all made that call throw a bare `TypeError` instead of the
42
+ // auth failure it was already reporting. There is no session to clear here, so this is a no-op
43
+ // unless the caller asked to hear about it through `onRejected`.
44
+ ;(service as GuardService & { update: (token: string | undefined) => Promise<void> }).update =
45
+ async () => { await opts.onRejected?.() }
46
+
39
47
  return service
40
48
  }
package/src/types.ts CHANGED
@@ -32,6 +32,18 @@ export interface AccessTokenRecord extends ResourceRecord {
32
32
  expiresAt?: Date
33
33
  /** Set once and never unset. A revoked token is kept so its display name still resolves. */
34
34
  revokedAt?: Date
35
+ /**
36
+ * Resources this token was issued FOR, when it was issued through an OAuth grant
37
+ * (`@owlmeans/server-oauth`).
38
+ *
39
+ * Absent for every token minted by hand through `createAccessToken` — those are admitted
40
+ * everywhere the caller's scopes reach, exactly as before this field existed. A non-empty
41
+ * audience narrows that: a guard configured with `resources` admits the token only when the
42
+ * two lists intersect, so a token minted for the `/mcp` connector cannot also drive the
43
+ * ordinary REST API, and vice versa (RFC 8707's audience-restriction, applied at OUR guard
44
+ * rather than by decoding a JWT audience claim, because this token is an opaque secret).
45
+ */
46
+ audience?: string[]
35
47
  }
36
48
 
37
49
  /** What a caller may see. The hash never leaves the server. */
@@ -69,6 +81,14 @@ export interface AccessTokenParams {
69
81
  export interface TokenCarrierOptions {
70
82
  token: string | (() => string | Promise<string>)
71
83
  scheme?: 'auth-token' | 'bearer'
84
+ /**
85
+ * Called when the platform rejects a request that presented this credential — `@owlmeans/api`
86
+ * reaches for `AuthService.update(undefined)` on such a 401, and a carrier with none of that
87
+ * machinery answers with a no-op unless this is given. A credential holder wires it to whatever
88
+ * it does about a dead token: sign in again if it minted the token itself, or report the
89
+ * problem rather than silently trying another identity if an operator supplied it by hand.
90
+ */
91
+ onRejected?: () => void | Promise<void>
72
92
  }
73
93
 
74
94
  export interface AuthTokenEntrypointOptions {
@@ -46,4 +46,24 @@ describe('@owlmeans/auth-token — the client carrier guard', () => {
46
46
  expect(await guard.authenticated()).toBeNull()
47
47
  expect(await guard.match({} as any, {} as any)).toBe(false)
48
48
  })
49
+
50
+ test('answers update() rather than throwing — the call `@owlmeans/api` makes on a 401', async () => {
51
+ const guard = makeTokenCarrierGuard('carrier', { token: 'owl_secret' }) as GuardService & {
52
+ update: (token: string | undefined) => Promise<void>
53
+ }
54
+ await contextWith(guard)
55
+
56
+ await expect(guard.update(undefined)).resolves.toBeUndefined()
57
+ })
58
+
59
+ test('update() calls onRejected, so a credential holder can act on a dead token', async () => {
60
+ let called = false
61
+ const guard = makeTokenCarrierGuard('carrier', {
62
+ token: 'owl_secret', onRejected: () => { called = true },
63
+ }) as GuardService & { update: (token: string | undefined) => Promise<void> }
64
+ await contextWith(guard)
65
+
66
+ await guard.update(undefined)
67
+ expect(called).toBe(true)
68
+ })
49
69
  })