@owlmeans/auth-token 0.1.18-rc.16 → 0.1.18-rc.17
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/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/auth-token/SKILL.md +18 -4
- package/build/carrier.d.ts.map +1 -1
- package/build/carrier.js +2 -0
- package/build/carrier.js.map +1 -1
- package/build/types.d.ts +20 -0
- package/build/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/carrier.ts +8 -0
- package/src/types.ts +20 -0
- package/tests/carrier.spec.ts +20 -0
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/auth-token",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-09-
|
|
4
|
+
"version": "0.1.18-rc.17",
|
|
5
|
+
"generatedAt": "2026-09-18T18:24:55.975Z",
|
|
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.
|
|
11
|
+
**Install:** `"@owlmeans/auth-token": "^0.1.18-rc.17"` 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`
|
package/build/carrier.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
package/build/carrier.js.map
CHANGED
|
@@ -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;
|
|
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. */
|
package/build/types.d.ts.map
CHANGED
|
@@ -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;
|
|
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.
|
|
3
|
+
"version": "0.1.18-rc.17",
|
|
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",
|
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 {
|
package/tests/carrier.spec.ts
CHANGED
|
@@ -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
|
})
|