@idp.global/sdk 17.0.0 → 17.1.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.
package/changelog.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026-09-13 - 17.1.0
4
+
5
+ ### Fixes
6
+
7
+ - Update release tooling, require build and test preflight, and qualify account persistence against NoSQLDB 10.2.0.
8
+
9
+ ### Features
10
+
11
+ - Add a subject-bound server UserInfo client with bounded transport and validated organization display claims.
12
+
3
13
  ## 2026-09-09 - 17.0.0
4
14
 
5
15
  ### Breaking Changes
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@idp.global/sdk',
6
- version: '17.0.0',
6
+ version: '17.1.0',
7
7
  description: 'Reusable browser and server SDK for idp.global authentication.'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSxpQkFBaUI7SUFDdkIsT0FBTyxFQUFFLFFBQVE7SUFDakIsV0FBVyxFQUFFLGdFQUFnRTtDQUM5RSxDQUFBIn0=
@@ -0,0 +1,22 @@
1
+ import * as plugins from './plugins.js';
2
+ export interface IOidcUserInfoClientOptions {
3
+ issuer: string;
4
+ timeoutMs?: number;
5
+ maxResponseBytes?: number;
6
+ }
7
+ export interface IOidcUserInfoClientRequest {
8
+ token: string;
9
+ /** Subject already established by the application's verified OIDC login. */
10
+ subject: string;
11
+ }
12
+ export declare class OidcUserInfoError extends Error {
13
+ readonly code: 'invalid_request' | 'invalid_response' | 'unauthenticated' | 'unavailable';
14
+ constructor(code: 'invalid_request' | 'invalid_response' | 'unauthenticated' | 'unavailable');
15
+ }
16
+ /** Fetches display claims using a server-held token; results are never authorization grants. */
17
+ export declare class OidcUserInfoClient {
18
+ #private;
19
+ constructor(optionsArg: IOidcUserInfoClientOptions);
20
+ getUserInfo(requestArg: IOidcUserInfoClientRequest): Promise<plugins.idpInterfaces.data.IUserInfoResponse>;
21
+ private validateResponse;
22
+ }
@@ -0,0 +1,154 @@
1
+ import * as plugins from './plugins.js';
2
+ export class OidcUserInfoError extends Error {
3
+ code;
4
+ constructor(code) {
5
+ super(`OIDC userinfo ${code.replace(/_/g, ' ')}.`);
6
+ this.code = code;
7
+ this.name = 'OidcUserInfoError';
8
+ }
9
+ }
10
+ const record = (valueArg) => valueArg !== null && typeof valueArg === 'object' && !Array.isArray(valueArg);
11
+ const exactString = (valueArg, maximumArg = 512) => typeof valueArg === 'string' && valueArg.length > 0 && valueArg.length <= maximumArg
12
+ && valueArg.trim() === valueArg && !/[\u0000-\u001f\u007f]/.test(valueArg);
13
+ const strings = (valueArg) => Array.isArray(valueArg) && valueArg.every((entryArg) => exactString(entryArg))
14
+ && new Set(valueArg).size === valueArg.length;
15
+ /** Fetches display claims using a server-held token; results are never authorization grants. */
16
+ export class OidcUserInfoClient {
17
+ #endpoint;
18
+ #timeoutMs;
19
+ #maxResponseBytes;
20
+ constructor(optionsArg) {
21
+ if (!optionsArg || !exactString(optionsArg.issuer, 2048))
22
+ throw new OidcUserInfoError('invalid_request');
23
+ let issuer;
24
+ try {
25
+ issuer = new URL(optionsArg.issuer);
26
+ }
27
+ catch {
28
+ throw new OidcUserInfoError('invalid_request');
29
+ }
30
+ const canonicalIssuer = issuer.origin + (issuer.pathname === '/' ? '' : issuer.pathname);
31
+ if (issuer.protocol !== 'https:' || issuer.username || issuer.password || issuer.search || issuer.hash
32
+ || issuer.pathname.includes('//') || issuer.pathname.endsWith('/') && issuer.pathname !== '/'
33
+ || canonicalIssuer !== optionsArg.issuer)
34
+ throw new OidcUserInfoError('invalid_request');
35
+ this.#timeoutMs = optionsArg.timeoutMs ?? 5000;
36
+ this.#maxResponseBytes = optionsArg.maxResponseBytes ?? 65536;
37
+ if (!Number.isSafeInteger(this.#timeoutMs) || this.#timeoutMs < 1 || this.#timeoutMs > 30000
38
+ || !Number.isSafeInteger(this.#maxResponseBytes) || this.#maxResponseBytes < 1 || this.#maxResponseBytes > 1048576) {
39
+ throw new OidcUserInfoError('invalid_request');
40
+ }
41
+ this.#endpoint = `${canonicalIssuer}/oauth/userinfo`;
42
+ }
43
+ async getUserInfo(requestArg) {
44
+ if (!requestArg || !exactString(requestArg.subject) || !exactString(requestArg.token, 16384)
45
+ || !/^[A-Za-z0-9\-._~+/]+=*$/.test(requestArg.token))
46
+ throw new OidcUserInfoError('invalid_request');
47
+ // Snapshot the principal and token before any asynchronous work.
48
+ const { token, subject } = requestArg;
49
+ const controller = new AbortController();
50
+ let timer;
51
+ const deadline = new Promise((_resolveArg, rejectArg) => {
52
+ timer = setTimeout(() => {
53
+ controller.abort();
54
+ rejectArg(new OidcUserInfoError('unavailable'));
55
+ }, this.#timeoutMs);
56
+ });
57
+ let reader;
58
+ try {
59
+ const response = await Promise.race([fetch(this.#endpoint, {
60
+ method: 'GET', redirect: 'error', credentials: 'omit', cache: 'no-store', signal: controller.signal,
61
+ headers: { authorization: `Bearer ${token}`, accept: 'application/json' },
62
+ }), deadline]);
63
+ if (response.status === 401 || response.status === 403)
64
+ throw new OidcUserInfoError('unauthenticated');
65
+ if (response.status !== 200)
66
+ throw new OidcUserInfoError('unavailable');
67
+ if (response.headers.get('content-type')?.split(';')[0].trim().toLowerCase() !== 'application/json'
68
+ || !response.headers.get('cache-control')?.toLowerCase().split(',').map((partArg) => partArg.trim()).includes('no-store')
69
+ || !response.body)
70
+ throw new OidcUserInfoError('invalid_response');
71
+ const contentLength = response.headers.get('content-length');
72
+ if (contentLength !== null && (!/^\d+$/.test(contentLength) || Number(contentLength) > this.#maxResponseBytes)) {
73
+ throw new OidcUserInfoError('invalid_response');
74
+ }
75
+ reader = response.body.getReader();
76
+ const chunks = [];
77
+ let size = 0;
78
+ while (true) {
79
+ const part = await Promise.race([reader.read(), deadline]);
80
+ if (part.done)
81
+ break;
82
+ size += part.value.byteLength;
83
+ if (size > this.#maxResponseBytes)
84
+ throw new OidcUserInfoError('invalid_response');
85
+ chunks.push(part.value);
86
+ }
87
+ let value;
88
+ try {
89
+ value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)));
90
+ }
91
+ catch {
92
+ throw new OidcUserInfoError('invalid_response');
93
+ }
94
+ return this.validateResponse(value, subject);
95
+ }
96
+ catch (errorArg) {
97
+ if (errorArg instanceof OidcUserInfoError)
98
+ throw errorArg;
99
+ // Transport errors can contain credentials, URLs or response bodies.
100
+ throw new OidcUserInfoError('unavailable');
101
+ }
102
+ finally {
103
+ clearTimeout(timer);
104
+ controller.abort();
105
+ if (reader)
106
+ void reader.cancel().catch(() => undefined);
107
+ }
108
+ }
109
+ validateResponse(valueArg, subjectArg) {
110
+ const invalid = () => new OidcUserInfoError('invalid_response');
111
+ if (!record(valueArg) || valueArg.sub !== subjectArg)
112
+ throw invalid();
113
+ const result = { sub: subjectArg };
114
+ for (const key of ['name', 'preferred_username', 'picture', 'email']) {
115
+ if (!Object.hasOwn(valueArg, key))
116
+ continue;
117
+ const value = valueArg[key];
118
+ if (typeof value !== 'string' || value.length > 4096)
119
+ throw invalid();
120
+ result[key] = value;
121
+ }
122
+ if (Object.hasOwn(valueArg, 'email_verified')) {
123
+ if (typeof valueArg.email_verified !== 'boolean')
124
+ throw invalid();
125
+ result.email_verified = valueArg.email_verified;
126
+ }
127
+ for (const key of ['roles', 'app_roles', 'app_permissions', 'app_scopes']) {
128
+ if (!Object.hasOwn(valueArg, key))
129
+ continue;
130
+ const value = valueArg[key];
131
+ if (!strings(value))
132
+ throw invalid();
133
+ result[key] = [...value];
134
+ }
135
+ if (Object.hasOwn(valueArg, 'organizations')) {
136
+ if (!Array.isArray(valueArg.organizations))
137
+ throw invalid();
138
+ const seen = new Set();
139
+ result.organizations = valueArg.organizations.map((organizationArg) => {
140
+ if (!record(organizationArg) || !exactString(organizationArg.id)
141
+ || typeof organizationArg.name !== 'string' || organizationArg.name.length > 4096
142
+ || !exactString(organizationArg.slug) || !strings(organizationArg.roles)
143
+ || seen.has(organizationArg.id)
144
+ || Object.keys(organizationArg).some((keyArg) => !['id', 'name', 'slug', 'roles'].includes(keyArg)))
145
+ throw invalid();
146
+ seen.add(organizationArg.id);
147
+ return { id: organizationArg.id, name: organizationArg.name, slug: organizationArg.slug, roles: [...organizationArg.roles] };
148
+ });
149
+ }
150
+ // OIDC allows additional claims; only the documented, validated fields leave the SDK.
151
+ return result;
152
+ }
153
+ }
154
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5vaWRjLXVzZXJpbmZvLWNsaWVudC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzX3NlcnZlci9jbGFzc2VzLm9pZGMtdXNlcmluZm8tY2xpZW50LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sY0FBYyxDQUFDO0FBY3hDLE1BQU0sT0FBTyxpQkFBa0IsU0FBUSxLQUFLO0lBQ2Q7SUFBNUIsWUFBNEIsSUFBZ0Y7UUFDMUcsS0FBSyxDQUFDLGlCQUFpQixJQUFJLENBQUMsT0FBTyxDQUFDLElBQUksRUFBRSxHQUFHLENBQUMsR0FBRyxDQUFDLENBQUM7UUFEekIsU0FBSSxHQUFKLElBQUksQ0FBNEU7UUFFMUcsSUFBSSxDQUFDLElBQUksR0FBRyxtQkFBbUIsQ0FBQztJQUNsQyxDQUFDO0NBQ0Y7QUFFRCxNQUFNLE1BQU0sR0FBRyxDQUFDLFFBQWlCLEVBQXVDLEVBQUUsQ0FDeEUsUUFBUSxLQUFLLElBQUksSUFBSSxPQUFPLFFBQVEsS0FBSyxRQUFRLElBQUksQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxDQUFDO0FBQ2hGLE1BQU0sV0FBVyxHQUFHLENBQUMsUUFBaUIsRUFBRSxVQUFVLEdBQUcsR0FBRyxFQUFzQixFQUFFLENBQzlFLE9BQU8sUUFBUSxLQUFLLFFBQVEsSUFBSSxRQUFRLENBQUMsTUFBTSxHQUFHLENBQUMsSUFBSSxRQUFRLENBQUMsTUFBTSxJQUFJLFVBQVU7T0FDakYsUUFBUSxDQUFDLElBQUksRUFBRSxLQUFLLFFBQVEsSUFBSSxDQUFDLHVCQUF1QixDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsQ0FBQztBQUM3RSxNQUFNLE9BQU8sR0FBRyxDQUFDLFFBQWlCLEVBQXdCLEVBQUUsQ0FDMUQsS0FBSyxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUMsSUFBSSxRQUFRLENBQUMsS0FBSyxDQUFDLENBQUMsUUFBUSxFQUFFLEVBQUUsQ0FBQyxXQUFX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
@@ -4,4 +4,5 @@ export * from './classes.account-doc.js';
4
4
  export * from './classes.account-store.js';
5
5
  export * from './classes.idp-global-server-client.js';
6
6
  export * from './classes.oidc-introspection-client.js';
7
+ export * from './classes.oidc-userinfo-client.js';
7
8
  export * from './classes.password-hasher.js';
@@ -4,5 +4,6 @@ export * from './classes.account-doc.js';
4
4
  export * from './classes.account-store.js';
5
5
  export * from './classes.idp-global-server-client.js';
6
6
  export * from './classes.oidc-introspection-client.js';
7
+ export * from './classes.oidc-userinfo-client.js';
7
8
  export * from './classes.password-hasher.js';
8
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19zZXJ2ZXIvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLG1DQUFtQyxDQUFDO0FBQ2xELGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLHVDQUF1QyxDQUFDO0FBQ3RELGNBQWMsd0NBQXdDLENBQUM7QUFDdkQsY0FBYyw4QkFBOEIsQ0FBQyJ9
9
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19zZXJ2ZXIvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLG1DQUFtQyxDQUFDO0FBQ2xELGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLHVDQUF1QyxDQUFDO0FBQ3RELGNBQWMsd0NBQXdDLENBQUM7QUFDdkQsY0FBYyxtQ0FBbUMsQ0FBQztBQUNsRCxjQUFjLDhCQUE4QixDQUFDIn0=
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idp.global/sdk",
3
- "version": "17.0.0",
3
+ "version": "17.1.0",
4
4
  "private": false,
5
5
  "description": "Reusable browser and server SDK for idp.global authentication.",
6
6
  "type": "module",
@@ -33,9 +33,9 @@
33
33
  "@git.zone/tsbuild": "^4.4.2",
34
34
  "@git.zone/tsdoc": "^2.2.0",
35
35
  "@git.zone/tsrun": "^2.0.6",
36
- "@git.zone/tstest": "^5.0.0",
37
- "@push.rocks/smartdb": "^7.0.1",
38
- "@types/node": "26.4.1"
36
+ "@git.zone/tstest": "^6.0.0",
37
+ "@types/node": "26.4.1",
38
+ "@lossless.org/nosqldb": "10.2.0"
39
39
  },
40
40
  "files": [
41
41
  "ts/**/*",
@@ -67,7 +67,7 @@
67
67
  ],
68
68
  "scripts": {
69
69
  "test": "pnpm run build && tstest test/ --verbose --logfile --timeout 60",
70
- "build": "tsbuild tsfolders --web --allowimplicitany",
70
+ "build": "tsbuild tsfolders --web",
71
71
  "buildDocs": "tsdoc"
72
72
  }
73
73
  }
package/readme.md CHANGED
@@ -338,6 +338,41 @@ existing login and principal binding and make a fresh observation for each
338
338
  operation requiring live authority. The response contains no identity profile
339
339
  or email-based organization inference.
340
340
 
341
+ ### Server-held OIDC UserInfo
342
+
343
+ Use `OidcUserInfoClient` to obtain current profile and organization picker data
344
+ with an access token held by your application's server session:
345
+
346
+ ```typescript
347
+ import { OidcUserInfoClient } from '@idp.global/sdk/server';
348
+
349
+ const userInfoClient = new OidcUserInfoClient({ issuer: 'https://idp.global' });
350
+ const claims = await userInfoClient.getUserInfo({
351
+ token: serverSession.accessToken,
352
+ subject: serverSession.idpSubject,
353
+ });
354
+ const ownerOrganizations = claims.organizations?.filter((organization) =>
355
+ organization.roles.includes('owner')) ?? [];
356
+ ```
357
+
358
+ The expected subject must come from the verified OIDC login. Keep the token
359
+ server-side; the SDK sends it only in the bearer header to the issuer's
360
+ `/oauth/userinfo` endpoint, rejects redirects, and validates the returned subject
361
+ and documented claims. Unknown top-level OIDC extension claims are omitted.
362
+ The client retains no access token or claim cache. Calls have a five-second
363
+ deadline, including body reads, and a 64 KiB response limit. Options allow a
364
+ deadline up to 30 seconds and a response limit up to 1 MiB.
365
+
366
+ Organization claims are display data within the token's original organization
367
+ grant. Newly granted organizations require a new OIDC login; removed memberships
368
+ or app connections disappear from subsequent responses. Authorize each protected
369
+ operation separately with fresh `OidcIntrospectionClient` introspection. UserInfo
370
+ does not turn a picker selection or an observed role into an authorization grant.
371
+
372
+ `OidcUserInfoError.code` is `invalid_request`, `invalid_response`,
373
+ `unauthenticated`, or `unavailable`. Provider 401/403 responses indicate that the
374
+ server-held token is no longer accepted. Errors omit token and provider content.
375
+
341
376
  ### Server API Surface
342
377
 
343
378
  | API | Description |
@@ -347,6 +382,8 @@ or email-based organization inference.
347
382
  | `IdpGlobalServerClient` | Typed-socket client for idp.global password login, JWT refresh, `whoIs` lookup, backend JWT validation fetches, and JWT key/blocklist pushes. |
348
383
  | `OidcIntrospectionClient` | Confidential server client for an exact live organization authority observation. |
349
384
  | `OidcIntrospectionError` | Sanitized introspection failure with a stable error code. |
385
+ | `OidcUserInfoClient` | Subject-bound profile and organization display claims using a server-held OIDC token. |
386
+ | `OidcUserInfoError` | Sanitized UserInfo failure with a stable error code. |
350
387
  | `IdpMfaRequiredError` | Password-login error carrying the MFA challenge token and available factor methods. |
351
388
  | `defaultIdpGlobalBaseUrl` | Default hosted server origin: `https://app.idp.global`. |
352
389
  | `PasswordHasher` | Static `hashPassword()` and `verifyPassword()` helpers using `scrypt:v1`. |
@@ -361,6 +398,8 @@ The server export includes these TypeScript types:
361
398
  | --- | --- |
362
399
  | `IOidcIntrospectionClientOptions` | Exact HTTPS issuer, confidential client credentials, and optional deadline/response limits. |
363
400
  | `IOidcIntrospectionClientRequest` | Server-held access token, exact subject, and canonical organization ID. |
401
+ | `IOidcUserInfoClientOptions` | Exact HTTPS issuer and optional deadline/response limits. |
402
+ | `IOidcUserInfoClientRequest` | Server-held access token and the subject established by verified OIDC login. |
364
403
  | `TIdpAccountAuthSource` | `'local' | 'idp.global'` |
365
404
  | `TIdpAccountRole` | `'admin' | 'user'` |
366
405
  | `TIdpAccountStatus` | `'active' | 'disabled'` |
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@idp.global/sdk',
6
- version: '17.0.0',
6
+ version: '17.1.0',
7
7
  description: 'Reusable browser and server SDK for idp.global authentication.'
8
8
  }
@@ -0,0 +1,143 @@
1
+ import * as plugins from './plugins.js';
2
+
3
+ export interface IOidcUserInfoClientOptions {
4
+ issuer: string;
5
+ timeoutMs?: number;
6
+ maxResponseBytes?: number;
7
+ }
8
+
9
+ export interface IOidcUserInfoClientRequest {
10
+ token: string;
11
+ /** Subject already established by the application's verified OIDC login. */
12
+ subject: string;
13
+ }
14
+
15
+ export class OidcUserInfoError extends Error {
16
+ constructor(public readonly code: 'invalid_request' | 'invalid_response' | 'unauthenticated' | 'unavailable') {
17
+ super(`OIDC userinfo ${code.replace(/_/g, ' ')}.`);
18
+ this.name = 'OidcUserInfoError';
19
+ }
20
+ }
21
+
22
+ const record = (valueArg: unknown): valueArg is Record<string, unknown> =>
23
+ valueArg !== null && typeof valueArg === 'object' && !Array.isArray(valueArg);
24
+ const exactString = (valueArg: unknown, maximumArg = 512): valueArg is string =>
25
+ typeof valueArg === 'string' && valueArg.length > 0 && valueArg.length <= maximumArg
26
+ && valueArg.trim() === valueArg && !/[\u0000-\u001f\u007f]/.test(valueArg);
27
+ const strings = (valueArg: unknown): valueArg is string[] =>
28
+ Array.isArray(valueArg) && valueArg.every((entryArg) => exactString(entryArg))
29
+ && new Set(valueArg).size === valueArg.length;
30
+
31
+ /** Fetches display claims using a server-held token; results are never authorization grants. */
32
+ export class OidcUserInfoClient {
33
+ readonly #endpoint: string;
34
+ readonly #timeoutMs: number;
35
+ readonly #maxResponseBytes: number;
36
+
37
+ constructor(optionsArg: IOidcUserInfoClientOptions) {
38
+ if (!optionsArg || !exactString(optionsArg.issuer, 2048)) throw new OidcUserInfoError('invalid_request');
39
+ let issuer: URL;
40
+ try { issuer = new URL(optionsArg.issuer); } catch { throw new OidcUserInfoError('invalid_request'); }
41
+ const canonicalIssuer = issuer.origin + (issuer.pathname === '/' ? '' : issuer.pathname);
42
+ if (issuer.protocol !== 'https:' || issuer.username || issuer.password || issuer.search || issuer.hash
43
+ || issuer.pathname.includes('//') || issuer.pathname.endsWith('/') && issuer.pathname !== '/'
44
+ || canonicalIssuer !== optionsArg.issuer) throw new OidcUserInfoError('invalid_request');
45
+ this.#timeoutMs = optionsArg.timeoutMs ?? 5000;
46
+ this.#maxResponseBytes = optionsArg.maxResponseBytes ?? 65536;
47
+ if (!Number.isSafeInteger(this.#timeoutMs) || this.#timeoutMs < 1 || this.#timeoutMs > 30000
48
+ || !Number.isSafeInteger(this.#maxResponseBytes) || this.#maxResponseBytes < 1 || this.#maxResponseBytes > 1048576) {
49
+ throw new OidcUserInfoError('invalid_request');
50
+ }
51
+ this.#endpoint = `${canonicalIssuer}/oauth/userinfo`;
52
+ }
53
+
54
+ public async getUserInfo(requestArg: IOidcUserInfoClientRequest): Promise<plugins.idpInterfaces.data.IUserInfoResponse> {
55
+ if (!requestArg || !exactString(requestArg.subject) || !exactString(requestArg.token, 16384)
56
+ || !/^[A-Za-z0-9\-._~+/]+=*$/.test(requestArg.token)) throw new OidcUserInfoError('invalid_request');
57
+ // Snapshot the principal and token before any asynchronous work.
58
+ const { token, subject } = requestArg;
59
+ const controller = new AbortController();
60
+ let timer: ReturnType<typeof setTimeout>;
61
+ const deadline = new Promise<never>((_resolveArg, rejectArg) => {
62
+ timer = setTimeout(() => {
63
+ controller.abort();
64
+ rejectArg(new OidcUserInfoError('unavailable'));
65
+ }, this.#timeoutMs);
66
+ });
67
+ let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
68
+ try {
69
+ const response = await Promise.race([fetch(this.#endpoint, {
70
+ method: 'GET', redirect: 'error', credentials: 'omit', cache: 'no-store', signal: controller.signal,
71
+ headers: { authorization: `Bearer ${token}`, accept: 'application/json' },
72
+ }), deadline]);
73
+ if (response.status === 401 || response.status === 403) throw new OidcUserInfoError('unauthenticated');
74
+ if (response.status !== 200) throw new OidcUserInfoError('unavailable');
75
+ if (response.headers.get('content-type')?.split(';')[0].trim().toLowerCase() !== 'application/json'
76
+ || !response.headers.get('cache-control')?.toLowerCase().split(',').map((partArg) => partArg.trim()).includes('no-store')
77
+ || !response.body) throw new OidcUserInfoError('invalid_response');
78
+ const contentLength = response.headers.get('content-length');
79
+ if (contentLength !== null && (!/^\d+$/.test(contentLength) || Number(contentLength) > this.#maxResponseBytes)) {
80
+ throw new OidcUserInfoError('invalid_response');
81
+ }
82
+ reader = response.body.getReader();
83
+ const chunks: Uint8Array[] = [];
84
+ let size = 0;
85
+ while (true) {
86
+ const part = await Promise.race([reader.read(), deadline]);
87
+ if (part.done) break;
88
+ size += part.value.byteLength;
89
+ if (size > this.#maxResponseBytes) throw new OidcUserInfoError('invalid_response');
90
+ chunks.push(part.value);
91
+ }
92
+ let value: unknown;
93
+ try { value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks))); }
94
+ catch { throw new OidcUserInfoError('invalid_response'); }
95
+ return this.validateResponse(value, subject);
96
+ } catch (errorArg) {
97
+ if (errorArg instanceof OidcUserInfoError) throw errorArg;
98
+ // Transport errors can contain credentials, URLs or response bodies.
99
+ throw new OidcUserInfoError('unavailable');
100
+ } finally {
101
+ clearTimeout(timer);
102
+ controller.abort();
103
+ if (reader) void reader.cancel().catch(() => undefined);
104
+ }
105
+ }
106
+
107
+ private validateResponse(valueArg: unknown, subjectArg: string): plugins.idpInterfaces.data.IUserInfoResponse {
108
+ const invalid = () => new OidcUserInfoError('invalid_response');
109
+ if (!record(valueArg) || valueArg.sub !== subjectArg) throw invalid();
110
+ const result: plugins.idpInterfaces.data.IUserInfoResponse = { sub: subjectArg };
111
+ for (const key of ['name', 'preferred_username', 'picture', 'email'] as const) {
112
+ if (!Object.hasOwn(valueArg, key)) continue;
113
+ const value = valueArg[key];
114
+ if (typeof value !== 'string' || value.length > 4096) throw invalid();
115
+ result[key] = value;
116
+ }
117
+ if (Object.hasOwn(valueArg, 'email_verified')) {
118
+ if (typeof valueArg.email_verified !== 'boolean') throw invalid();
119
+ result.email_verified = valueArg.email_verified;
120
+ }
121
+ for (const key of ['roles', 'app_roles', 'app_permissions', 'app_scopes'] as const) {
122
+ if (!Object.hasOwn(valueArg, key)) continue;
123
+ const value = valueArg[key];
124
+ if (!strings(value)) throw invalid();
125
+ result[key] = [...value];
126
+ }
127
+ if (Object.hasOwn(valueArg, 'organizations')) {
128
+ if (!Array.isArray(valueArg.organizations)) throw invalid();
129
+ const seen = new Set<string>();
130
+ result.organizations = valueArg.organizations.map((organizationArg: unknown) => {
131
+ if (!record(organizationArg) || !exactString(organizationArg.id)
132
+ || typeof organizationArg.name !== 'string' || organizationArg.name.length > 4096
133
+ || !exactString(organizationArg.slug) || !strings(organizationArg.roles)
134
+ || seen.has(organizationArg.id)
135
+ || Object.keys(organizationArg).some((keyArg) => !['id', 'name', 'slug', 'roles'].includes(keyArg))) throw invalid();
136
+ seen.add(organizationArg.id);
137
+ return { id: organizationArg.id, name: organizationArg.name, slug: organizationArg.slug, roles: [...organizationArg.roles] };
138
+ });
139
+ }
140
+ // OIDC allows additional claims; only the documented, validated fields leave the SDK.
141
+ return result;
142
+ }
143
+ }
@@ -4,4 +4,5 @@ export * from './classes.account-doc.js';
4
4
  export * from './classes.account-store.js';
5
5
  export * from './classes.idp-global-server-client.js';
6
6
  export * from './classes.oidc-introspection-client.js';
7
+ export * from './classes.oidc-userinfo-client.js';
7
8
  export * from './classes.password-hasher.js';