@idp.global/sdk 18.0.1 → 18.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 +17 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts_relyingparty/classes.actorscope.d.ts +23 -0
- package/dist_ts_relyingparty/classes.actorscope.js +22 -0
- package/dist_ts_relyingparty/classes.authorityobserver.d.ts +81 -0
- package/dist_ts_relyingparty/classes.authorityobserver.js +185 -0
- package/dist_ts_relyingparty/classes.errors.d.ts +33 -0
- package/dist_ts_relyingparty/classes.errors.js +53 -0
- package/dist_ts_relyingparty/classes.idpoidcprovider.d.ts +142 -0
- package/dist_ts_relyingparty/classes.idpoidcprovider.js +255 -0
- package/dist_ts_relyingparty/classes.relyingpartyroutes.d.ts +84 -0
- package/dist_ts_relyingparty/classes.relyingpartyroutes.js +232 -0
- package/dist_ts_relyingparty/classes.relyingpartysessions.d.ts +210 -0
- package/dist_ts_relyingparty/classes.relyingpartysessions.js +786 -0
- package/dist_ts_relyingparty/classes.rpmodels.d.ts +79 -0
- package/dist_ts_relyingparty/classes.rpmodels.js +283 -0
- package/dist_ts_relyingparty/classes.secretbox.d.ts +14 -0
- package/dist_ts_relyingparty/classes.secretbox.js +45 -0
- package/dist_ts_relyingparty/classes.socketbinding.d.ts +75 -0
- package/dist_ts_relyingparty/classes.socketbinding.js +94 -0
- package/dist_ts_relyingparty/helpers.cookies.d.ts +11 -0
- package/dist_ts_relyingparty/helpers.cookies.js +62 -0
- package/dist_ts_relyingparty/helpers.crypto.d.ts +12 -0
- package/dist_ts_relyingparty/helpers.crypto.js +40 -0
- package/dist_ts_relyingparty/helpers.returnpath.d.ts +9 -0
- package/dist_ts_relyingparty/helpers.returnpath.js +39 -0
- package/dist_ts_relyingparty/index.d.ts +13 -0
- package/dist_ts_relyingparty/index.js +14 -0
- package/dist_ts_relyingparty/interfaces.d.ts +113 -0
- package/dist_ts_relyingparty/interfaces.js +2 -0
- package/dist_ts_relyingparty/plugins.d.ts +10 -0
- package/dist_ts_relyingparty/plugins.js +11 -0
- package/package.json +13 -10
- package/readme.md +158 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts_relyingparty/classes.actorscope.ts +39 -0
- package/ts_relyingparty/classes.authorityobserver.ts +253 -0
- package/ts_relyingparty/classes.errors.ts +69 -0
- package/ts_relyingparty/classes.idpoidcprovider.ts +386 -0
- package/ts_relyingparty/classes.relyingpartyroutes.ts +286 -0
- package/ts_relyingparty/classes.relyingpartysessions.ts +970 -0
- package/ts_relyingparty/classes.rpmodels.ts +146 -0
- package/ts_relyingparty/classes.secretbox.ts +49 -0
- package/ts_relyingparty/classes.socketbinding.ts +142 -0
- package/ts_relyingparty/helpers.cookies.ts +79 -0
- package/ts_relyingparty/helpers.crypto.ts +49 -0
- package/ts_relyingparty/helpers.returnpath.ts +44 -0
- package/ts_relyingparty/index.ts +13 -0
- package/ts_relyingparty/interfaces.ts +133 -0
- package/ts_relyingparty/plugins.ts +17 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Control characters, space, DEL and the cookie delimiters are never valid.
|
|
2
|
+
const isInvalidCookieCharacter = (characterArg) => {
|
|
3
|
+
const code = characterArg.charCodeAt(0);
|
|
4
|
+
return code <= 0x20 || code === 0x7f || characterArg === ';' || characterArg === ',';
|
|
5
|
+
};
|
|
6
|
+
const assertCookiePart = (valueArg, labelArg) => {
|
|
7
|
+
if (!valueArg || Array.from(valueArg).some(isInvalidCookieCharacter)) {
|
|
8
|
+
throw new Error('Invalid cookie ' + labelArg);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
/** The cookies of a `Cookie` header; the first value of a repeated name wins, an undecodable value is skipped. */
|
|
12
|
+
export const parseCookies = (cookieHeaderArg) => {
|
|
13
|
+
const result = new Map();
|
|
14
|
+
if (!cookieHeaderArg) {
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
for (const part of cookieHeaderArg.split(';')) {
|
|
18
|
+
const separatorIndex = part.indexOf('=');
|
|
19
|
+
if (separatorIndex <= 0) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const name = part.slice(0, separatorIndex).trim();
|
|
23
|
+
const encodedValue = part.slice(separatorIndex + 1).trim();
|
|
24
|
+
if (!name || result.has(name)) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
result.set(name, decodeURIComponent(encodedValue));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
/** One `Set-Cookie` value, always `Path=/` and never with a `Domain`, so a `__Host-` name stays valid. */
|
|
37
|
+
export const serializeCookie = (nameArg, valueArg, optionsArg = {}) => {
|
|
38
|
+
assertCookiePart(nameArg, 'name');
|
|
39
|
+
assertCookiePart(valueArg, 'value');
|
|
40
|
+
const parts = [nameArg + '=' + encodeURIComponent(valueArg), 'Path=/'];
|
|
41
|
+
if (optionsArg.httpOnly) {
|
|
42
|
+
parts.push('HttpOnly');
|
|
43
|
+
}
|
|
44
|
+
if (optionsArg.secure) {
|
|
45
|
+
parts.push('Secure');
|
|
46
|
+
}
|
|
47
|
+
if (optionsArg.sameSite) {
|
|
48
|
+
parts.push('SameSite=' + optionsArg.sameSite);
|
|
49
|
+
}
|
|
50
|
+
if (optionsArg.maxAgeSeconds !== undefined) {
|
|
51
|
+
if (!Number.isSafeInteger(optionsArg.maxAgeSeconds) || optionsArg.maxAgeSeconds < 0) {
|
|
52
|
+
throw new Error('Invalid cookie max age');
|
|
53
|
+
}
|
|
54
|
+
parts.push('Max-Age=' + optionsArg.maxAgeSeconds);
|
|
55
|
+
}
|
|
56
|
+
return parts.join('; ');
|
|
57
|
+
};
|
|
58
|
+
export const serializeClearedCookie = (nameArg, optionsArg = {}) => serializeCookie(nameArg, 'deleted', {
|
|
59
|
+
...optionsArg,
|
|
60
|
+
maxAgeSeconds: 0,
|
|
61
|
+
}) + '; Expires=Thu, 01 Jan 1970 00:00:00 GMT';
|
|
62
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaGVscGVycy5jb29raWVzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHNfcmVseWluZ3BhcnR5L2hlbHBlcnMuY29va2llcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFPQSw0RUFBNEU7QUFDNUUsTUFBTSx3QkFBd0IsR0FBRyxDQUFDLFlBQW9CLEVBQVcsRUFBRTtJQUNqRSxNQUFNLElBQUksR0FBRyxZQUFZLENBQUMsVUFBVSxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBQ3hDLE9BQU8sSUFBSSxJQUFJLElBQUksSUFBSSxJQUFJLEtBQUssSUFBSSxJQUFJLFlBQVksS0FBSyxHQUFHLElBQUksWUFBWSxLQUFLLEdBQUcsQ0FBQztBQUN2RixDQUFDLENBQUM7QUFFRixNQUFNLGdCQUFnQixHQUFHLENBQUMsUUFBZ0IsRUFBRSxRQUFnQixFQUFRLEVBQUU7SUFDcEUsSUFBSSxDQUFDLFFBQVEsSUFBSSxLQUFLLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxDQUFDLElBQUksQ0FBQyx3QkFBd0IsQ0FBQyxFQUFFLENBQUM7UUFDckUsTUFBTSxJQUFJLEtBQUssQ0FBQyxpQkFBaUIsR0FBRyxRQUFRLENBQUMsQ0FBQztJQUNoRCxDQUFDO0FBQ0gsQ0FBQyxDQUFDO0FBRUYsa0hBQWtIO0FBQ2xILE1BQU0sQ0FBQyxNQUFNLFlBQVksR0FBRyxDQUFDLGVBQTBDLEVBQXVCLEVBQUU7SUFDOUYsTUFBTSxNQUFNLEdBQUcsSUFBSSxHQUFHLEVBQWtCLENBQUM7SUFDekMsSUFBSSxDQUFDLGVBQWUsRUFBRSxDQUFDO1FBQ3JCLE9BQU8sTUFBTSxDQUFDO0lBQ2hCLENBQUM7SUFDRCxLQUFLLE1BQU0sSUFBSSxJQUFJLGVBQWUsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQztRQUM5QyxNQUFNLGNBQWMsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBQ3pDLElBQUksY0FBYyxJQUFJLENBQUMsRUFBRSxDQUFDO1lBQ3hCLFNBQVM7UUFDWCxDQUFDO1FBQ0QsTUFBTSxJQUFJLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsY0FBYyxDQUFDLENBQUMsSUFBSSxFQUFFLENBQUM7UUFDbEQsTUFBTSxZQUFZLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxjQUFjLEdBQUcsQ0FBQyxDQUFDLENBQUMsSUFBSSxFQUFFLENBQUM7UUFDM0QsSUFBSSxDQUFDLElBQUksSUFBSSxNQUFNLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUM7WUFDOUIsU0FBUztRQUNYLENBQUM7UUFDRCxJQUFJLENBQUM7WUFDSCxNQUFNLENBQUMsR0FBRyxDQUFDLElBQUksRUFBRSxrQkFBa0IsQ0FBQyxZQUFZLENBQUMsQ0FBQyxDQUFDO1FBQ3JELENBQUM7UUFBQyxNQUFNLENBQUM7WUFDUCxTQUFTO1FBQ1gsQ0FBQztJQUNILENBQUM7SUFDRCxPQUFPLE1BQU0sQ0FBQztBQUNoQixDQUFDLENBQUM7QUFFRiwwR0FBMEc7QUFDMUcsTUFBTSxDQUFDLE1BQU0sZUFBZSxHQUFHLENBQzdCLE9BQWUsRUFDZixRQUFnQixFQUNoQixhQUE2QixFQUFFLEVBQ3ZCLEVBQUU7SUFDVixnQkFBZ0IsQ0FBQyxPQUFPLEVBQUUsTUFBTSxDQUFDLENBQUM7SUFDbEMsZ0JBQWdCLENBQUMsUUFBUSxFQUFFLE9BQU8sQ0FBQyxDQUFDO0lBQ3BDLE1BQU0sS0FBSyxHQUFHLENBQUMsT0FBTyxHQUFHLEdBQUcsR0FBRyxrQkFBa0IsQ0FBQyxRQUFRLENBQUMsRUFBRSxRQUFRLENBQUMsQ0FBQztJQUN2RSxJQUFJLFVBQVUsQ0FBQyxRQUFRLEVBQUUsQ0FBQztRQUN4QixLQUFLLENBQUMsSUFBSSxDQUFDLFVBQVUsQ0FBQyxDQUFDO0lBQ3pCLENBQUM7SUFDRCxJQUFJLFVBQVUsQ0FBQyxNQUFNLEVBQUUsQ0FBQztRQUN0QixLQUFLLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxDQUFDO0lBQ3ZCLENBQUM7SUFDRCxJQUFJLFVBQVUsQ0FBQyxRQUFRLEVBQUUsQ0FBQztRQUN4QixLQUFLLENBQUMsSUFBSSxDQUFDLFdBQVcsR0FBRyxVQUFVLENBQUMsUUFBUSxDQUFDLENBQUM7SUFDaEQsQ0FBQztJQUNELElBQUksVUFBVSxDQUFDLGFBQWEsS0FBSyxTQUFTLEVBQUUsQ0FBQztRQUMzQyxJQUFJLENBQUMsTUFBTSxDQUFDLGFBQWEsQ0FBQyxVQUFVLENBQUMsYUFBYSxDQUFDLElBQUksVUFBVSxDQUFDLGFBQWEsR0FBRyxDQUFDLEVBQUUsQ0FBQztZQUNwRixNQUFNLElBQUksS0FBSyxDQUFDLHdCQUF3QixDQUFDLENBQUM7UUFDNUMsQ0FBQztRQUNELEtBQUssQ0FBQyxJQUFJLENBQUMsVUFBVSxHQUFHLFVBQVUsQ0FBQyxhQUFhLENBQUMsQ0FBQztJQUNwRCxDQUFDO0lBQ0QsT0FBTyxLQUFLLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO0FBQzFCLENBQUMsQ0FBQztBQUVGLE1BQU0sQ0FBQyxNQUFNLHNCQUFzQixHQUFHLENBQ3BDLE9BQWUsRUFDZixhQUFvRCxFQUFFLEVBQzlDLEVBQUUsQ0FDVixlQUFlLENBQUMsT0FBTyxFQUFFLFNBQVMsRUFBRTtJQUNsQyxHQUFHLFVBQVU7SUFDYixhQUFhLEVBQUUsQ0FBQztDQUNqQixDQUFDLEdBQUcseUNBQXlDLENBQUMifQ==
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** A 32-byte key given as base64 or base64url; anything else is refused by `labelArg`, never echoed. */
|
|
2
|
+
export declare const decodeSecretKey: (valueArg: string, labelArg: string) => Buffer;
|
|
3
|
+
export declare const randomToken: (byteLengthArg?: number) => string;
|
|
4
|
+
/** base64url SHA-256 of a UTF-8 string; how tokens, states and bindings are stored. */
|
|
5
|
+
export declare const sha256: (valueArg: string) => string;
|
|
6
|
+
export declare const timingSafeEqualString: (leftArg: string, rightArg: string) => boolean;
|
|
7
|
+
/**
|
|
8
|
+
* The CSRF token of a session: an HMAC of the session cookie token under the
|
|
9
|
+
* deployment's CSRF key, after a label that separates this use of the key from
|
|
10
|
+
* any other. It is derived again on every request and never stored.
|
|
11
|
+
*/
|
|
12
|
+
export declare const deriveCsrfToken: (sessionTokenArg: string, csrfKeyArg: Buffer, labelArg: string) => string;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
/** A 32-byte key given as base64 or base64url; anything else is refused by `labelArg`, never echoed. */
|
|
3
|
+
export const decodeSecretKey = (valueArg, labelArg) => {
|
|
4
|
+
const normalized = typeof valueArg === 'string' ? valueArg.trim() : '';
|
|
5
|
+
if (!normalized) {
|
|
6
|
+
throw new Error(labelArg + ' must not be empty');
|
|
7
|
+
}
|
|
8
|
+
const base64Normalized = normalized
|
|
9
|
+
.replace(/-/gu, '+')
|
|
10
|
+
.replace(/_/gu, '/')
|
|
11
|
+
.replace(/=+$/u, '');
|
|
12
|
+
const decoded = Buffer.from(normalized, 'base64');
|
|
13
|
+
if (decoded.length !== 32
|
|
14
|
+
|| decoded.toString('base64').replace(/=+$/u, '') !== base64Normalized) {
|
|
15
|
+
throw new Error(labelArg + ' must decode to exactly 32 bytes');
|
|
16
|
+
}
|
|
17
|
+
return decoded;
|
|
18
|
+
};
|
|
19
|
+
export const randomToken = (byteLengthArg = 32) => plugins.crypto.randomBytes(byteLengthArg).toString('base64url');
|
|
20
|
+
/** base64url SHA-256 of a UTF-8 string; how tokens, states and bindings are stored. */
|
|
21
|
+
export const sha256 = (valueArg) => plugins.crypto.createHash('sha256').update(valueArg, 'utf8').digest('base64url');
|
|
22
|
+
export const timingSafeEqualString = (leftArg, rightArg) => {
|
|
23
|
+
const left = Buffer.from(leftArg, 'utf8');
|
|
24
|
+
const right = Buffer.from(rightArg, 'utf8');
|
|
25
|
+
if (left.length !== right.length) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return plugins.crypto.timingSafeEqual(left, right);
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* The CSRF token of a session: an HMAC of the session cookie token under the
|
|
32
|
+
* deployment's CSRF key, after a label that separates this use of the key from
|
|
33
|
+
* any other. It is derived again on every request and never stored.
|
|
34
|
+
*/
|
|
35
|
+
export const deriveCsrfToken = (sessionTokenArg, csrfKeyArg, labelArg) => plugins.crypto
|
|
36
|
+
.createHmac('sha256', csrfKeyArg)
|
|
37
|
+
.update(labelArg, 'utf8')
|
|
38
|
+
.update(sessionTokenArg, 'utf8')
|
|
39
|
+
.digest('base64url');
|
|
40
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaGVscGVycy5jcnlwdG8uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90c19yZWx5aW5ncGFydHkvaGVscGVycy5jcnlwdG8udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSxjQUFjLENBQUM7QUFFeEMsd0dBQXdHO0FBQ3hHLE1BQU0sQ0FBQyxNQUFNLGVBQWUsR0FBRyxDQUFDLFFBQWdCLEVBQUUsUUFBZ0IsRUFBVSxFQUFFO0lBQzVFLE1BQU0sVUFBVSxHQUFHLE9BQU8sUUFBUSxLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLElBQUksRUFBRSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7SUFDdkUsSUFBSSxDQUFDLFVBQVUsRUFBRSxDQUFDO1FBQ2hCLE1BQU0sSUFBSSxLQUFLLENBQUMsUUFBUSxHQUFHLG9CQUFvQixDQUFDLENBQUM7SUFDbkQsQ0FBQztJQUNELE1BQU0sZ0JBQWdCLEdBQUcsVUFBVTtTQUNoQyxPQUFPLENBQUMsS0FBSyxFQUFFLEdBQUcsQ0FBQztTQUNuQixPQUFPLENBQUMsS0FBSyxFQUFFLEdBQUcsQ0FBQztTQUNuQixPQUFPLENBQUMsTUFBTSxFQUFFLEVBQUUsQ0FBQyxDQUFDO0lBQ3ZCLE1BQU0sT0FBTyxHQUFHLE1BQU0sQ0FBQyxJQUFJLENBQUMsVUFBVSxFQUFFLFFBQVEsQ0FBQyxDQUFDO0lBQ2xELElBQ0UsT0FBTyxDQUFDLE1BQU0sS0FBSyxFQUFFO1dBQ2xCLE9BQU8sQ0FBQyxRQUFRLENBQUMsUUFBUSxDQUFDLENBQUMsT0FBTyxDQUFDLE1BQU0sRUFBRSxFQUFFLENBQUMsS0FBSyxnQkFBZ0IsRUFDdEUsQ0FBQztRQUNELE1BQU0sSUFBSSxLQUFLLENBQUMsUUFBUSxHQUFHLGtDQUFrQyxDQUFDLENBQUM7SUFDakUsQ0FBQztJQUNELE9BQU8sT0FBTyxDQUFDO0FBQ2pCLENBQUMsQ0FBQztBQUVGLE1BQU0sQ0FBQyxNQUFNLFdBQVcsR0FBRyxDQUFDLGdCQUF3QixFQUFFLEVBQVUsRUFBRSxDQUNoRSxPQUFPLENBQUMsTUFBTSxDQUFDLFdBQVcsQ0FBQyxhQUFhLENBQUMsQ0FBQyxRQUFRLENBQUMsV0FBVyxDQUFDLENBQUM7QUFFbEUsdUZBQXVGO0FBQ3ZGLE1BQU0sQ0FBQyxNQUFNLE1BQU0sR0FBRyxDQUFDLFFBQWdCLEVBQVUsRUFBRSxDQUNqRCxPQUFPLENBQUMsTUFBTSxDQUFDLFVBQVUsQ0FBQyxRQUFRLENBQUMsQ0FBQyxNQUFNLENBQUMsUUFBUSxFQUFFLE1BQU0sQ0FBQyxDQUFDLE1BQU0sQ0FBQyxXQUFXLENBQUMsQ0FBQztBQUVuRixNQUFNLENBQUMsTUFBTSxxQkFBcUIsR0FBRyxDQUFDLE9BQWUsRUFBRSxRQUFnQixFQUFXLEVBQUU7SUFDbEYsTUFBTSxJQUFJLEdBQUcsTUFBTSxDQUFDLElBQUksQ0FBQyxPQUFPLEVBQUUsTUFBTSxDQUFDLENBQUM7SUFDMUMsTUFBTSxLQUFLLEdBQUcsTUFBTSxDQUFDLElBQUksQ0FBQyxRQUFRLEVBQUUsTUFBTSxDQUFDLENBQUM7SUFDNUMsSUFBSSxJQUFJLENBQUMsTUFBTSxLQUFLLEtBQUssQ0FBQyxNQUFNLEVBQUUsQ0FBQztRQUNqQyxPQUFPLEtBQUssQ0FBQztJQUNmLENBQUM7SUFDRCxPQUFPLE9BQU8sQ0FBQyxNQUFNLENBQUMsZUFBZSxDQUFDLElBQUksRUFBRSxLQUFLLENBQUMsQ0FBQztBQUNyRCxDQUFDLENBQUM7QUFFRjs7OztHQUlHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sZUFBZSxHQUFHLENBQUMsZUFBdUIsRUFBRSxVQUFrQixFQUFFLFFBQWdCLEVBQVUsRUFBRSxDQUN2RyxPQUFPLENBQUMsTUFBTTtLQUNYLFVBQVUsQ0FBQyxRQUFRLEVBQUUsVUFBVSxDQUFDO0tBQ2hDLE1BQU0sQ0FBQyxRQUFRLEVBQUUsTUFBTSxDQUFDO0tBQ3hCLE1BQU0sQ0FBQyxlQUFlLEVBQUUsTUFBTSxDQUFDO0tBQy9CLE1BQU0sQ0FBQyxXQUFXLENBQUMsQ0FBQyJ9
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one path a login may return to, judged on the normalized result and
|
|
3
|
+
* never on the shape of the input: `/.//evil.com` parses on this origin but
|
|
4
|
+
* normalizes to `//evil.com`, which a browser reads as another host. Everything
|
|
5
|
+
* that is not a plain same-origin path outside the login routes — a scheme, a
|
|
6
|
+
* host, a backslash, a control character, an overlong value, a path under
|
|
7
|
+
* `loginPathPrefixArg` — becomes `/`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const sanitizeReturnPath: (returnPathArg: unknown, publicOriginArg: string, loginPathPrefixArg: string) => string;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** A return path is a location the application produced, not a place to park data. */
|
|
2
|
+
const MAX_RETURN_PATH_LENGTH = 1_024;
|
|
3
|
+
/**
|
|
4
|
+
* The one path a login may return to, judged on the normalized result and
|
|
5
|
+
* never on the shape of the input: `/.//evil.com` parses on this origin but
|
|
6
|
+
* normalizes to `//evil.com`, which a browser reads as another host. Everything
|
|
7
|
+
* that is not a plain same-origin path outside the login routes — a scheme, a
|
|
8
|
+
* host, a backslash, a control character, an overlong value, a path under
|
|
9
|
+
* `loginPathPrefixArg` — becomes `/`.
|
|
10
|
+
*/
|
|
11
|
+
export const sanitizeReturnPath = (returnPathArg, publicOriginArg, loginPathPrefixArg) => {
|
|
12
|
+
const candidate = typeof returnPathArg === 'string' ? returnPathArg.trim() : '';
|
|
13
|
+
if (!candidate.startsWith('/')
|
|
14
|
+
|| candidate.length > MAX_RETURN_PATH_LENGTH
|
|
15
|
+
|| candidate.includes('\\')
|
|
16
|
+
// the URL parser drops tabs and newlines silently; such a path is refused instead of mangled
|
|
17
|
+
|| /\p{Cc}/u.test(candidate)) {
|
|
18
|
+
return '/';
|
|
19
|
+
}
|
|
20
|
+
const loginRoot = loginPathPrefixArg.endsWith('/') ? loginPathPrefixArg.slice(0, -1) : loginPathPrefixArg;
|
|
21
|
+
try {
|
|
22
|
+
const parsed = new URL(candidate, publicOriginArg);
|
|
23
|
+
if (parsed.origin !== publicOriginArg) {
|
|
24
|
+
return '/';
|
|
25
|
+
}
|
|
26
|
+
if (parsed.pathname === loginRoot || parsed.pathname.startsWith(loginRoot + '/')) {
|
|
27
|
+
return '/';
|
|
28
|
+
}
|
|
29
|
+
const normalized = parsed.pathname + parsed.search + parsed.hash;
|
|
30
|
+
if (!normalized.startsWith('/') || normalized.startsWith('//') || normalized.includes('\\')) {
|
|
31
|
+
return '/';
|
|
32
|
+
}
|
|
33
|
+
return normalized;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return '/';
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaGVscGVycy5yZXR1cm5wYXRoLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHNfcmVseWluZ3BhcnR5L2hlbHBlcnMucmV0dXJucGF0aC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxzRkFBc0Y7QUFDdEYsTUFBTSxzQkFBc0IsR0FBRyxLQUFLLENBQUM7QUFFckM7Ozs7Ozs7R0FPRztBQUNILE1BQU0sQ0FBQyxNQUFNLGtCQUFrQixHQUFHLENBQ2hDLGFBQXNCLEVBQ3RCLGVBQXVCLEVBQ3ZCLGtCQUEwQixFQUNsQixFQUFFO0lBQ1YsTUFBTSxTQUFTLEdBQUcsT0FBTyxhQUFhLEtBQUssUUFBUSxDQUFDLENBQUMsQ0FBQyxhQUFhLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztJQUNoRixJQUNFLENBQUMsU0FBUyxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUM7V0FDdkIsU0FBUyxDQUFDLE1BQU0sR0FBRyxzQkFBc0I7V0FDekMsU0FBUyxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUM7UUFDM0IsNkZBQTZGO1dBQzFGLFNBQVMsQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLEVBQzVCLENBQUM7UUFDRCxPQUFPLEdBQUcsQ0FBQztJQUNiLENBQUM7SUFDRCxNQUFNLFNBQVMsR0FBRyxrQkFBa0IsQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLGtCQUFrQixDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsa0JBQWtCLENBQUM7SUFDMUcsSUFBSSxDQUFDO1FBQ0gsTUFBTSxNQUFNLEdBQUcsSUFBSSxHQUFHLENBQUMsU0FBUyxFQUFFLGVBQWUsQ0FBQyxDQUFDO1FBQ25ELElBQUksTUFBTSxDQUFDLE1BQU0sS0FBSyxlQUFlLEVBQUUsQ0FBQztZQUN0QyxPQUFPLEdBQUcsQ0FBQztRQUNiLENBQUM7UUFDRCxJQUFJLE1BQU0sQ0FBQyxRQUFRLEtBQUssU0FBUyxJQUFJLE1BQU0sQ0FBQyxRQUFRLENBQUMsVUFBVSxDQUFDLFNBQVMsR0FBRyxHQUFHLENBQUMsRUFBRSxDQUFDO1lBQ2pGLE9BQU8sR0FBRyxDQUFDO1FBQ2IsQ0FBQztRQUNELE1BQU0sVUFBVSxHQUFHLE1BQU0sQ0FBQyxRQUFRLEdBQUcsTUFBTSxDQUFDLE1BQU0sR0FBRyxNQUFNLENBQUMsSUFBSSxDQUFDO1FBQ2pFLElBQUksQ0FBQyxVQUFVLENBQUMsVUFBVSxDQUFDLEdBQUcsQ0FBQyxJQUFJLFVBQVUsQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFDLElBQUksVUFBVSxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDO1lBQzVGLE9BQU8sR0FBRyxDQUFDO1FBQ2IsQ0FBQztRQUNELE9BQU8sVUFBVSxDQUFDO0lBQ3BCLENBQUM7SUFBQyxNQUFNLENBQUM7UUFDUCxPQUFPLEdBQUcsQ0FBQztJQUNiLENBQUM7QUFDSCxDQUFDLENBQUMifQ==
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from './interfaces.js';
|
|
2
|
+
export * from './classes.errors.js';
|
|
3
|
+
export * from './helpers.cookies.js';
|
|
4
|
+
export * from './helpers.crypto.js';
|
|
5
|
+
export * from './helpers.returnpath.js';
|
|
6
|
+
export * from './classes.secretbox.js';
|
|
7
|
+
export * from './classes.idpoidcprovider.js';
|
|
8
|
+
export * from './classes.rpmodels.js';
|
|
9
|
+
export * from './classes.relyingpartysessions.js';
|
|
10
|
+
export * from './classes.authorityobserver.js';
|
|
11
|
+
export * from './classes.relyingpartyroutes.js';
|
|
12
|
+
export * from './classes.actorscope.js';
|
|
13
|
+
export * from './classes.socketbinding.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from './interfaces.js';
|
|
2
|
+
export * from './classes.errors.js';
|
|
3
|
+
export * from './helpers.cookies.js';
|
|
4
|
+
export * from './helpers.crypto.js';
|
|
5
|
+
export * from './helpers.returnpath.js';
|
|
6
|
+
export * from './classes.secretbox.js';
|
|
7
|
+
export * from './classes.idpoidcprovider.js';
|
|
8
|
+
export * from './classes.rpmodels.js';
|
|
9
|
+
export * from './classes.relyingpartysessions.js';
|
|
10
|
+
export * from './classes.authorityobserver.js';
|
|
11
|
+
export * from './classes.relyingpartyroutes.js';
|
|
12
|
+
export * from './classes.actorscope.js';
|
|
13
|
+
export * from './classes.socketbinding.js';
|
|
14
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90c19yZWx5aW5ncGFydHkvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMsc0JBQXNCLENBQUM7QUFDckMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLHlCQUF5QixDQUFDO0FBQ3hDLGNBQWMsd0JBQXdCLENBQUM7QUFDdkMsY0FBYyw4QkFBOEIsQ0FBQztBQUM3QyxjQUFjLHVCQUF1QixDQUFDO0FBQ3RDLGNBQWMsbUNBQW1DLENBQUM7QUFDbEQsY0FBYyxnQ0FBZ0MsQ0FBQztBQUMvQyxjQUFjLGlDQUFpQyxDQUFDO0FBQ2hELGNBQWMseUJBQXlCLENBQUM7QUFDeEMsY0FBYyw0QkFBNEIsQ0FBQyJ9
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { IOidcCompletion, TOidcCompletionFailureStage, TOidcTokenHint } from './classes.idpoidcprovider.js';
|
|
2
|
+
/** A session as the application reads it; no token and no sealed value is part of it. */
|
|
3
|
+
export interface IRelyingPartySession {
|
|
4
|
+
id: string;
|
|
5
|
+
/** the application's principal (user) the session acts as */
|
|
6
|
+
principalId: string;
|
|
7
|
+
createdAt: number;
|
|
8
|
+
lastSeenAt: number;
|
|
9
|
+
expiresAt: number;
|
|
10
|
+
/** the provider subject; absent for a session the application opened itself */
|
|
11
|
+
idpSubject?: string;
|
|
12
|
+
loginAttemptId?: string;
|
|
13
|
+
}
|
|
14
|
+
/** A live session behind a cookie, with the CSRF token derived for it. */
|
|
15
|
+
export interface IResolvedRelyingPartySession {
|
|
16
|
+
session: IRelyingPartySession;
|
|
17
|
+
csrfToken: string;
|
|
18
|
+
}
|
|
19
|
+
/** A session just opened: the answer, the CSRF token and the `Set-Cookie` value that carries it. */
|
|
20
|
+
export interface IRelyingPartySessionOpening extends IResolvedRelyingPartySession {
|
|
21
|
+
sessionCookie: string;
|
|
22
|
+
}
|
|
23
|
+
/** The redirect that starts a provider login, and the cookie that binds it to this browser. */
|
|
24
|
+
export interface IRelyingPartyLoginStart {
|
|
25
|
+
authorizationUrl: URL;
|
|
26
|
+
intentCookie: string;
|
|
27
|
+
}
|
|
28
|
+
/** A provider access token usable right now, with the subject it belongs to. */
|
|
29
|
+
export interface IProviderToken {
|
|
30
|
+
accessToken: string;
|
|
31
|
+
subject: string;
|
|
32
|
+
/** epoch milliseconds; `0` when the provider named no lifetime */
|
|
33
|
+
expiresAt: number;
|
|
34
|
+
}
|
|
35
|
+
/** What an authority observer needs from the session owner. `RelyingPartySessions` implements it. */
|
|
36
|
+
export interface IAuthorityTokenSource {
|
|
37
|
+
/**
|
|
38
|
+
* The access token of a login session, rotated first when it is due. The
|
|
39
|
+
* session must belong to `expectedPrincipalIdArg`. Throws `AuthorityTokenError`.
|
|
40
|
+
*/
|
|
41
|
+
liveProviderToken(sessionIdArg: string, expectedPrincipalIdArg: string): Promise<IProviderToken>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The application's side of a provider login: who the verified subject is
|
|
45
|
+
* here. `admit` creates or updates the local principal (and whatever the
|
|
46
|
+
* application projects from the organization claims) and answers its id, or
|
|
47
|
+
* throws `RelyingPartyRefusal` to refuse the login; the provider tokens are
|
|
48
|
+
* then handed back and the refusal reaches the caller unchanged.
|
|
49
|
+
*/
|
|
50
|
+
export interface IRelyingPartyPrincipals {
|
|
51
|
+
admit(completionArg: IOidcCompletion): Promise<{
|
|
52
|
+
principalId: string;
|
|
53
|
+
}>;
|
|
54
|
+
/** Whether a principal may act right now; a session of an inactive principal resolves to nothing. */
|
|
55
|
+
isActive(principalIdArg: string): Promise<boolean>;
|
|
56
|
+
}
|
|
57
|
+
export type TRelyingPartySessionEndReason =
|
|
58
|
+
/** the application or the user ended it */
|
|
59
|
+
'logout'
|
|
60
|
+
/** the provider refused to rotate its tokens: the login is over there */
|
|
61
|
+
| 'refresh_token_refused'
|
|
62
|
+
/** its refresh token was sealed under a key this deployment no longer has */
|
|
63
|
+
| 'sealed_refresh_token_unreadable'
|
|
64
|
+
/** its access token is dead and there is nothing to rotate it with */
|
|
65
|
+
| 'access_token_unusable'
|
|
66
|
+
/** a rotation found it revoked by someone else */
|
|
67
|
+
| 'revoked_elsewhere';
|
|
68
|
+
export type TRelyingPartyEventName = 'idp_token_refresh_failed' | 'idp_token_store_failed' | 'idp_session_ended' | 'oidc_sealed_token_unreadable' | 'oidc_token_revocation_failed' | 'oidc_token_revocation_timed_out' | 'login_attempt_sweep_failed'
|
|
69
|
+
/** an application hook threw; the step it belonged to ran on regardless */
|
|
70
|
+
| 'hook_failed'
|
|
71
|
+
/** a session whose opening failed could not be revoked; its provider tokens went back anyway */
|
|
72
|
+
| 'session_discard_failed';
|
|
73
|
+
/** The application hooks whose failures `hook_failed` names. */
|
|
74
|
+
export type TRelyingPartyHookName = 'onProviderSessionOpened' | 'onSessionEnded' | 'onEvent';
|
|
75
|
+
/** One structured diagnostic line. It never carries a token, a cookie or a sealed value. */
|
|
76
|
+
export interface IRelyingPartyEvent {
|
|
77
|
+
event: TRelyingPartyEventName;
|
|
78
|
+
sessionId?: string;
|
|
79
|
+
reason?: string;
|
|
80
|
+
hint?: TOidcTokenHint;
|
|
81
|
+
hook?: TRelyingPartyHookName;
|
|
82
|
+
message?: string;
|
|
83
|
+
}
|
|
84
|
+
export interface IRelyingPartyHooks {
|
|
85
|
+
/**
|
|
86
|
+
* A provider login opened a session, linked to its attempt. Runs before the
|
|
87
|
+
* login answers; a throw fails the login: the session is revoked, the
|
|
88
|
+
* provider tokens are handed back and the original error is rethrown.
|
|
89
|
+
*/
|
|
90
|
+
onProviderSessionOpened?(eventArg: {
|
|
91
|
+
session: IRelyingPartySession;
|
|
92
|
+
}): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* A session ended, or a caller tried to end one that was already over
|
|
95
|
+
* (`revokedHere: false`). Close its live connections when `revokedHere`,
|
|
96
|
+
* drop cached authority either way. Must not wait. A throw (or a rejected
|
|
97
|
+
* promise) is reported as a `hook_failed` event and never stops the end:
|
|
98
|
+
* the provider tokens go back regardless.
|
|
99
|
+
*/
|
|
100
|
+
onSessionEnded?(eventArg: {
|
|
101
|
+
sessionId: string;
|
|
102
|
+
reason: TRelyingPartySessionEndReason;
|
|
103
|
+
revokedHere: boolean;
|
|
104
|
+
}): void;
|
|
105
|
+
/**
|
|
106
|
+
* Structured diagnostics; the default writes one JSON line to
|
|
107
|
+
* `console.error`. A throwing sink never interrupts the step that reported:
|
|
108
|
+
* the event and a `hook_failed` line go to `console.error` instead.
|
|
109
|
+
*/
|
|
110
|
+
onEvent?(eventArg: IRelyingPartyEvent): void;
|
|
111
|
+
}
|
|
112
|
+
/** Where a failed login callback stopped; for the log, never for the browser. */
|
|
113
|
+
export type TLoginCallbackStage = TOidcCompletionFailureStage | 'attempt_persistence' | 'principal_admission' | 'session_persistence';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as asyncHooks from 'node:async_hooks';
|
|
2
|
+
import * as crypto from 'node:crypto';
|
|
3
|
+
export { asyncHooks, crypto };
|
|
4
|
+
import * as typedrequest from '@api.global/typedrequest';
|
|
5
|
+
import * as typedsocket from '@api.global/typedsocket';
|
|
6
|
+
export { typedrequest, typedsocket };
|
|
7
|
+
import * as nosqldb from '@lossless.org/client/nosqldb';
|
|
8
|
+
export { nosqldb };
|
|
9
|
+
import * as openidClient from 'openid-client';
|
|
10
|
+
export { openidClient };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as asyncHooks from 'node:async_hooks';
|
|
2
|
+
import * as crypto from 'node:crypto';
|
|
3
|
+
export { asyncHooks, crypto };
|
|
4
|
+
import * as typedrequest from '@api.global/typedrequest';
|
|
5
|
+
import * as typedsocket from '@api.global/typedsocket';
|
|
6
|
+
export { typedrequest, typedsocket };
|
|
7
|
+
import * as nosqldb from '@lossless.org/client/nosqldb';
|
|
8
|
+
export { nosqldb };
|
|
9
|
+
import * as openidClient from 'openid-client';
|
|
10
|
+
export { openidClient };
|
|
11
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzX3JlbHlpbmdwYXJ0eS9wbHVnaW5zLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxVQUFVLE1BQU0sa0JBQWtCLENBQUM7QUFDL0MsT0FBTyxLQUFLLE1BQU0sTUFBTSxhQUFhLENBQUM7QUFFdEMsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLEVBQUUsQ0FBQztBQUU5QixPQUFPLEtBQUssWUFBWSxNQUFNLDBCQUEwQixDQUFDO0FBQ3pELE9BQU8sS0FBSyxXQUFXLE1BQU0seUJBQXlCLENBQUM7QUFFdkQsT0FBTyxFQUFFLFlBQVksRUFBRSxXQUFXLEVBQUUsQ0FBQztBQUVyQyxPQUFPLEtBQUssT0FBTyxNQUFNLDhCQUE4QixDQUFDO0FBRXhELE9BQU8sRUFBRSxPQUFPLEVBQUUsQ0FBQztBQUVuQixPQUFPLEtBQUssWUFBWSxNQUFNLGVBQWUsQ0FBQztBQUU5QyxPQUFPLEVBQUUsWUFBWSxFQUFFLENBQUMifQ==
|
package/package.json
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idp.global/sdk",
|
|
3
|
-
"version": "18.0
|
|
3
|
+
"version": "18.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Reusable browser and server SDK for idp.global authentication.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./dist_ts/index.js",
|
|
9
9
|
"./browser": "./dist_ts_browser/index.js",
|
|
10
|
-
"./server": "./dist_ts_server/index.js"
|
|
10
|
+
"./server": "./dist_ts_server/index.js",
|
|
11
|
+
"./relyingparty": "./dist_ts_relyingparty/index.js"
|
|
11
12
|
},
|
|
12
13
|
"main": "dist_ts/index.js",
|
|
13
14
|
"typings": "dist_ts/index.d.ts",
|
|
14
15
|
"author": "Task Venture Capital GmbH",
|
|
15
16
|
"license": "MIT",
|
|
16
17
|
"dependencies": {
|
|
17
|
-
"@api.global/typedrequest": "^8.0
|
|
18
|
+
"@api.global/typedrequest": "^8.2.0",
|
|
18
19
|
"@api.global/typedrequest-interfaces": "^7.1.0",
|
|
19
|
-
"@api.global/typedsocket": "^8.
|
|
20
|
+
"@api.global/typedsocket": "^8.7.0",
|
|
20
21
|
"@idp.global/interfaces": "^29.1.0",
|
|
21
22
|
"@push.rocks/smartjson": "^6.0.1",
|
|
22
23
|
"@push.rocks/smartpromise": "^4.2.4",
|
|
@@ -25,25 +26,27 @@
|
|
|
25
26
|
"@push.rocks/smarttime": "^4.2.3",
|
|
26
27
|
"@push.rocks/smarturl": "^3.1.0",
|
|
27
28
|
"@push.rocks/webjwt": "^1.0.10",
|
|
28
|
-
"@push.rocks/webstore": "^2.1.0"
|
|
29
|
+
"@push.rocks/webstore": "^2.1.0",
|
|
30
|
+
"openid-client": "^6.8.8"
|
|
29
31
|
},
|
|
30
32
|
"peerDependencies": {
|
|
31
33
|
"@lossless.org/client": ">=1.0.0 <2.0.0"
|
|
32
34
|
},
|
|
33
35
|
"devDependencies": {
|
|
34
36
|
"@api.global/typedserver": "^11.4.0",
|
|
35
|
-
"@git.zone/tsbuild": "^
|
|
36
|
-
"@git.zone/tsdoc": "^2.2.
|
|
37
|
-
"@git.zone/tsrun": "^
|
|
38
|
-
"@git.zone/tstest": "^6.
|
|
37
|
+
"@git.zone/tsbuild": "^5.0.0",
|
|
38
|
+
"@git.zone/tsdoc": "^2.2.2",
|
|
39
|
+
"@git.zone/tsrun": "^3.0.2",
|
|
40
|
+
"@git.zone/tstest": "^6.3.2",
|
|
39
41
|
"@lossless.org/client": "^1.0.0",
|
|
40
42
|
"@lossless.org/nosqldb": "10.5.0",
|
|
41
|
-
"@types/node": "26.6.
|
|
43
|
+
"@types/node": "26.6.2"
|
|
42
44
|
},
|
|
43
45
|
"files": [
|
|
44
46
|
"ts/**/*",
|
|
45
47
|
"ts_browser/**/*",
|
|
46
48
|
"ts_server/**/*",
|
|
49
|
+
"ts_relyingparty/**/*",
|
|
47
50
|
"dist/**/*",
|
|
48
51
|
"dist_*/**/*",
|
|
49
52
|
"readme.md",
|
package/readme.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
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
|
-
The package is intentionally split by runtime. Use `@idp.global/sdk/browser` in browser bundles
|
|
5
|
+
The package is intentionally split by runtime. Use `@idp.global/sdk/browser` in browser bundles, `@idp.global/sdk/server` in Node.js services and `@idp.global/sdk/relyingparty` for a server application that signs its users in through idp.global. The root `@idp.global/sdk` export does not expose application APIs.
|
|
6
6
|
|
|
7
7
|
## Issue Reporting and Security
|
|
8
8
|
|
|
@@ -14,6 +14,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|
|
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
16
|
| Server | `@idp.global/sdk/server` | NoSQL-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
|
+
| Relying party | `@idp.global/sdk/relyingparty` | The OIDC relying-party session layer: authorization code flow with PKCE on `openid-client`, login attempts and HttpOnly cookie sessions with CSRF tokens on NoSQL, sealed provider tokens rotated under a cross-replica lock, live organization authority, HTTP login routes, a TypedRouter actor scope and TypedSocket session binding. |
|
|
17
18
|
|
|
18
19
|
## Install
|
|
19
20
|
|
|
@@ -21,8 +22,9 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|
|
21
22
|
pnpm add @idp.global/sdk
|
|
22
23
|
```
|
|
23
24
|
|
|
24
|
-
The server
|
|
25
|
-
which provides the `SmartdataDb` the account store
|
|
25
|
+
The server and relying-party subpaths additionally require the peer dependency
|
|
26
|
+
`@lossless.org/client`, which provides the `SmartdataDb` the account store and the
|
|
27
|
+
session models persist through:
|
|
26
28
|
|
|
27
29
|
```bash
|
|
28
30
|
pnpm add @lossless.org/client
|
|
@@ -349,6 +351,16 @@ existing login and principal binding and make a fresh observation for each
|
|
|
349
351
|
operation requiring live authority. The response contains no identity profile
|
|
350
352
|
or email-based organization inference.
|
|
351
353
|
|
|
354
|
+
`AuthorityObserver` in `@idp.global/sdk/relyingparty` follows this contract by
|
|
355
|
+
default: `positiveTtlMs` and `negativeTtlMs` are `0`, so every operation is
|
|
356
|
+
observed freshly. An application may opt into reusing a positive observation for
|
|
357
|
+
a bounded time (`positiveTtlMs`) and, separately, a refusal (`negativeTtlMs`),
|
|
358
|
+
per login session and organization. The trade-off is explicit: a membership
|
|
359
|
+
removed, a role lowered or an app connection revoked at idp.global takes effect
|
|
360
|
+
in that application only once the reused answer expires — up to the configured
|
|
361
|
+
TTL — while ending the login session in the application drops its reused answers
|
|
362
|
+
at once. A failure to observe is never reused.
|
|
363
|
+
|
|
352
364
|
### Server-held OIDC UserInfo
|
|
353
365
|
|
|
354
366
|
Use `OidcUserInfoClient` to obtain current profile and organization picker data
|
|
@@ -421,6 +433,148 @@ The server export includes these TypeScript types:
|
|
|
421
433
|
| `IIdpGlobalServerClientOptions` | Server client configuration with optional `baseUrl` and `backendToken`; defaults to `https://app.idp.global`. |
|
|
422
434
|
| `IIdpPasswordAuthResult` | Privacy-safe current-user DTO, JWT, and refresh-token result. |
|
|
423
435
|
|
|
436
|
+
## Relying-Party Sessions
|
|
437
|
+
|
|
438
|
+
`@idp.global/sdk/relyingparty` is the session layer of a server application that
|
|
439
|
+
signs its users in through idp.global. The application supplies its database,
|
|
440
|
+
its keys and what a verified subject means to it; the module runs everything in
|
|
441
|
+
between.
|
|
442
|
+
|
|
443
|
+
```ts
|
|
444
|
+
import * as nosqldb from '@lossless.org/client/nosqldb';
|
|
445
|
+
import {
|
|
446
|
+
AuthorityObserver,
|
|
447
|
+
createRelyingPartyModels,
|
|
448
|
+
IdpOidcProvider,
|
|
449
|
+
RelyingPartyRoutes,
|
|
450
|
+
RelyingPartySessions,
|
|
451
|
+
decodeSecretKey,
|
|
452
|
+
} from '@idp.global/sdk/relyingparty';
|
|
453
|
+
|
|
454
|
+
const provider = new IdpOidcProvider({
|
|
455
|
+
issuer: new URL('https://idp.global'),
|
|
456
|
+
clientId: process.env.IDP_CLIENT_ID!,
|
|
457
|
+
clientSecret: process.env.IDP_CLIENT_SECRET!,
|
|
458
|
+
redirectUri: new URL('/auth/callback', 'https://app.example'),
|
|
459
|
+
});
|
|
460
|
+
await provider.init();
|
|
461
|
+
|
|
462
|
+
const models = createRelyingPartyModels({ db: smartdataDb });
|
|
463
|
+
await models.Session.ensureInitialized();
|
|
464
|
+
await models.LoginAttempt.ensureInitialized();
|
|
465
|
+
|
|
466
|
+
let authority: AuthorityObserver<'manage' | 'read'>;
|
|
467
|
+
const sessions = new RelyingPartySessions({
|
|
468
|
+
models,
|
|
469
|
+
provider,
|
|
470
|
+
publicOrigin: 'https://app.example',
|
|
471
|
+
sessionEncryptionKey: decodeSecretKey(process.env.SESSION_ENCRYPTION_KEY!, 'SESSION_ENCRYPTION_KEY'),
|
|
472
|
+
csrfKey: decodeSecretKey(process.env.CSRF_HMAC_KEY!, 'CSRF_HMAC_KEY'),
|
|
473
|
+
principals: {
|
|
474
|
+
// create or update the local user; throw RelyingPartyRefusal to refuse the login
|
|
475
|
+
admit: async (completion) => ({ principalId: await users.upsertFromIdp(completion) }),
|
|
476
|
+
isActive: async (principalId) => users.isActive(principalId),
|
|
477
|
+
},
|
|
478
|
+
hooks: {
|
|
479
|
+
onSessionEnded: ({ sessionId, revokedHere }) => {
|
|
480
|
+
authority.forgetSession(sessionId);
|
|
481
|
+
if (revokedHere) socketGate.closeSession(sessionId);
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
authority = new AuthorityObserver({
|
|
487
|
+
provider,
|
|
488
|
+
tokens: sessions,
|
|
489
|
+
project: (roles) => (roles.includes('owner') ? 'manage' : roles.includes('viewer') ? 'read' : undefined),
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
const routes = new RelyingPartyRoutes({ sessions, failureLocation: '/?auth=error' });
|
|
493
|
+
typedServer.addRoute('/auth/*', 'ALL', routes.handle);
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
What it does:
|
|
497
|
+
|
|
498
|
+
- **Login**: `GET /auth/login?return=/path` stores one login attempt (state and
|
|
499
|
+
browser binding stored as SHA-256, PKCE verifier sealed) and redirects to the
|
|
500
|
+
provider. `GET /auth/callback` consumes the attempt exactly once, exchanges
|
|
501
|
+
the code, reads UserInfo with a subject check, calls `principals.admit` and
|
|
502
|
+
opens a session; a provider `error` abandons the attempt. `GET /auth/finalize`
|
|
503
|
+
releases the return path only to the browser holding the session that attempt
|
|
504
|
+
created. Return paths stay on the public origin and outside the login routes.
|
|
505
|
+
Expired attempts are swept when the next login starts.
|
|
506
|
+
- **Sessions**: an HttpOnly, `SameSite=Lax` cookie whose token is stored as a
|
|
507
|
+
hash, touched at most once a minute, with a CSRF token derived per request as
|
|
508
|
+
an HMAC of the cookie token. `GET /auth/session` answers the session and
|
|
509
|
+
`POST /auth/logout` needs the public origin and the CSRF token; its body is
|
|
510
|
+
cancelled as soon as it passes `maxBodyBytes`. Every end of a session is one
|
|
511
|
+
conditional update; its provider tokens go back to idp.global within a
|
|
512
|
+
three-second budget afterwards, and so do the tokens of a login that failed
|
|
513
|
+
after the code exchange.
|
|
514
|
+
- **Provider tokens**: sealed with AES-256-GCM (`SecretBox`) and rotated shortly
|
|
515
|
+
before they expire, once per process and under a lock stored on the session,
|
|
516
|
+
so two replicas never spend one refresh token. A refused refresh ends the
|
|
517
|
+
session; an unreachable provider fails closed.
|
|
518
|
+
- **Live authority**: `AuthorityObserver.observe({ loginSessionId, principalId,
|
|
519
|
+
idpOrganizationId })` introspects the organization with the session's live
|
|
520
|
+
token and projects the role keys through the application's table. It answers
|
|
521
|
+
a grant or throws `AuthorityRefusedError` (with `firstRefusal` once per
|
|
522
|
+
transition, for an audit trail) or `AuthorityTokenError`.
|
|
523
|
+
- **Transports**: `createActorScope()` gives a TypedRouter request scope
|
|
524
|
+
(`addRequestScope`) that runs every dispatch as the actor its trusted local
|
|
525
|
+
data names. `RelyingPartySocketBinding` registers a TypedSocket connection
|
|
526
|
+
from its upgrade `Origin` and `Cookie` plus the page's CSRF token, pins it to
|
|
527
|
+
the session, re-validates it per request and finds a session's connections by
|
|
528
|
+
a server-owned tag. Connection limits and sweeps stay with the application.
|
|
529
|
+
|
|
530
|
+
Nothing runs on a timer, and no token, cookie or sealed value ever reaches a
|
|
531
|
+
log line or an error message.
|
|
532
|
+
|
|
533
|
+
Application hooks never stand in the way of that cleanup. A throwing (or
|
|
534
|
+
rejecting) `onSessionEnded` or `onEvent` is reported as a `hook_failed` event —
|
|
535
|
+
`onEvent`'s own failure on `console.error` — and the provider tokens go back
|
|
536
|
+
regardless. A throwing `onProviderSessionOpened` fails the login: the session it
|
|
537
|
+
opened is revoked, its tokens are handed back and the hook's error is the
|
|
538
|
+
login's cause. Custom `cookieNames` with a `__Host-` or `__Secure-` prefix need
|
|
539
|
+
an https `publicOrigin`; on http the constructor refuses them, because browsers
|
|
540
|
+
would silently drop those cookies.
|
|
541
|
+
|
|
542
|
+
**Adopting existing session data.** The persisted fields and index declarations
|
|
543
|
+
are those nevermind.finance has stored since mainapp 0b693cb. An application
|
|
544
|
+
that already holds such collections binds the models to them and passes its own
|
|
545
|
+
cookie names and CSRF label, so live sessions and sealed tokens keep working:
|
|
546
|
+
|
|
547
|
+
```ts
|
|
548
|
+
const models = createRelyingPartyModels({
|
|
549
|
+
db: smartdataDb,
|
|
550
|
+
collectionNames: { sessions: 'SessionDoc', loginAttempts: 'LoginAttemptDoc' },
|
|
551
|
+
});
|
|
552
|
+
const sessions = new RelyingPartySessions({
|
|
553
|
+
models,
|
|
554
|
+
cookieNames: { session: 'nm_session', intent: '__Host-nm_oidc_intent' },
|
|
555
|
+
csrfLabel: 'nevermind.finance:csrf:v1\u0000',
|
|
556
|
+
// …
|
|
557
|
+
});
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
A deployment without a provider (`provider` omitted) keeps the same session
|
|
561
|
+
store for sessions it opens itself with `openSession({ principalId })`, for
|
|
562
|
+
example after a local password check.
|
|
563
|
+
|
|
564
|
+
### Relying-Party API Surface
|
|
565
|
+
|
|
566
|
+
| API | Description |
|
|
567
|
+
| --- | --- |
|
|
568
|
+
| `IdpOidcProvider` | Discovery, PKCE authorization URL, code exchange with UserInfo, refresh, revocation and organization introspection for idp.global. Implements `IOidcProvider`. |
|
|
569
|
+
| `createRelyingPartyModels()` | Binds the session and login-attempt models to one `SmartdataDb`, optionally under existing collection names. |
|
|
570
|
+
| `RelyingPartySessions` | Login start/complete/abandon/finalize, `openSession`, `resolveCookieSession`, `revalidateSession`, `isSessionLive`, `validateCsrf`, `logout`/`endSession`, `endSessionsOfPrincipal`, `liveProviderToken`. |
|
|
571
|
+
| `AuthorityObserver` | Live organization authority with opt-in positive and negative reuse. |
|
|
572
|
+
| `RelyingPartyRoutes` | `handle(ctx)` for `/auth/login`, `/auth/callback`, `/auth/finalize`, `/auth/session` and `/auth/logout`; `null` for anything else. Takes a typedserver route context. |
|
|
573
|
+
| `createActorScope()` | AsyncLocalStorage actor context with a TypedRouter request scope. |
|
|
574
|
+
| `RelyingPartySocketBinding` | TypedSocket browser registration, per-request re-validation and session tags. |
|
|
575
|
+
| `SecretBox`, `deriveCsrfToken()`, `decodeSecretKey()`, `parseCookies()`, `serializeCookie()`, `sanitizeReturnPath()` | The primitives underneath. |
|
|
576
|
+
| `RelyingPartyError`, `RelyingPartyRefusal`, `LoginCallbackError`, `AuthorityTokenError`, `AuthorityRefusedError` | Errors with stable codes; none carries token material. |
|
|
577
|
+
|
|
424
578
|
## Runtime Notes
|
|
425
579
|
|
|
426
580
|
Use the explicit subpath imports. They keep browser bundles free of Node-only code and keep server processes free of browser storage dependencies.
|
|
@@ -438,7 +592,7 @@ The SDK uses typed sockets and typed requests from `@api.global/*`, shared idp.g
|
|
|
438
592
|
pnpm test
|
|
439
593
|
```
|
|
440
594
|
|
|
441
|
-
The test suite covers the server account store and auth service, browser transfer-token handling, typed administrative request names,
|
|
595
|
+
The test suite covers the server account store and auth service, browser transfer-token handling, typed administrative request names, the exact backend JWT keyset and blocklist contracts, and the relying-party layer: its primitives, the OIDC provider against a fetch-level issuer double, sessions and two-replica token rotation on an in-memory NoSQL engine with an injected clock, the persisted shape, the authority observer, the HTTP routes, the actor scope and the socket binding.
|
|
442
596
|
|
|
443
597
|
## License and Legal Information
|
|
444
598
|
|