@idp.global/sdk 15.0.1 → 16.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,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026-09-09 - 16.1.0
4
+
5
+ ### Features
6
+
7
+ - add confidential OIDC introspection client (server)
8
+ - Add a server-only OidcIntrospectionClient with Basic authentication, nonce binding, deadlines, response size limits, and sanitized errors.
9
+ - Validate active and inactive introspection responses against issuer, client, subject, organization, scopes, revisions, roles, and freshness.
10
+ - Export the client and error types from the server entrypoint and document live organization authority usage.
11
+ - Update @idp.global/interfaces to ^29.1.0 for introspection response types.
12
+
13
+ ### Fixes
14
+
15
+ - update development tooling dependencies (tooling)
16
+ - Bump @git.zone/tsdoc to ^2.2.0 and @git.zone/tstest to ^4.1.1
17
+ - Pin @types/node to 26.4.1
18
+ - Update packageManager to pnpm@12.2.1
19
+
20
+ ## 2026-09-05 - 16.0.0
21
+
22
+ ### Breaking Changes
23
+
24
+ - upgrade SmartData dependency to major 11 (smartdata)
25
+ - Updates @push.rocks/smartdata from ^7.2.1 to ^11.7.0.
26
+ - Requires applications that provide a SmartdataDb to use SmartData 11.7 or newer within major 11.
27
+ - Documents that existing SDK account documents and the id_1 identity index are retained without migration.
28
+ - Adds regression coverage to ensure login state updates preserve account identity, credentials, and optional IdP subject.
29
+
30
+ ### Fixes
31
+
32
+ - update development tooling dependencies (deps-dev)
33
+ - Bumps @git.zone/tsrun to ^2.0.6, @git.zone/tstest to ^4.0.0, and @types/node to ^26.4.1
34
+ - Updates pnpm package manager from 11.11.0 to 11.25.0
35
+
3
36
  ## 2026-08-02 - 15.0.1
4
37
 
5
38
  ### Fixes
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@idp.global/sdk',
6
- version: '15.0.1',
6
+ version: '16.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,24 @@
1
+ import * as plugins from './plugins.js';
2
+ export interface IOidcIntrospectionClientOptions {
3
+ issuer: string;
4
+ clientId: string;
5
+ clientSecret: string;
6
+ timeoutMs?: number;
7
+ maxResponseBytes?: number;
8
+ }
9
+ export interface IOidcIntrospectionClientRequest {
10
+ token: string;
11
+ subject: string;
12
+ organizationId: string;
13
+ }
14
+ export declare class OidcIntrospectionError extends Error {
15
+ readonly code: 'invalid_request' | 'invalid_response' | 'unavailable';
16
+ constructor(code: 'invalid_request' | 'invalid_response' | 'unavailable');
17
+ }
18
+ /** Server-only confidential client. Each result is one live observation, never a cached grant. */
19
+ export declare class OidcIntrospectionClient {
20
+ #private;
21
+ constructor(optionsArg: IOidcIntrospectionClientOptions);
22
+ introspect(requestArg: IOidcIntrospectionClientRequest): Promise<plugins.idpInterfaces.data.TOidcIntrospectionResponse>;
23
+ private validateResponse;
24
+ }
@@ -0,0 +1,144 @@
1
+ import * as plugins from './plugins.js';
2
+ export class OidcIntrospectionError extends Error {
3
+ code;
4
+ constructor(code) {
5
+ super(`OIDC introspection ${code.replace(/_/g, ' ')}.`);
6
+ this.code = code;
7
+ this.name = 'OidcIntrospectionError';
8
+ }
9
+ }
10
+ const exactString = (valueArg, maximumArg = 512) => typeof valueArg === 'string' && valueArg.length > 0 && valueArg.length <= maximumArg
11
+ && valueArg.trim() === valueArg;
12
+ const record = (valueArg) => valueArg !== null && typeof valueArg === 'object' && !Array.isArray(valueArg);
13
+ const exactKeys = (valueArg, keysArg) => Object.keys(valueArg).length === keysArg.length && keysArg.every((keyArg) => Object.hasOwn(valueArg, keyArg));
14
+ const revision = (valueArg) => Number.isSafeInteger(valueArg) && valueArg >= 0;
15
+ const sortedUnique = (valuesArg) => Array.isArray(valuesArg)
16
+ && valuesArg.length > 0 && valuesArg.every((valueArg, indexArg) => exactString(valueArg, 64)
17
+ && (indexArg === 0 || valuesArg[indexArg - 1] < valueArg));
18
+ /** Server-only confidential client. Each result is one live observation, never a cached grant. */
19
+ export class OidcIntrospectionClient {
20
+ #issuer;
21
+ #clientId;
22
+ #authorization;
23
+ #timeoutMs;
24
+ #maxResponseBytes;
25
+ constructor(optionsArg) {
26
+ if (!optionsArg || !exactString(optionsArg.issuer, 2048) || !exactString(optionsArg.clientId)
27
+ || !exactString(optionsArg.clientSecret, 4096))
28
+ throw new OidcIntrospectionError('invalid_request');
29
+ let issuer;
30
+ try {
31
+ issuer = new URL(optionsArg.issuer);
32
+ }
33
+ catch {
34
+ throw new OidcIntrospectionError('invalid_request');
35
+ }
36
+ const canonicalIssuer = issuer.origin + (issuer.pathname === '/' ? '' : issuer.pathname);
37
+ if (issuer.protocol !== 'https:' || issuer.username || issuer.password || issuer.search || issuer.hash
38
+ || issuer.pathname.includes('//') || issuer.pathname.endsWith('/') && issuer.pathname !== '/'
39
+ || canonicalIssuer !== optionsArg.issuer)
40
+ throw new OidcIntrospectionError('invalid_request');
41
+ this.#timeoutMs = optionsArg.timeoutMs ?? 5000;
42
+ this.#maxResponseBytes = optionsArg.maxResponseBytes ?? 16384;
43
+ if (!Number.isSafeInteger(this.#timeoutMs) || this.#timeoutMs < 1 || this.#timeoutMs > 30000
44
+ || !Number.isSafeInteger(this.#maxResponseBytes) || this.#maxResponseBytes < 1 || this.#maxResponseBytes > 65536) {
45
+ throw new OidcIntrospectionError('invalid_request');
46
+ }
47
+ this.#issuer = canonicalIssuer;
48
+ this.#clientId = optionsArg.clientId;
49
+ // OAuth Basic components are form-encoded separately before joining with ':'.
50
+ const encode = (valueArg) => new URLSearchParams({ value: valueArg }).toString().slice(6);
51
+ this.#authorization = `Basic ${Buffer.from(`${encode(optionsArg.clientId)}:${encode(optionsArg.clientSecret)}`, 'utf8').toString('base64')}`;
52
+ }
53
+ async introspect(requestArg) {
54
+ if (!requestArg || !exactString(requestArg.token, 16384) || !exactString(requestArg.subject)
55
+ || !exactString(requestArg.organizationId))
56
+ throw new OidcIntrospectionError('invalid_request');
57
+ // Capture bindings before crossing any asynchronous boundary.
58
+ const { token, subject, organizationId } = requestArg;
59
+ const nonce = plugins.crypto.randomBytes(32).toString('base64url');
60
+ const controller = new AbortController();
61
+ let timer;
62
+ const deadline = new Promise((_resolveArg, rejectArg) => {
63
+ timer = setTimeout(() => { controller.abort(); rejectArg(new OidcIntrospectionError('unavailable')); }, this.#timeoutMs);
64
+ });
65
+ let reader;
66
+ try {
67
+ const response = await Promise.race([fetch(`${this.#issuer}/oauth/introspect`, {
68
+ method: 'POST', redirect: 'error', credentials: 'omit', cache: 'no-store', signal: controller.signal,
69
+ headers: { authorization: this.#authorization, 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },
70
+ body: new URLSearchParams({ token, subject, organization_id: organizationId, nonce, token_type_hint: 'access_token' }),
71
+ }), deadline]);
72
+ if (response.status !== 200)
73
+ throw new OidcIntrospectionError('unavailable');
74
+ if (response.headers.get('content-type')?.split(';')[0].trim().toLowerCase() !== 'application/json'
75
+ || !response.headers.get('cache-control')?.toLowerCase().split(',').map((partArg) => partArg.trim()).includes('no-store')
76
+ || !response.body)
77
+ throw new OidcIntrospectionError('invalid_response');
78
+ const contentLength = response.headers.get('content-length');
79
+ if (contentLength !== null && (!/^\d+$/.test(contentLength) || Number(contentLength) > this.#maxResponseBytes)) {
80
+ throw new OidcIntrospectionError('invalid_response');
81
+ }
82
+ reader = response.body.getReader();
83
+ const chunks = [];
84
+ let size = 0;
85
+ while (true) {
86
+ const part = await Promise.race([reader.read(), deadline]);
87
+ if (part.done)
88
+ break;
89
+ size += part.value.byteLength;
90
+ if (size > this.#maxResponseBytes)
91
+ throw new OidcIntrospectionError('invalid_response');
92
+ chunks.push(part.value);
93
+ }
94
+ let value;
95
+ try {
96
+ value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)));
97
+ }
98
+ catch {
99
+ throw new OidcIntrospectionError('invalid_response');
100
+ }
101
+ return this.validateResponse(value, subject, organizationId, nonce);
102
+ }
103
+ catch (errorArg) {
104
+ if (errorArg instanceof OidcIntrospectionError)
105
+ throw errorArg;
106
+ // Never include request, credentials, token, transport details or body in an error.
107
+ throw new OidcIntrospectionError('unavailable');
108
+ }
109
+ finally {
110
+ clearTimeout(timer);
111
+ controller.abort();
112
+ if (reader)
113
+ void reader.cancel().catch(() => undefined);
114
+ }
115
+ }
116
+ validateResponse(valueArg, subjectArg, organizationIdArg, nonceArg) {
117
+ const invalid = () => new OidcIntrospectionError('invalid_response');
118
+ if (!record(valueArg))
119
+ throw invalid();
120
+ if (valueArg.active === false && exactKeys(valueArg, ['active']))
121
+ return { active: false };
122
+ if (valueArg.active !== true || !exactKeys(valueArg, ['active', 'token_type', 'client_id', 'scope', 'iss', 'aud', 'sub', 'exp', 'iat', 'idp_global_authority'])
123
+ || valueArg.token_type !== 'Bearer' || valueArg.client_id !== this.#clientId || valueArg.aud !== this.#clientId
124
+ || valueArg.iss !== this.#issuer || valueArg.sub !== subjectArg || !revision(valueArg.exp) || !revision(valueArg.iat)
125
+ || valueArg.exp <= Math.floor(Date.now() / 1000) || valueArg.iat > Math.floor(Date.now() / 1000)
126
+ || valueArg.exp <= valueArg.iat || typeof valueArg.scope !== 'string')
127
+ throw invalid();
128
+ const scopes = valueArg.scope.split(' ');
129
+ if (!sortedUnique(scopes) || !scopes.includes('openid') || !scopes.includes('organizations')
130
+ || !scopes.every((scopeArg) => ['openid', 'profile', 'email', 'organizations', 'roles'].includes(scopeArg)))
131
+ throw invalid();
132
+ const authority = valueArg.idp_global_authority;
133
+ if (!record(authority) || !exactKeys(authority, ['version', 'nonce', 'subject_revision', 'organization_id',
134
+ 'organization_revision', 'membership_id', 'membership_revision', 'app_connection_id', 'app_connection_revision', 'roles'])
135
+ || authority.version !== 1 || authority.nonce !== nonceArg || authority.organization_id !== organizationIdArg
136
+ || !revision(authority.subject_revision) || !revision(authority.organization_revision)
137
+ || !revision(authority.membership_revision) || !revision(authority.app_connection_revision)
138
+ || !exactString(authority.membership_id) || !exactString(authority.app_connection_id) || !sortedUnique(authority.roles)
139
+ || !authority.roles.every((roleArg) => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(roleArg)))
140
+ throw invalid();
141
+ return valueArg;
142
+ }
143
+ }
144
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5vaWRjLWludHJvc3BlY3Rpb24tY2xpZW50LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHNfc2VydmVyL2NsYXNzZXMub2lkYy1pbnRyb3NwZWN0aW9uLWNsaWVudC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQWdCeEMsTUFBTSxPQUFPLHNCQUF1QixTQUFRLEtBQUs7SUFDbkI7SUFBNUIsWUFBNEIsSUFBNEQ7UUFDdEYsS0FBSyxDQUFDLHNCQUFzQixJQUFJLENBQUMsT0FBTyxDQUFDLElBQUksRUFBRSxHQUFHLENBQUMsR0FBRyxDQUFDLENBQUM7UUFEOUIsU0FBSSxHQUFKLElBQUksQ0FBd0Q7UUFFdEYsSUFBSSxDQUFDLElBQUksR0FBRyx3QkFBd0IsQ0FBQztJQUN2QyxDQUFDO0NBQ0Y7QUFFRCxNQUFNLFdBQVcsR0FBRyxDQUFDLFFBQWlCLEVBQUUsVUFBVSxHQUFHLEdBQUcsRUFBc0IsRUFBRSxDQUM5RSxPQUFPLFFBQVEsS0FBSyxRQUFRLElBQUksUUFBUSxDQUFDLE1BQU0sR0FBRyxDQUFDLElBQUksUUFBUSxDQUFDLE1BQU0sSUFBSSxVQUFVO09BQ2pGLFFBQVEsQ0FBQyxJQUFJLEVBQUUsS0FBSyxRQUFRLENBQUM7QUFDbEMsTUFBTSxNQUFNLEdBQUcsQ0FBQyxRQUFpQixFQUF1QyxFQUFFLENBQ3hFLFFBQVEsS0FBSyxJQUFJLElBQUksT0FBTyxRQUFRLEtBQUssUUFBUSxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUMsQ0FBQztBQUNoRixNQUFNLFNBQVMsR0FBRyxDQUFDLFFBQWlDLEVBQUUsT0FBaUIsRUFBRSxFQUFFLENBQ3pFLE1BQU0sQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLENBQUMsTUFBTSxLQUFLLE9BQU8sQ0FBQyxNQUFNLElBQUksT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDLE1BQU0sRUFBRSxFQUFF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
@@ -3,4 +3,5 @@ export * from './classes.account-auth-service.js';
3
3
  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
+ export * from './classes.oidc-introspection-client.js';
6
7
  export * from './classes.password-hasher.js';
@@ -3,5 +3,6 @@ export * from './classes.account-auth-service.js';
3
3
  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
+ export * from './classes.oidc-introspection-client.js';
6
7
  export * from './classes.password-hasher.js';
7
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19zZXJ2ZXIvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLG1DQUFtQyxDQUFDO0FBQ2xELGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLHVDQUF1QyxDQUFDO0FBQ3RELGNBQWMsOEJBQThCLENBQUMifQ==
8
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19zZXJ2ZXIvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLG1DQUFtQyxDQUFDO0FBQ2xELGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLHVDQUF1QyxDQUFDO0FBQ3RELGNBQWMsd0NBQXdDLENBQUM7QUFDdkQsY0FBYyw4QkFBOEIsQ0FBQyJ9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idp.global/sdk",
3
- "version": "15.0.1",
3
+ "version": "16.1.0",
4
4
  "private": false,
5
5
  "description": "Reusable browser and server SDK for idp.global authentication.",
6
6
  "type": "module",
@@ -17,8 +17,8 @@
17
17
  "@api.global/typedrequest": "^5.2.1",
18
18
  "@api.global/typedrequest-interfaces": "^5.1.1",
19
19
  "@api.global/typedsocket": "^6.0.0",
20
- "@idp.global/interfaces": "^29.0.0",
21
- "@push.rocks/smartdata": "^7.2.1",
20
+ "@idp.global/interfaces": "^29.1.0",
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",
@@ -30,11 +30,11 @@
30
30
  },
31
31
  "devDependencies": {
32
32
  "@git.zone/tsbuild": "^4.4.2",
33
- "@git.zone/tsdoc": "^2.1.1",
34
- "@git.zone/tsrun": "^2.0.5",
35
- "@git.zone/tstest": "^3.6.6",
33
+ "@git.zone/tsdoc": "^2.2.0",
34
+ "@git.zone/tsrun": "^2.0.6",
35
+ "@git.zone/tstest": "^4.1.1",
36
36
  "@push.rocks/smartdb": "^2.12.2",
37
- "@types/node": "^26.1.1"
37
+ "@types/node": "26.4.1"
38
38
  },
39
39
  "files": [
40
40
  "ts/**/*",
@@ -44,7 +44,7 @@
44
44
  "dist_*/**/*",
45
45
  "readme.md",
46
46
  "changelog.md",
47
- "license"
47
+ "license.md"
48
48
  ],
49
49
  "repository": {
50
50
  "type": "git",
package/readme.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @idp.global/sdk
2
2
 
3
- Reusable TypeScript SDK for building against idp.global from both sides of the wire: browser apps that need SSO, JWT housekeeping, transfer-token handoff, and typed IdP requests; and server apps that need explicit account storage, local password auth, and optional idp.global password verification.
3
+ Reusable TypeScript SDK for building against idp.global from both sides of the wire: browser apps that need SSO, JWT housekeeping, transfer-token handoff, and typed IdP requests; and server apps that need explicit account storage, local password auth, optional idp.global password verification, and confidential OIDC membership introspection.
4
4
 
5
5
  The package is intentionally split by runtime. Use `@idp.global/sdk/browser` in browser bundles and `@idp.global/sdk/server` in Node.js services. The root `@idp.global/sdk` export does not expose application APIs.
6
6
 
@@ -13,7 +13,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
13
13
  | Runtime | Import | What it does |
14
14
  | --- | --- | --- |
15
15
  | Browser | `@idp.global/sdk/browser` | SSO client, JWT and refresh-token storage, login redirects, logout, transfer-token exchange, and typed request shortcuts for user, organization, billing, OIDC, and admin flows. |
16
- | Server | `@idp.global/sdk/server` | Smartdata-backed account store, scrypt password hashing, local/idp.global account authentication, and a server-side typed-socket client for idp.global password login. |
16
+ | Server | `@idp.global/sdk/server` | Smartdata-backed account store, scrypt password hashing, local/idp.global account authentication, a server-side typed-socket client for idp.global password login, and confidential OIDC introspection. |
17
17
 
18
18
  ## Install
19
19
 
@@ -183,6 +183,11 @@ The server SDK is deliberately explicit: it never auto-creates accounts during a
183
183
 
184
184
  `SmartdataAccountStore` persists accounts through `@push.rocks/smartdata` and the exported `IdpSdkAccountDoc` collection.
185
185
 
186
+ The server SDK requires SmartData 11.7 or newer within major 11. Applications
187
+ that pass a `SmartdataDb` must use the same SmartData major. Existing SDK account
188
+ documents and their `id_1` identity index retain their schema; this dependency
189
+ migration does not transform stored accounts.
190
+
186
191
  ```ts
187
192
  import * as smartdata from '@push.rocks/smartdata';
188
193
  import { SmartdataAccountStore } from '@idp.global/sdk/server';
@@ -286,6 +291,46 @@ themselves.
286
291
  Verification is keyset-only. Replace the cached snapshot on every push, select
287
292
  the verification key by the JWT header `kid`, and reject unknown key IDs.
288
293
 
294
+ ### Live Organization Authority
295
+
296
+ `OidcIntrospectionClient` calls the public issuer's `/oauth/introspect` endpoint
297
+ with confidential OAuth Basic authentication. Keep the client secret and access
298
+ token on the server. The token must belong to that same OAuth client and include
299
+ `openid organizations`; use the exact subject from the validated OIDC login and
300
+ the canonical organization ID.
301
+
302
+ ```ts
303
+ import { OidcIntrospectionClient } from '@idp.global/sdk/server';
304
+
305
+ const introspection = new OidcIntrospectionClient({
306
+ issuer: 'https://idp.global',
307
+ clientId: process.env.IDP_OIDC_CLIENT_ID!,
308
+ clientSecret: process.env.IDP_OIDC_CLIENT_SECRET!,
309
+ });
310
+ const authority = await introspection.introspect({
311
+ token: serverSession.accessToken,
312
+ subject: serverSession.idpSubject,
313
+ organizationId: requestedOrganizationId,
314
+ });
315
+ if (!authority.active) throw new Error('Organization access denied.');
316
+ const currentRoles = authority.idp_global_authority.roles;
317
+ ```
318
+
319
+ Every call generates a fresh nonce and validates the exact issuer, client,
320
+ subject, organization, response shape, revisions, and current roles. The default
321
+ request deadline is five seconds and the response limit is 16 KiB; redirects
322
+ are rejected. `OidcIntrospectionError` exposes only a sanitized `code`:
323
+ `invalid_request`, `invalid_response`, or `unavailable`. Deny the operation when
324
+ introspection fails.
325
+
326
+ An active result observes current authority while the provider holds its
327
+ mutation fences. It is not a cached membership grant: membership can change
328
+ after that observation. `exp` bounds token, session, and refresh-family expiry;
329
+ it does not promise membership remains valid until then. Keep the application's
330
+ existing login and principal binding and make a fresh observation for each
331
+ operation requiring live authority. The response contains no identity profile
332
+ or email-based organization inference.
333
+
289
334
  ### Server API Surface
290
335
 
291
336
  | API | Description |
@@ -293,6 +338,8 @@ the verification key by the JWT header `kid`, and reject unknown key IDs.
293
338
  | `SmartdataAccountStore` | Smartdata-backed account persistence and local password verification. |
294
339
  | `AccountAuthService` | Existing-account authentication orchestration for `local`, `idp.global`, and `auto`. |
295
340
  | `IdpGlobalServerClient` | Typed-socket client for idp.global password login, JWT refresh, `whoIs` lookup, backend JWT validation fetches, and JWT key/blocklist pushes. |
341
+ | `OidcIntrospectionClient` | Confidential server client for an exact live organization authority observation. |
342
+ | `OidcIntrospectionError` | Sanitized introspection failure with a stable error code. |
296
343
  | `IdpMfaRequiredError` | Password-login error carrying the MFA challenge token and available factor methods. |
297
344
  | `defaultIdpGlobalBaseUrl` | Default hosted server origin: `https://app.idp.global`. |
298
345
  | `PasswordHasher` | Static `hashPassword()` and `verifyPassword()` helpers using `scrypt:v1`. |
@@ -305,6 +352,8 @@ The server export includes these TypeScript types:
305
352
 
306
353
  | Type | Values or purpose |
307
354
  | --- | --- |
355
+ | `IOidcIntrospectionClientOptions` | Exact HTTPS issuer, confidential client credentials, and optional deadline/response limits. |
356
+ | `IOidcIntrospectionClientRequest` | Server-held access token, exact subject, and canonical organization ID. |
308
357
  | `TIdpAccountAuthSource` | `'local' | 'idp.global'` |
309
358
  | `TIdpAccountRole` | `'admin' | 'user'` |
310
359
  | `TIdpAccountStatus` | `'active' | 'disabled'` |
@@ -336,7 +385,7 @@ The test suite covers the server account store and auth service, browser transfe
336
385
 
337
386
  ## License and Legal Information
338
387
 
339
- This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
388
+ This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license.md](./license.md) file.
340
389
 
341
390
  **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
342
391
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@idp.global/sdk',
6
- version: '15.0.1',
6
+ version: '16.1.0',
7
7
  description: 'Reusable browser and server SDK for idp.global authentication.'
8
8
  }
@@ -0,0 +1,140 @@
1
+ import * as plugins from './plugins.js';
2
+
3
+ export interface IOidcIntrospectionClientOptions {
4
+ issuer: string;
5
+ clientId: string;
6
+ clientSecret: string;
7
+ timeoutMs?: number;
8
+ maxResponseBytes?: number;
9
+ }
10
+
11
+ export interface IOidcIntrospectionClientRequest {
12
+ token: string;
13
+ subject: string;
14
+ organizationId: string;
15
+ }
16
+
17
+ export class OidcIntrospectionError extends Error {
18
+ constructor(public readonly code: 'invalid_request' | 'invalid_response' | 'unavailable') {
19
+ super(`OIDC introspection ${code.replace(/_/g, ' ')}.`);
20
+ this.name = 'OidcIntrospectionError';
21
+ }
22
+ }
23
+
24
+ const exactString = (valueArg: unknown, maximumArg = 512): valueArg is string =>
25
+ typeof valueArg === 'string' && valueArg.length > 0 && valueArg.length <= maximumArg
26
+ && valueArg.trim() === valueArg;
27
+ const record = (valueArg: unknown): valueArg is Record<string, unknown> =>
28
+ valueArg !== null && typeof valueArg === 'object' && !Array.isArray(valueArg);
29
+ const exactKeys = (valueArg: Record<string, unknown>, keysArg: string[]) =>
30
+ Object.keys(valueArg).length === keysArg.length && keysArg.every((keyArg) => Object.hasOwn(valueArg, keyArg));
31
+ const revision = (valueArg: unknown): valueArg is number => Number.isSafeInteger(valueArg) && (valueArg as number) >= 0;
32
+ const sortedUnique = (valuesArg: unknown): valuesArg is string[] => Array.isArray(valuesArg)
33
+ && valuesArg.length > 0 && valuesArg.every((valueArg, indexArg) => exactString(valueArg, 64)
34
+ && (indexArg === 0 || valuesArg[indexArg - 1] < valueArg));
35
+
36
+ /** Server-only confidential client. Each result is one live observation, never a cached grant. */
37
+ export class OidcIntrospectionClient {
38
+ readonly #issuer: string;
39
+ readonly #clientId: string;
40
+ readonly #authorization: string;
41
+ readonly #timeoutMs: number;
42
+ readonly #maxResponseBytes: number;
43
+
44
+ constructor(optionsArg: IOidcIntrospectionClientOptions) {
45
+ if (!optionsArg || !exactString(optionsArg.issuer, 2048) || !exactString(optionsArg.clientId)
46
+ || !exactString(optionsArg.clientSecret, 4096)) throw new OidcIntrospectionError('invalid_request');
47
+ let issuer: URL;
48
+ try { issuer = new URL(optionsArg.issuer); } catch { throw new OidcIntrospectionError('invalid_request'); }
49
+ const canonicalIssuer = issuer.origin + (issuer.pathname === '/' ? '' : issuer.pathname);
50
+ if (issuer.protocol !== 'https:' || issuer.username || issuer.password || issuer.search || issuer.hash
51
+ || issuer.pathname.includes('//') || issuer.pathname.endsWith('/') && issuer.pathname !== '/'
52
+ || canonicalIssuer !== optionsArg.issuer) throw new OidcIntrospectionError('invalid_request');
53
+ this.#timeoutMs = optionsArg.timeoutMs ?? 5000;
54
+ this.#maxResponseBytes = optionsArg.maxResponseBytes ?? 16384;
55
+ if (!Number.isSafeInteger(this.#timeoutMs) || this.#timeoutMs < 1 || this.#timeoutMs > 30000
56
+ || !Number.isSafeInteger(this.#maxResponseBytes) || this.#maxResponseBytes < 1 || this.#maxResponseBytes > 65536) {
57
+ throw new OidcIntrospectionError('invalid_request');
58
+ }
59
+ this.#issuer = canonicalIssuer;
60
+ this.#clientId = optionsArg.clientId;
61
+ // OAuth Basic components are form-encoded separately before joining with ':'.
62
+ const encode = (valueArg: string) => new URLSearchParams({ value: valueArg }).toString().slice(6);
63
+ this.#authorization = `Basic ${Buffer.from(`${encode(optionsArg.clientId)}:${encode(optionsArg.clientSecret)}`, 'utf8').toString('base64')}`;
64
+ }
65
+
66
+ public async introspect(requestArg: IOidcIntrospectionClientRequest): Promise<plugins.idpInterfaces.data.TOidcIntrospectionResponse> {
67
+ if (!requestArg || !exactString(requestArg.token, 16384) || !exactString(requestArg.subject)
68
+ || !exactString(requestArg.organizationId)) throw new OidcIntrospectionError('invalid_request');
69
+ // Capture bindings before crossing any asynchronous boundary.
70
+ const { token, subject, organizationId } = requestArg;
71
+ const nonce = plugins.crypto.randomBytes(32).toString('base64url');
72
+ const controller = new AbortController();
73
+ let timer: ReturnType<typeof setTimeout>;
74
+ const deadline = new Promise<never>((_resolveArg, rejectArg) => {
75
+ timer = setTimeout(() => { controller.abort(); rejectArg(new OidcIntrospectionError('unavailable')); }, this.#timeoutMs);
76
+ });
77
+ let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
78
+ try {
79
+ const response = await Promise.race([fetch(`${this.#issuer}/oauth/introspect`, {
80
+ method: 'POST', redirect: 'error', credentials: 'omit', cache: 'no-store', signal: controller.signal,
81
+ headers: { authorization: this.#authorization, 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },
82
+ body: new URLSearchParams({ token, subject, organization_id: organizationId, nonce, token_type_hint: 'access_token' }),
83
+ }), deadline]);
84
+ if (response.status !== 200) throw new OidcIntrospectionError('unavailable');
85
+ if (response.headers.get('content-type')?.split(';')[0].trim().toLowerCase() !== 'application/json'
86
+ || !response.headers.get('cache-control')?.toLowerCase().split(',').map((partArg) => partArg.trim()).includes('no-store')
87
+ || !response.body) throw new OidcIntrospectionError('invalid_response');
88
+ const contentLength = response.headers.get('content-length');
89
+ if (contentLength !== null && (!/^\d+$/.test(contentLength) || Number(contentLength) > this.#maxResponseBytes)) {
90
+ throw new OidcIntrospectionError('invalid_response');
91
+ }
92
+ reader = response.body.getReader();
93
+ const chunks: Uint8Array[] = [];
94
+ let size = 0;
95
+ while (true) {
96
+ const part = await Promise.race([reader.read(), deadline]);
97
+ if (part.done) break;
98
+ size += part.value.byteLength;
99
+ if (size > this.#maxResponseBytes) throw new OidcIntrospectionError('invalid_response');
100
+ chunks.push(part.value);
101
+ }
102
+ let value: unknown;
103
+ try { value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks))); }
104
+ catch { throw new OidcIntrospectionError('invalid_response'); }
105
+ return this.validateResponse(value, subject, organizationId, nonce);
106
+ } catch (errorArg) {
107
+ if (errorArg instanceof OidcIntrospectionError) throw errorArg;
108
+ // Never include request, credentials, token, transport details or body in an error.
109
+ throw new OidcIntrospectionError('unavailable');
110
+ } finally {
111
+ clearTimeout(timer);
112
+ controller.abort();
113
+ if (reader) void reader.cancel().catch(() => undefined);
114
+ }
115
+ }
116
+
117
+ private validateResponse(valueArg: unknown, subjectArg: string, organizationIdArg: string, nonceArg: string): plugins.idpInterfaces.data.TOidcIntrospectionResponse {
118
+ const invalid = () => new OidcIntrospectionError('invalid_response');
119
+ if (!record(valueArg)) throw invalid();
120
+ if (valueArg.active === false && exactKeys(valueArg, ['active'])) return { active: false };
121
+ if (valueArg.active !== true || !exactKeys(valueArg,
122
+ ['active', 'token_type', 'client_id', 'scope', 'iss', 'aud', 'sub', 'exp', 'iat', 'idp_global_authority'])
123
+ || valueArg.token_type !== 'Bearer' || valueArg.client_id !== this.#clientId || valueArg.aud !== this.#clientId
124
+ || valueArg.iss !== this.#issuer || valueArg.sub !== subjectArg || !revision(valueArg.exp) || !revision(valueArg.iat)
125
+ || valueArg.exp <= Math.floor(Date.now() / 1000) || valueArg.iat > Math.floor(Date.now() / 1000)
126
+ || valueArg.exp <= valueArg.iat || typeof valueArg.scope !== 'string') throw invalid();
127
+ const scopes = valueArg.scope.split(' ');
128
+ if (!sortedUnique(scopes) || !scopes.includes('openid') || !scopes.includes('organizations')
129
+ || !scopes.every((scopeArg) => ['openid', 'profile', 'email', 'organizations', 'roles'].includes(scopeArg))) throw invalid();
130
+ const authority = valueArg.idp_global_authority;
131
+ if (!record(authority) || !exactKeys(authority, ['version', 'nonce', 'subject_revision', 'organization_id',
132
+ 'organization_revision', 'membership_id', 'membership_revision', 'app_connection_id', 'app_connection_revision', 'roles'])
133
+ || authority.version !== 1 || authority.nonce !== nonceArg || authority.organization_id !== organizationIdArg
134
+ || !revision(authority.subject_revision) || !revision(authority.organization_revision)
135
+ || !revision(authority.membership_revision) || !revision(authority.app_connection_revision)
136
+ || !exactString(authority.membership_id) || !exactString(authority.app_connection_id) || !sortedUnique(authority.roles)
137
+ || !authority.roles.every((roleArg) => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(roleArg))) throw invalid();
138
+ return valueArg as unknown as plugins.idpInterfaces.data.IOidcActiveIntrospectionResponse;
139
+ }
140
+ }
@@ -3,4 +3,5 @@ export * from './classes.account-auth-service.js';
3
3
  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
+ export * from './classes.oidc-introspection-client.js';
6
7
  export * from './classes.password-hasher.js';
File without changes