nexus-ai-pro 2.2.0 → 2.3.0

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.
Files changed (135) hide show
  1. package/API_STABILITY.md +39 -0
  2. package/CHANGELOG.md +142 -1
  3. package/README.md +10 -5
  4. package/dist/agent/capabilities.d.ts +39 -0
  5. package/dist/agent/capabilities.js +36 -0
  6. package/dist/agent/create-agent.d.ts +61 -11
  7. package/dist/agent/create-agent.js +136 -22
  8. package/dist/agent/index.d.ts +3 -2
  9. package/dist/agent/index.js +1 -1
  10. package/dist/agent/middleware/approval.d.ts +26 -0
  11. package/dist/agent/middleware/approval.js +23 -0
  12. package/dist/agent/{middleware.d.ts → middleware/basic.d.ts} +5 -4
  13. package/dist/agent/{middleware.js → middleware/basic.js} +3 -5
  14. package/dist/agent/middleware/context-editor.d.ts +52 -0
  15. package/dist/agent/middleware/context-editor.js +66 -0
  16. package/dist/agent/middleware/filesystem.d.ts +44 -0
  17. package/dist/agent/middleware/filesystem.js +83 -0
  18. package/dist/agent/middleware/index.d.ts +19 -0
  19. package/dist/agent/middleware/index.js +9 -0
  20. package/dist/agent/middleware/limits.d.ts +42 -0
  21. package/dist/agent/middleware/limits.js +52 -0
  22. package/dist/agent/middleware/models.d.ts +48 -0
  23. package/dist/agent/middleware/models.js +44 -0
  24. package/dist/agent/middleware/pii.d.ts +67 -0
  25. package/dist/agent/middleware/pii.js +173 -0
  26. package/dist/agent/middleware/retry.d.ts +66 -0
  27. package/dist/agent/middleware/retry.js +59 -0
  28. package/dist/agent/middleware/shared.d.ts +41 -0
  29. package/dist/agent/middleware/shared.js +73 -0
  30. package/dist/agent/middleware/tool-selector.d.ts +64 -0
  31. package/dist/agent/middleware/tool-selector.js +106 -0
  32. package/dist/agent/permissions.d.ts +113 -0
  33. package/dist/agent/permissions.js +180 -0
  34. package/dist/agent/sandbox.d.ts +151 -0
  35. package/dist/agent/sandbox.js +257 -0
  36. package/dist/agent/tool.d.ts +8 -3
  37. package/dist/agent/tool.js +14 -3
  38. package/dist/deep-agent/index.d.ts +109 -0
  39. package/dist/deep-agent/index.js +216 -0
  40. package/dist/graph/functional.d.ts +9 -0
  41. package/dist/graph/functional.js +20 -3
  42. package/dist/graph/graph.js +25 -4
  43. package/dist/graph/lint.d.ts +4 -1
  44. package/dist/graph/lint.js +35 -0
  45. package/dist/postgres/fulltext.d.ts +67 -0
  46. package/dist/postgres/fulltext.js +91 -0
  47. package/dist/rag/elasticsearch.d.ts +71 -0
  48. package/dist/rag/elasticsearch.js +130 -0
  49. package/dist/rag/pipeline.d.ts +140 -0
  50. package/dist/rag/pipeline.js +187 -0
  51. package/dist/rag/rerankers.d.ts +92 -0
  52. package/dist/rag/rerankers.js +97 -0
  53. package/dist/rag/retrievers.d.ts +12 -1
  54. package/dist/server/assistant.d.ts +5 -1
  55. package/dist/server/assistant.js +4 -2
  56. package/dist/server/auth.d.ts +250 -0
  57. package/dist/server/auth.js +392 -0
  58. package/dist/store/memory.d.ts +10 -0
  59. package/dist/store/memory.js +10 -0
  60. package/dist/types/graph.d.ts +38 -0
  61. package/dist/types/messages.d.ts +36 -2
  62. package/dist/types/principal.d.ts +21 -0
  63. package/dist/types/principal.js +1 -0
  64. package/dist/types/server.d.ts +2 -9
  65. package/dist/utils/tenant.d.ts +11 -0
  66. package/dist/utils/tenant.js +14 -0
  67. package/dist/version.d.ts +1 -1
  68. package/dist/version.js +1 -1
  69. package/dist-cjs/agent/capabilities.d.ts +39 -0
  70. package/dist-cjs/agent/capabilities.js +41 -0
  71. package/dist-cjs/agent/create-agent.d.ts +61 -11
  72. package/dist-cjs/agent/create-agent.js +136 -22
  73. package/dist-cjs/agent/index.d.ts +3 -2
  74. package/dist-cjs/agent/index.js +4 -4
  75. package/dist-cjs/agent/middleware/approval.d.ts +26 -0
  76. package/dist-cjs/agent/middleware/approval.js +26 -0
  77. package/dist-cjs/agent/{middleware.d.ts → middleware/basic.d.ts} +5 -4
  78. package/dist-cjs/agent/{middleware.js → middleware/basic.js} +3 -5
  79. package/dist-cjs/agent/middleware/context-editor.d.ts +52 -0
  80. package/dist-cjs/agent/middleware/context-editor.js +70 -0
  81. package/dist-cjs/agent/middleware/filesystem.d.ts +44 -0
  82. package/dist-cjs/agent/middleware/filesystem.js +86 -0
  83. package/dist-cjs/agent/middleware/index.d.ts +19 -0
  84. package/dist-cjs/agent/middleware/index.js +28 -0
  85. package/dist-cjs/agent/middleware/limits.d.ts +42 -0
  86. package/dist-cjs/agent/middleware/limits.js +57 -0
  87. package/dist-cjs/agent/middleware/models.d.ts +48 -0
  88. package/dist-cjs/agent/middleware/models.js +48 -0
  89. package/dist-cjs/agent/middleware/pii.d.ts +67 -0
  90. package/dist-cjs/agent/middleware/pii.js +178 -0
  91. package/dist-cjs/agent/middleware/retry.d.ts +66 -0
  92. package/dist-cjs/agent/middleware/retry.js +63 -0
  93. package/dist-cjs/agent/middleware/shared.d.ts +41 -0
  94. package/dist-cjs/agent/middleware/shared.js +83 -0
  95. package/dist-cjs/agent/middleware/tool-selector.d.ts +64 -0
  96. package/dist-cjs/agent/middleware/tool-selector.js +109 -0
  97. package/dist-cjs/agent/permissions.d.ts +113 -0
  98. package/dist-cjs/agent/permissions.js +189 -0
  99. package/dist-cjs/agent/sandbox.d.ts +151 -0
  100. package/dist-cjs/agent/sandbox.js +297 -0
  101. package/dist-cjs/agent/tool.d.ts +8 -3
  102. package/dist-cjs/agent/tool.js +14 -3
  103. package/dist-cjs/deep-agent/index.d.ts +109 -0
  104. package/dist-cjs/deep-agent/index.js +219 -0
  105. package/dist-cjs/graph/functional.d.ts +9 -0
  106. package/dist-cjs/graph/functional.js +20 -3
  107. package/dist-cjs/graph/graph.js +25 -4
  108. package/dist-cjs/graph/lint.d.ts +4 -1
  109. package/dist-cjs/graph/lint.js +35 -0
  110. package/dist-cjs/postgres/fulltext.d.ts +67 -0
  111. package/dist-cjs/postgres/fulltext.js +129 -0
  112. package/dist-cjs/rag/elasticsearch.d.ts +71 -0
  113. package/dist-cjs/rag/elasticsearch.js +135 -0
  114. package/dist-cjs/rag/pipeline.d.ts +140 -0
  115. package/dist-cjs/rag/pipeline.js +191 -0
  116. package/dist-cjs/rag/rerankers.d.ts +92 -0
  117. package/dist-cjs/rag/rerankers.js +107 -0
  118. package/dist-cjs/rag/retrievers.d.ts +12 -1
  119. package/dist-cjs/server/assistant.d.ts +5 -1
  120. package/dist-cjs/server/assistant.js +4 -2
  121. package/dist-cjs/server/auth.d.ts +250 -0
  122. package/dist-cjs/server/auth.js +402 -0
  123. package/dist-cjs/store/memory.d.ts +10 -0
  124. package/dist-cjs/store/memory.js +10 -0
  125. package/dist-cjs/types/graph.d.ts +38 -0
  126. package/dist-cjs/types/messages.d.ts +36 -2
  127. package/dist-cjs/types/principal.d.ts +21 -0
  128. package/dist-cjs/types/principal.js +2 -0
  129. package/dist-cjs/types/server.d.ts +2 -9
  130. package/dist-cjs/utils/tenant.d.ts +11 -0
  131. package/dist-cjs/utils/tenant.js +15 -0
  132. package/dist-cjs/version.d.ts +1 -1
  133. package/dist-cjs/version.js +1 -1
  134. package/llms.txt +4 -3
  135. package/package.json +118 -1
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Authentication for the agent server: JSON Web Tokens, API keys, and a trusted proxy.
3
+ *
4
+ * Each builds the `authenticate` hook of `createAgentServer()`, and each returns the same
5
+ * `Principal`: a subject, a tenant, roles, and scopes. That principal is what scopes threads, runs,
6
+ * budgets, and the store to a tenant, and what reaches every graph node, tool, and middleware of the
7
+ * run. Everything is built on Web Crypto, so there is no dependency and it runs wherever `fetch` does.
8
+ */
9
+ import type { webcrypto } from 'node:crypto';
10
+ import type { Principal } from '../types/principal.js';
11
+ type JsonWebKey = webcrypto.JsonWebKey;
12
+ type CryptoKey = webcrypto.CryptoKey;
13
+ /** The `authenticate` hook of `createAgentServer()`. */
14
+ export type AuthHook = (request: Request) => Promise<Principal | Response | undefined> | Principal | Response | undefined;
15
+ /** The signature algorithms a token may use. `none` is never accepted. */
16
+ export type JwtAlgorithm = 'HS256' | 'HS384' | 'HS512' | 'RS256' | 'RS384' | 'RS512' | 'PS256' | 'PS384' | 'PS512' | 'ES256' | 'ES384' | 'ES512' | 'EdDSA';
17
+ /** A token's claims, the registered ones typed. */
18
+ export interface JwtPayload {
19
+ /** The issuer. */
20
+ iss?: string;
21
+ /** The subject: who the token is about. */
22
+ sub?: string;
23
+ /** The audience: who the token is for. */
24
+ aud?: string | string[];
25
+ /** Expiry, in seconds since the epoch. */
26
+ exp?: number;
27
+ /** Not valid before, in seconds since the epoch. */
28
+ nbf?: number;
29
+ /** Issued at, in seconds since the epoch. */
30
+ iat?: number;
31
+ /** The token's id. */
32
+ jti?: string;
33
+ /** Every other claim. */
34
+ [claim: string]: unknown;
35
+ }
36
+ /** A token's header. */
37
+ export interface JwtHeader {
38
+ /** The signature algorithm. */
39
+ alg: string;
40
+ /** The id of the key that signed it, for choosing one from a key set. */
41
+ kid?: string;
42
+ /** The token type, usually `JWT`. */
43
+ typ?: string;
44
+ /** Any other header field. */
45
+ [field: string]: unknown;
46
+ }
47
+ /** A verified token. */
48
+ export interface VerifiedJwt {
49
+ /** Its header. */
50
+ header: JwtHeader;
51
+ /** Its claims, each checked. */
52
+ payload: JwtPayload;
53
+ }
54
+ /** Why a token was refused. */
55
+ export type JwtErrorCode = 'JWT_MALFORMED' | 'JWT_ALGORITHM' | 'JWT_KEY' | 'JWT_SIGNATURE' | 'JWT_EXPIRED' | 'JWT_NOT_YET_VALID' | 'JWT_TOO_OLD' | 'JWT_ISSUER' | 'JWT_AUDIENCE' | 'JWT_CLAIM';
56
+ /** Thrown when a token is refused: malformed, signed wrongly, expired, or for someone else. */
57
+ export declare class JwtError extends Error {
58
+ /** Why. */
59
+ readonly code: JwtErrorCode;
60
+ constructor(
61
+ /** Why. */
62
+ code: JwtErrorCode, message: string);
63
+ }
64
+ /** Options for `createJwtVerifier()`. Give it one key source: `secret`, `publicKey`, or `jwks`. */
65
+ export interface JwtVerifyOptions {
66
+ /** A shared secret, for HS256, HS384, and HS512. */
67
+ secret?: string | Uint8Array;
68
+ /** One public key: PEM (`-----BEGIN PUBLIC KEY-----`), a JWK, or a `CryptoKey`. */
69
+ publicKey?: string | JsonWebKey | CryptoKey;
70
+ /**
71
+ * The identity provider's key set: its URL, or the keys themselves. Fetched keys are cached, and
72
+ * a token naming a key not yet seen fetches the set again, so a provider's key rotation needs no
73
+ * restart.
74
+ */
75
+ jwks?: string | URL | {
76
+ keys: JsonWebKey[];
77
+ };
78
+ /**
79
+ * The algorithms accepted. Defaults to HS256, HS384, and HS512 for a secret and to every
80
+ * asymmetric algorithm for a public key or a key set. A shared secret is never accepted for an
81
+ * asymmetric algorithm, nor a public key for HMAC, so one cannot be passed off as the other.
82
+ */
83
+ algorithms?: readonly JwtAlgorithm[];
84
+ /** The issuer a token must name: one, or any of several. */
85
+ issuer?: string | readonly string[];
86
+ /**
87
+ * The audience a token must be for: one, or any of several. Required with `jwks`, where a
88
+ * provider signs tokens for every application it serves; `false` accepts any audience,
89
+ * deliberately.
90
+ */
91
+ audience?: string | readonly string[] | false;
92
+ /** Seconds of clock difference tolerated on `exp`, `nbf`, and `iat`. Defaults to 60. */
93
+ clockToleranceSec?: number;
94
+ /** The oldest a token may be, from its `iat`, in seconds. */
95
+ maxAgeSec?: number;
96
+ /** Requires an `exp` claim. Defaults to true: a token that never expires is refused. */
97
+ requireExpiry?: boolean;
98
+ /** Claims a token must carry, besides `exp`. */
99
+ requiredClaims?: readonly string[];
100
+ /** How long a fetched key set is kept, in milliseconds. Defaults to 10 minutes. */
101
+ jwksCacheMs?: number;
102
+ /**
103
+ * The least time between two fetches for keys not yet seen, in milliseconds, so tokens naming keys
104
+ * that do not exist cannot make the server hammer the provider. Defaults to 30 seconds.
105
+ */
106
+ jwksCooldownMs?: number;
107
+ /** Fetches the key set. Defaults to the global `fetch`. */
108
+ fetch?: (input: string | URL, init?: RequestInit) => Promise<Response>;
109
+ /** Replaces the clock, in milliseconds since the epoch, for tests. */
110
+ now?: () => number;
111
+ }
112
+ /** A reusable verifier: it keeps imported keys and the fetched key set between tokens. */
113
+ export interface JwtVerifier {
114
+ /** Verifies a token: its form, its signature, and its claims. Throws `JwtError` when it is refused. */
115
+ verify(token: string): Promise<VerifiedJwt>;
116
+ }
117
+ /**
118
+ * Builds a verifier for tokens from one issuer.
119
+ *
120
+ * ```ts
121
+ * const verifier = createJwtVerifier({
122
+ * jwks: 'https://login.example.com/.well-known/jwks.json',
123
+ * issuer: 'https://login.example.com/',
124
+ * audience: 'agents-api',
125
+ * });
126
+ * const { payload } = await verifier.verify(token);
127
+ * ```
128
+ */
129
+ export declare function createJwtVerifier(options: JwtVerifyOptions): JwtVerifier;
130
+ /** Which claims a principal is read from. A dot reaches into a nested claim: `realm_access.roles`. */
131
+ export interface JwtClaimNames {
132
+ /** The subject. Defaults to `sub`. */
133
+ subject?: string;
134
+ /** The tenant. Defaults to `tenant_id`. */
135
+ tenant?: string;
136
+ /** The roles, as a list or a space-separated string. Defaults to `roles`. */
137
+ roles?: string;
138
+ /** The scopes, as a space-separated string or a list. Defaults to `scope`, then `scp`. */
139
+ scopes?: string;
140
+ }
141
+ /** Options for `jwtAuth()`. */
142
+ export interface JwtAuthOptions extends JwtVerifyOptions {
143
+ /** Reads the token from a request. Defaults to `Authorization: Bearer <token>`; a cookie works too. */
144
+ token?: (request: Request) => string | undefined;
145
+ /** Which claims the principal is read from. */
146
+ claims?: JwtClaimNames;
147
+ /**
148
+ * Builds the principal from the verified claims instead, for a mapping of your own. Returning
149
+ * nothing refuses the request with 403: the token is valid, but not for this server.
150
+ */
151
+ principal?: (payload: JwtPayload, request: Request) => Principal | undefined | Promise<Principal | undefined>;
152
+ }
153
+ /**
154
+ * Authenticates requests by a JSON Web Token: an identity provider's, through its key set, or your
155
+ * own, signed with a shared secret.
156
+ *
157
+ * A request with no token is passed over, so another hook in `anyAuth()` can try. A token that is
158
+ * refused answers 401 with a `WWW-Authenticate` challenge naming why, and a valid one becomes a
159
+ * `Principal`: its subject, tenant, roles, and scopes, with `method: 'jwt'` and the verified claims.
160
+ *
161
+ * ```ts
162
+ * createAgentServer({
163
+ * assistants,
164
+ * authenticate: jwtAuth({
165
+ * jwks: 'https://login.example.com/.well-known/jwks.json',
166
+ * issuer: 'https://login.example.com/',
167
+ * audience: 'agents-api',
168
+ * claims: { tenant: 'org_id', roles: 'realm_access.roles' },
169
+ * }),
170
+ * });
171
+ * ```
172
+ */
173
+ export declare function jwtAuth(options: JwtAuthOptions): AuthHook;
174
+ /** Options for `apiKeyAuth()`. Give it `keys`, `lookup`, or both. */
175
+ export interface ApiKeyAuthOptions {
176
+ /**
177
+ * Keys by their SHA-256 hash, from `hashApiKey()`, each with the principal it stands for. The keys
178
+ * themselves are never written into configuration.
179
+ */
180
+ keys?: Readonly<Record<string, Principal>>;
181
+ /** Looks a key up by its hash, in a database or a secret store, when `keys` does not have it. */
182
+ lookup?: (hash: string, request: Request) => Principal | undefined | Promise<Principal | undefined>;
183
+ /** The header the key is read from. Defaults to `x-api-key`, then `Authorization: Bearer`. */
184
+ header?: string;
185
+ /**
186
+ * Only a credential with this prefix is taken for a key, such as `nxk_`, so a token in the same
187
+ * header is left to another hook. Without it, anything shaped like a JWT is left alone.
188
+ */
189
+ prefix?: string;
190
+ }
191
+ /** The SHA-256 of an API key, as hex: what `apiKeyAuth({ keys })` is configured with. */
192
+ export declare function hashApiKey(key: string): Promise<string>;
193
+ /**
194
+ * Authenticates requests by an API key, for services and scripts rather than people.
195
+ *
196
+ * Keys are compared by their hash, so configuration and the database hold hashes only. A request
197
+ * with no key is passed over; an unknown key answers 401. The principal is the one the key maps to,
198
+ * with `method: 'api-key'`.
199
+ *
200
+ * ```ts
201
+ * apiKeyAuth({ keys: { [await hashApiKey(process.env.CI_KEY)]: { userId: 'ci', tenantId: 'acme', scopes: ['write'] } } })
202
+ * ```
203
+ */
204
+ export declare function apiKeyAuth(options: ApiKeyAuthOptions): AuthHook;
205
+ /** The headers a proxy names the caller in. */
206
+ export interface TrustedProxyHeaders {
207
+ /** The user. Defaults to `x-forwarded-user`. */
208
+ user?: string;
209
+ /** The tenant. Defaults to `x-forwarded-tenant`. */
210
+ tenant?: string;
211
+ /** The roles. Defaults to `x-forwarded-roles`. */
212
+ roles?: string;
213
+ /** The scopes. Defaults to `x-forwarded-scopes`. */
214
+ scopes?: string;
215
+ }
216
+ /** Options for `trustedProxyAuth()`. Give it `secret`, `trust`, or both. */
217
+ export interface TrustedProxyAuthOptions {
218
+ /** A header the proxy sets to a shared secret, which proves the request came through it. */
219
+ secret?: {
220
+ header: string;
221
+ value: string;
222
+ };
223
+ /** Decides whether a request came through the proxy, by a check of your own. */
224
+ trust?: (request: Request) => boolean | Promise<boolean>;
225
+ /** The headers the caller is named in. */
226
+ headers?: TrustedProxyHeaders;
227
+ /** What separates several roles or scopes in one header. Defaults to a comma; spaces are trimmed. */
228
+ separator?: string;
229
+ }
230
+ /**
231
+ * Takes the caller from headers an authenticating proxy sets: an identity-aware proxy, an API
232
+ * gateway, a service mesh.
233
+ *
234
+ * Headers are only as trustworthy as the path a request took, so this refuses to exist without a
235
+ * proof that the proxy sent the request: a shared secret it adds, a check of your own, or both. A
236
+ * request without that proof, or without a user, is passed over. The principal carries
237
+ * `method: 'proxy'`.
238
+ */
239
+ export declare function trustedProxyAuth(options: TrustedProxyAuthOptions): AuthHook;
240
+ /**
241
+ * Tries hooks in order and takes the first principal: tokens for people and keys for services on
242
+ * one server. When none accepts the request, the first refusal any of them gave is the answer, so a
243
+ * caller learns why their credential was refused.
244
+ *
245
+ * ```ts
246
+ * authenticate: anyAuth(jwtAuth({ ... }), apiKeyAuth({ ... }))
247
+ * ```
248
+ */
249
+ export declare function anyAuth(...hooks: AuthHook[]): AuthHook;
250
+ export {};
@@ -0,0 +1,392 @@
1
+ import { assertTenantId } from '../utils/tenant.js';
2
+ export class JwtError extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(message);
6
+ this.code = code;
7
+ this.name = 'JwtError';
8
+ }
9
+ }
10
+ const HMAC = ['HS256', 'HS384', 'HS512'];
11
+ const ASYMMETRIC = [
12
+ 'RS256',
13
+ 'RS384',
14
+ 'RS512',
15
+ 'PS256',
16
+ 'PS384',
17
+ 'PS512',
18
+ 'ES256',
19
+ 'ES384',
20
+ 'ES512',
21
+ 'EdDSA',
22
+ ];
23
+ function cryptoParams(alg) {
24
+ const bits = alg.slice(2);
25
+ const hash = `SHA-${bits === 'DSA' ? '512' : bits}`;
26
+ switch (alg.slice(0, 2)) {
27
+ case 'HS':
28
+ return { importAs: { name: 'HMAC', hash }, verifyAs: { name: 'HMAC' } };
29
+ case 'RS':
30
+ return { importAs: { name: 'RSASSA-PKCS1-v1_5', hash }, verifyAs: { name: 'RSASSA-PKCS1-v1_5' } };
31
+ case 'PS':
32
+ return { importAs: { name: 'RSA-PSS', hash }, verifyAs: { name: 'RSA-PSS', saltLength: Number(bits) / 8 } };
33
+ case 'ES': {
34
+ const namedCurve = bits === '512' ? 'P-521' : `P-${bits}`;
35
+ return { importAs: { name: 'ECDSA', namedCurve }, verifyAs: { name: 'ECDSA', hash } };
36
+ }
37
+ default:
38
+ return { importAs: { name: 'Ed25519' }, verifyAs: { name: 'Ed25519' } };
39
+ }
40
+ }
41
+ function jwkFits(jwk, alg) {
42
+ if (jwk.alg && jwk.alg !== alg && !(alg === 'EdDSA' && jwk.alg === 'Ed25519'))
43
+ return false;
44
+ if (jwk.use && jwk.use !== 'sig')
45
+ return false;
46
+ if (jwk.key_ops && !jwk.key_ops.includes('verify'))
47
+ return false;
48
+ if (alg.startsWith('RS') || alg.startsWith('PS'))
49
+ return jwk.kty === 'RSA';
50
+ if (alg.startsWith('ES')) {
51
+ const curve = alg === 'ES512' ? 'P-521' : `P-${alg.slice(2)}`;
52
+ return jwk.kty === 'EC' && jwk.crv === curve;
53
+ }
54
+ if (alg === 'EdDSA')
55
+ return jwk.kty === 'OKP' && jwk.crv === 'Ed25519';
56
+ return false;
57
+ }
58
+ export function createJwtVerifier(options) {
59
+ const sources = [options.secret, options.publicKey, options.jwks].filter((source) => source !== undefined);
60
+ if (sources.length !== 1) {
61
+ throw new TypeError('createJwtVerifier() needs exactly one of secret, publicKey, or jwks');
62
+ }
63
+ if (options.jwks !== undefined && options.audience === undefined) {
64
+ throw new TypeError('A key set signs tokens for every application its provider serves: set audience, or audience: false to accept any');
65
+ }
66
+ const allowed = new Set((options.algorithms ?? (options.secret !== undefined ? HMAC : ASYMMETRIC)).filter((alg) => options.secret !== undefined ? HMAC.includes(alg) : ASYMMETRIC.includes(alg)));
67
+ if (allowed.size === 0)
68
+ throw new TypeError('None of the algorithms given can be used with this kind of key');
69
+ const now = options.now ?? Date.now;
70
+ const tolerance = options.clockToleranceSec ?? 60;
71
+ const subtle = globalThis.crypto.subtle;
72
+ const imported = new Map();
73
+ const importOnce = (cacheKey, load) => {
74
+ let key = imported.get(cacheKey);
75
+ if (!key) {
76
+ key = load();
77
+ key.catch(() => imported.delete(cacheKey));
78
+ imported.set(cacheKey, key);
79
+ }
80
+ return key;
81
+ };
82
+ let keySet = options.jwks !== undefined && typeof options.jwks === 'object' && !(options.jwks instanceof URL)
83
+ ? { keys: options.jwks.keys, fetchedAt: Number.POSITIVE_INFINITY }
84
+ : undefined;
85
+ let lastUnknownFetch = Number.NEGATIVE_INFINITY;
86
+ let inFlight;
87
+ const refresh = () => {
88
+ if (inFlight)
89
+ return inFlight;
90
+ const fetcher = options.fetch ?? globalThis.fetch;
91
+ inFlight = (async () => {
92
+ const response = await fetcher(String(options.jwks), { headers: { accept: 'application/json' } });
93
+ if (!response.ok)
94
+ throw new JwtError('JWT_KEY', `The key set answered ${response.status}`);
95
+ const body = (await response.json());
96
+ if (!Array.isArray(body.keys))
97
+ throw new JwtError('JWT_KEY', 'The key set has no keys');
98
+ keySet = { keys: body.keys, fetchedAt: now() };
99
+ imported.clear();
100
+ })().finally(() => {
101
+ inFlight = undefined;
102
+ });
103
+ return inFlight;
104
+ };
105
+ const keyFor = async (header, alg) => {
106
+ const { importAs } = cryptoParams(alg);
107
+ if (options.secret !== undefined) {
108
+ const raw = typeof options.secret === 'string' ? new TextEncoder().encode(options.secret) : options.secret;
109
+ return importOnce(`secret:${alg}`, () => subtle.importKey('raw', raw, importAs, false, ['verify']));
110
+ }
111
+ if (options.publicKey !== undefined) {
112
+ const key = options.publicKey;
113
+ if (typeof key === 'object' && 'type' in key && 'algorithm' in key)
114
+ return key;
115
+ if (typeof key === 'string') {
116
+ return importOnce(`pem:${alg}`, () => subtle.importKey('spki', pemToDer(key), importAs, false, ['verify']));
117
+ }
118
+ if (!jwkFits(key, alg))
119
+ throw new JwtError('JWT_KEY', `The key cannot verify ${alg}`);
120
+ return importOnce(`jwk:${alg}`, () => subtle.importKey('jwk', key, importAs, false, ['verify']));
121
+ }
122
+ const remote = typeof options.jwks === 'string' || options.jwks instanceof URL;
123
+ const cacheMs = options.jwksCacheMs ?? 10 * 60_000;
124
+ if (remote && (!keySet || now() - keySet.fetchedAt > cacheMs))
125
+ await refresh();
126
+ const find = () => keySet?.keys.filter((jwk) => (header.kid === undefined || jwk.kid === header.kid) && jwkFits(jwk, alg)) ?? [];
127
+ let matches = find();
128
+ if (matches.length === 0 && remote && now() - lastUnknownFetch >= (options.jwksCooldownMs ?? 30_000)) {
129
+ lastUnknownFetch = now();
130
+ await refresh();
131
+ matches = find();
132
+ }
133
+ const [jwk] = matches;
134
+ if (!jwk || (header.kid === undefined && matches.length > 1)) {
135
+ throw new JwtError('JWT_KEY', header.kid ? `No key "${header.kid}" for ${alg}` : `No single key for ${alg}`);
136
+ }
137
+ return importOnce(`jwks:${jwk.kid ?? ''}:${alg}`, () => subtle.importKey('jwk', jwk, importAs, false, ['verify']));
138
+ };
139
+ return {
140
+ async verify(token) {
141
+ const parts = token.split('.');
142
+ if (parts.length !== 3 || parts.some((part) => !/^[A-Za-z0-9_-]*$/.test(part)) || !parts[2]) {
143
+ throw new JwtError('JWT_MALFORMED', 'The token is not a signed JWT');
144
+ }
145
+ const [encodedHeader, encodedPayload, encodedSignature] = parts;
146
+ const header = decodeJson(encodedHeader, 'header');
147
+ const payload = decodeJson(encodedPayload, 'payload');
148
+ if (typeof header.alg !== 'string' || !allowed.has(header.alg)) {
149
+ throw new JwtError('JWT_ALGORITHM', `The algorithm "${String(header.alg)}" is not accepted`);
150
+ }
151
+ if (header.crit !== undefined)
152
+ throw new JwtError('JWT_MALFORMED', 'The token has critical headers this verifier does not understand');
153
+ const alg = header.alg;
154
+ const key = await keyFor(header, alg).catch((error) => {
155
+ throw error instanceof JwtError ? error : new JwtError('JWT_KEY', `No usable key for ${alg}`);
156
+ });
157
+ const valid = await subtle
158
+ .verify(cryptoParams(alg).verifyAs, key, base64UrlBytes(encodedSignature), new TextEncoder().encode(`${encodedHeader}.${encodedPayload}`))
159
+ .catch(() => false);
160
+ if (!valid)
161
+ throw new JwtError('JWT_SIGNATURE', 'The signature does not match');
162
+ checkClaims(payload, options, now() / 1000, tolerance);
163
+ return { header, payload };
164
+ },
165
+ };
166
+ }
167
+ function checkClaims(payload, options, seconds, tolerance) {
168
+ const numeric = (name) => {
169
+ const value = payload[name];
170
+ if (value !== undefined && typeof value !== 'number')
171
+ throw new JwtError('JWT_CLAIM', `"${name}" is not a number`);
172
+ return value;
173
+ };
174
+ const exp = numeric('exp');
175
+ const nbf = numeric('nbf');
176
+ const iat = numeric('iat');
177
+ if (exp === undefined && options.requireExpiry !== false)
178
+ throw new JwtError('JWT_CLAIM', 'The token has no expiry');
179
+ if (exp !== undefined && seconds >= exp + tolerance)
180
+ throw new JwtError('JWT_EXPIRED', 'The token has expired');
181
+ if (nbf !== undefined && seconds + tolerance < nbf)
182
+ throw new JwtError('JWT_NOT_YET_VALID', 'The token is not valid yet');
183
+ if (options.maxAgeSec !== undefined) {
184
+ if (iat === undefined)
185
+ throw new JwtError('JWT_CLAIM', 'The token has no issue time');
186
+ if (seconds - iat > options.maxAgeSec + tolerance)
187
+ throw new JwtError('JWT_TOO_OLD', 'The token is too old');
188
+ }
189
+ if (options.issuer !== undefined) {
190
+ const issuers = typeof options.issuer === 'string' ? [options.issuer] : options.issuer;
191
+ if (typeof payload.iss !== 'string' || !issuers.includes(payload.iss)) {
192
+ throw new JwtError('JWT_ISSUER', 'The token is from another issuer');
193
+ }
194
+ }
195
+ if (options.audience !== undefined && options.audience !== false) {
196
+ const wanted = typeof options.audience === 'string' ? [options.audience] : options.audience;
197
+ const audiences = typeof payload.aud === 'string' ? [payload.aud] : Array.isArray(payload.aud) ? payload.aud : [];
198
+ if (!audiences.some((audience) => wanted.includes(audience))) {
199
+ throw new JwtError('JWT_AUDIENCE', 'The token is for another audience');
200
+ }
201
+ }
202
+ for (const claim of options.requiredClaims ?? []) {
203
+ if (payload[claim] === undefined)
204
+ throw new JwtError('JWT_CLAIM', `The token has no "${claim}" claim`);
205
+ }
206
+ }
207
+ export function jwtAuth(options) {
208
+ const verifier = createJwtVerifier(options);
209
+ const read = options.token ?? bearerToken;
210
+ return async (request) => {
211
+ const token = read(request);
212
+ if (token?.split('.').length !== 3)
213
+ return undefined;
214
+ let payload;
215
+ try {
216
+ payload = (await verifier.verify(token)).payload;
217
+ }
218
+ catch (error) {
219
+ if (!(error instanceof JwtError))
220
+ throw error;
221
+ const description = error.message.replace(/["\\]/g, "'");
222
+ return refuse(401, error.message, `Bearer error="invalid_token", error_description="${description}"`);
223
+ }
224
+ try {
225
+ const principal = options.principal
226
+ ? await options.principal(payload, request)
227
+ : principalFromClaims(payload, options.claims);
228
+ if (!principal)
229
+ return refuse(403, 'The token is valid, but not for this server');
230
+ return { method: 'jwt', claims: payload, ...principal };
231
+ }
232
+ catch (error) {
233
+ return refuse(403, error instanceof Error ? error.message : String(error));
234
+ }
235
+ };
236
+ }
237
+ function principalFromClaims(payload, names = {}) {
238
+ const subject = claimAt(payload, names.subject ?? 'sub');
239
+ const tenant = claimAt(payload, names.tenant ?? 'tenant_id');
240
+ const roles = listOf(claimAt(payload, names.roles ?? 'roles'));
241
+ const scopes = listOf(names.scopes ? claimAt(payload, names.scopes) : (payload.scope ?? payload.scp));
242
+ return {
243
+ ...(typeof subject === 'string' ? { userId: subject } : {}),
244
+ ...(typeof tenant === 'string' ? { tenantId: assertTenantId(tenant) } : {}),
245
+ ...(roles ? { roles } : {}),
246
+ ...(scopes ? { scopes } : {}),
247
+ };
248
+ }
249
+ export async function hashApiKey(key) {
250
+ const digest = await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(key));
251
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('');
252
+ }
253
+ export function apiKeyAuth(options) {
254
+ if (!options.keys && !options.lookup)
255
+ throw new TypeError('apiKeyAuth() needs keys, a lookup, or both');
256
+ const header = options.header?.toLowerCase();
257
+ return async (request) => {
258
+ const key = header
259
+ ? (request.headers.get(header) ?? undefined)
260
+ : (request.headers.get('x-api-key') ?? bearerToken(request));
261
+ if (!key)
262
+ return undefined;
263
+ if (options.prefix ? !key.startsWith(options.prefix) : /^[\w-]+\.[\w-]+\.[\w-]+$/.test(key))
264
+ return undefined;
265
+ const hash = await hashApiKey(key);
266
+ const known = Object.hasOwn(options.keys ?? {}, hash) ? options.keys?.[hash] : undefined;
267
+ const principal = known ?? (await options.lookup?.(hash, request));
268
+ if (!principal)
269
+ return refuse(401, 'The API key is not valid', 'Bearer error="invalid_token"');
270
+ if (principal.tenantId !== undefined)
271
+ assertTenantId(principal.tenantId);
272
+ return { method: 'api-key', ...principal };
273
+ };
274
+ }
275
+ export function trustedProxyAuth(options) {
276
+ if (!options.secret && !options.trust) {
277
+ throw new TypeError('trustedProxyAuth() needs a secret or a trust check: otherwise anyone who sets the headers is trusted');
278
+ }
279
+ const names = {
280
+ user: 'x-forwarded-user',
281
+ tenant: 'x-forwarded-tenant',
282
+ roles: 'x-forwarded-roles',
283
+ scopes: 'x-forwarded-scopes',
284
+ ...options.headers,
285
+ };
286
+ const separator = options.separator ?? ',';
287
+ const list = (value) => value
288
+ ?.split(separator)
289
+ .map((item) => item.trim())
290
+ .filter(Boolean);
291
+ return async (request) => {
292
+ if (options.secret && !sameText(request.headers.get(options.secret.header) ?? '', options.secret.value)) {
293
+ return undefined;
294
+ }
295
+ if (options.trust && !(await options.trust(request)))
296
+ return undefined;
297
+ const user = request.headers.get(names.user);
298
+ if (!user)
299
+ return undefined;
300
+ const tenant = request.headers.get(names.tenant);
301
+ if (tenant && !/^[A-Za-z0-9_-]{1,128}$/.test(tenant))
302
+ return refuse(403, `"${tenant}" is not a valid tenant id`);
303
+ const roles = list(request.headers.get(names.roles));
304
+ const scopes = list(request.headers.get(names.scopes));
305
+ return {
306
+ method: 'proxy',
307
+ userId: user,
308
+ ...(tenant ? { tenantId: tenant } : {}),
309
+ ...(roles?.length ? { roles } : {}),
310
+ ...(scopes?.length ? { scopes } : {}),
311
+ };
312
+ };
313
+ }
314
+ export function anyAuth(...hooks) {
315
+ return async (request) => {
316
+ let refusal;
317
+ for (const hook of hooks) {
318
+ const result = await hook(request);
319
+ if (result instanceof Response)
320
+ refusal ??= result;
321
+ else if (result)
322
+ return result;
323
+ }
324
+ return refusal;
325
+ };
326
+ }
327
+ function bearerToken(request) {
328
+ const match = /^Bearer\s+(\S+)\s*$/i.exec(request.headers.get('authorization') ?? '');
329
+ return match?.[1];
330
+ }
331
+ function refuse(status, message, challenge) {
332
+ return new Response(JSON.stringify({ error: { code: status === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN', message } }), {
333
+ status,
334
+ headers: {
335
+ 'content-type': 'application/json; charset=utf-8',
336
+ ...(challenge ? { 'www-authenticate': challenge } : {}),
337
+ },
338
+ });
339
+ }
340
+ function sameText(left, right) {
341
+ let difference = left.length ^ right.length;
342
+ const length = Math.max(left.length, right.length);
343
+ for (let index = 0; index < length; index += 1) {
344
+ difference |= (left.charCodeAt(index) || 0) ^ (right.charCodeAt(index) || 0);
345
+ }
346
+ return difference === 0;
347
+ }
348
+ function base64UrlBytes(text) {
349
+ const base64 = text
350
+ .replace(/-/g, '+')
351
+ .replace(/_/g, '/')
352
+ .padEnd(Math.ceil(text.length / 4) * 4, '=');
353
+ const binary = atob(base64);
354
+ const bytes = new Uint8Array(binary.length);
355
+ for (let index = 0; index < binary.length; index += 1)
356
+ bytes[index] = binary.charCodeAt(index);
357
+ return bytes;
358
+ }
359
+ function decodeJson(part, what) {
360
+ try {
361
+ const value = JSON.parse(new TextDecoder().decode(base64UrlBytes(part)));
362
+ if (value && typeof value === 'object' && !Array.isArray(value))
363
+ return value;
364
+ }
365
+ catch {
366
+ }
367
+ throw new JwtError('JWT_MALFORMED', `The token's ${what} is not a JSON object`);
368
+ }
369
+ function pemToDer(pem) {
370
+ const body = pem.replace(/-----(?:BEGIN|END) PUBLIC KEY-----/g, '').replace(/\s+/g, '');
371
+ if (!body || pem.includes('PRIVATE KEY'))
372
+ throw new JwtError('JWT_KEY', 'The public key is not a PEM public key');
373
+ return base64UrlBytes(body.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')).buffer;
374
+ }
375
+ function claimAt(payload, path) {
376
+ if (path in payload)
377
+ return payload[path];
378
+ let current = payload;
379
+ for (const segment of path.split('.')) {
380
+ if (!current || typeof current !== 'object')
381
+ return undefined;
382
+ current = current[segment];
383
+ }
384
+ return current;
385
+ }
386
+ function listOf(value) {
387
+ if (typeof value === 'string')
388
+ return value.split(/\s+/).filter(Boolean);
389
+ if (Array.isArray(value))
390
+ return value.filter((item) => typeof item === 'string');
391
+ return undefined;
392
+ }
@@ -20,6 +20,10 @@ export declare class MemoryStore implements Store {
20
20
  private readonly options;
21
21
  private readonly items;
22
22
  private readonly maxItems;
23
+ /** Whether any item was ever given a time to live; until one is, there is nothing to sweep. */
24
+ private expiring;
25
+ /** When expired items were last swept, in milliseconds. */
26
+ private sweptAt;
23
27
  private readonly now;
24
28
  constructor(options?: MemoryStoreOptions);
25
29
  /** Stores an item, keeping its original `createdAt` when it replaces one. */
@@ -42,5 +46,11 @@ export declare class MemoryStore implements Store {
42
46
  size(): number;
43
47
  private embed;
44
48
  private expired;
49
+ /**
50
+ * Drops expired items. Reads already skip them, so this only reclaims memory: it runs once any item
51
+ * has a time to live, and at most once a second, so a write stays constant-time however many items
52
+ * there are. A store that is full sweeps before it evicts, so a live item never makes way for an
53
+ * expired one.
54
+ */
45
55
  private sweep;
46
56
  }