@idp.global/sdk 16.1.1 → 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,25 @@
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
+
13
+ ## 2026-09-09 - 17.0.0
14
+
15
+ ### Breaking Changes
16
+
17
+ - migrate SDK transports to TypedRequest 8 and TypedSocket 8 (transport)
18
+ - Upgrades the typed transport dependency stack, including TypedRequest, TypedSocket, SmartServe, SmartDB, and TypedServer test support.
19
+ - Adds native transport integration coverage for server handshake, login, public key retrieval, targeted public key pushes, and browser HTTP/WebSocket RPCs.
20
+ - Updates account-store test fixtures to use an in-memory SmartdbServer with cleanup coverage for failed startup paths.
21
+ - Documents that SDK clients and IdP listeners must be upgraded together because earlier transport majors are incompatible.
22
+
3
23
  ## 2026-09-09 - 16.1.1
4
24
 
5
25
  ### Fixes
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@idp.global/sdk',
6
- version: '16.1.1',
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": "16.1.1",
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",
@@ -14,27 +14,28 @@
14
14
  "author": "Task Venture Capital GmbH",
15
15
  "license": "MIT",
16
16
  "dependencies": {
17
- "@api.global/typedrequest": "^5.2.1",
18
- "@api.global/typedrequest-interfaces": "^5.1.1",
19
- "@api.global/typedsocket": "^6.0.0",
17
+ "@api.global/typedrequest": "^8.0.4",
18
+ "@api.global/typedrequest-interfaces": "^7.1.0",
19
+ "@api.global/typedsocket": "^8.1.0",
20
20
  "@idp.global/interfaces": "^29.1.0",
21
21
  "@push.rocks/smartdata": "^11.7.0",
22
22
  "@push.rocks/smartjson": "^6.0.1",
23
23
  "@push.rocks/smartpromise": "^4.2.4",
24
24
  "@push.rocks/smartrx": "^3.0.12",
25
- "@push.rocks/smartserve": "^4.3.0",
25
+ "@push.rocks/smartserve": "^6.2.2",
26
26
  "@push.rocks/smarttime": "^4.2.3",
27
27
  "@push.rocks/smarturl": "^3.1.0",
28
28
  "@push.rocks/webjwt": "^1.0.10",
29
29
  "@push.rocks/webstore": "^2.1.0"
30
30
  },
31
31
  "devDependencies": {
32
+ "@api.global/typedserver": "^11.4.0",
32
33
  "@git.zone/tsbuild": "^4.4.2",
33
34
  "@git.zone/tsdoc": "^2.2.0",
34
35
  "@git.zone/tsrun": "^2.0.6",
35
- "@git.zone/tstest": "^5.0.0",
36
- "@push.rocks/smartdb": "^2.12.2",
37
- "@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"
38
39
  },
39
40
  "files": [
40
41
  "ts/**/*",
@@ -66,7 +67,7 @@
66
67
  ],
67
68
  "scripts": {
68
69
  "test": "pnpm run build && tstest test/ --verbose --logfile --timeout 60",
69
- "build": "tsbuild tsfolders --web --allowimplicitany",
70
+ "build": "tsbuild tsfolders --web",
70
71
  "buildDocs": "tsdoc"
71
72
  }
72
73
  }
package/readme.md CHANGED
@@ -23,6 +23,13 @@ pnpm add @idp.global/sdk
23
23
 
24
24
  This is an ESM TypeScript package. Published builds expose declaration files and runtime JavaScript through package exports.
25
25
 
26
+ Version 17 requires an IdP listener using TypedSocket 8 and TypedRequest 8
27
+ (TypedServer 11). Earlier listener and SDK transport majors cannot share the
28
+ WebSocket handshake. Upgrade the IdP app, its browser bundle, and services using
29
+ `IdpGlobalServerClient` together; this includes dcrouter's IdP password-login
30
+ flow. HTTP typed requests also use TypedRequest 8 response identities. The
31
+ confidential OIDC introspection client continues to use its separate HTTP API.
32
+
26
33
  ## Browser SDK
27
34
 
28
35
  Import the browser client from the browser subpath:
@@ -331,6 +338,41 @@ existing login and principal binding and make a fresh observation for each
331
338
  operation requiring live authority. The response contains no identity profile
332
339
  or email-based organization inference.
333
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
+
334
376
  ### Server API Surface
335
377
 
336
378
  | API | Description |
@@ -340,6 +382,8 @@ or email-based organization inference.
340
382
  | `IdpGlobalServerClient` | Typed-socket client for idp.global password login, JWT refresh, `whoIs` lookup, backend JWT validation fetches, and JWT key/blocklist pushes. |
341
383
  | `OidcIntrospectionClient` | Confidential server client for an exact live organization authority observation. |
342
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. |
343
387
  | `IdpMfaRequiredError` | Password-login error carrying the MFA challenge token and available factor methods. |
344
388
  | `defaultIdpGlobalBaseUrl` | Default hosted server origin: `https://app.idp.global`. |
345
389
  | `PasswordHasher` | Static `hashPassword()` and `verifyPassword()` helpers using `scrypt:v1`. |
@@ -354,6 +398,8 @@ The server export includes these TypeScript types:
354
398
  | --- | --- |
355
399
  | `IOidcIntrospectionClientOptions` | Exact HTTPS issuer, confidential client credentials, and optional deadline/response limits. |
356
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. |
357
403
  | `TIdpAccountAuthSource` | `'local' | 'idp.global'` |
358
404
  | `TIdpAccountRole` | `'admin' | 'user'` |
359
405
  | `TIdpAccountStatus` | `'active' | 'disabled'` |
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@idp.global/sdk',
6
- version: '16.1.1',
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';