@idp.global/sdk 18.1.0 → 18.2.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 +11 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts_relyingparty/classes.errors.d.ts +4 -0
- package/dist_ts_relyingparty/classes.errors.js +8 -1
- package/dist_ts_relyingparty/classes.relyingpartyroutes.d.ts +26 -8
- package/dist_ts_relyingparty/classes.relyingpartyroutes.js +19 -3
- package/dist_ts_relyingparty/classes.relyingpartysessions.d.ts +22 -9
- package/dist_ts_relyingparty/classes.relyingpartysessions.js +50 -11
- package/dist_ts_relyingparty/classes.rpmodels.d.ts +10 -2
- package/dist_ts_relyingparty/classes.rpmodels.js +26 -7
- package/dist_ts_relyingparty/classes.socketbinding.d.ts +18 -9
- package/dist_ts_relyingparty/classes.socketbinding.js +17 -11
- package/dist_ts_relyingparty/interfaces.d.ts +32 -3
- package/package.json +1 -1
- package/readme.md +62 -5
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts_relyingparty/classes.errors.ts +8 -0
- package/ts_relyingparty/classes.relyingpartyroutes.ts +46 -13
- package/ts_relyingparty/classes.relyingpartysessions.ts +65 -18
- package/ts_relyingparty/classes.rpmodels.ts +31 -7
- package/ts_relyingparty/classes.socketbinding.ts +30 -16
- package/ts_relyingparty/interfaces.ts +35 -3
|
@@ -57,7 +57,13 @@ export declare const RELYING_PARTY_DEFAULT_COLLECTION_NAMES: {
|
|
|
57
57
|
readonly loginAttempts: "IdpRelyingPartyLoginAttempt";
|
|
58
58
|
};
|
|
59
59
|
export interface IRelyingPartyModelsOptions {
|
|
60
|
-
|
|
60
|
+
/**
|
|
61
|
+
* The database, or a function that answers it once it exists. The function
|
|
62
|
+
* is called on every use of a model — never while the models or
|
|
63
|
+
* `RelyingPartySessions` are constructed — so an application creates both
|
|
64
|
+
* before its database connects and binds the database it connects later.
|
|
65
|
+
*/
|
|
66
|
+
db: plugins.nosqldb.SmartdataDb | (() => plugins.nosqldb.SmartdataDb);
|
|
61
67
|
/**
|
|
62
68
|
* The persisted collection names. An application that already stores these
|
|
63
69
|
* documents names its existing collections here.
|
|
@@ -74,6 +80,8 @@ export interface IRelyingPartyModels {
|
|
|
74
80
|
/**
|
|
75
81
|
* Binds the two relying-party models to one database. Each call declares its
|
|
76
82
|
* own model classes, so several databases — and several applications in one
|
|
77
|
-
* process — never share a binding.
|
|
83
|
+
* process — never share a binding. A deferred `db` is resolved on every use,
|
|
84
|
+
* the way `@Collection(() => db)` resolves it; a use before it answers a
|
|
85
|
+
* `SmartdataDb` throws `RelyingPartyDatabaseUnavailableError`.
|
|
78
86
|
*/
|
|
79
87
|
export declare const createRelyingPartyModels: (optionsArg: IRelyingPartyModelsOptions) => IRelyingPartyModels;
|
|
@@ -33,6 +33,7 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
|
|
|
33
33
|
return useValue ? value : void 0;
|
|
34
34
|
};
|
|
35
35
|
import * as plugins from './plugins.js';
|
|
36
|
+
import { RelyingPartyDatabaseUnavailableError } from './classes.errors.js';
|
|
36
37
|
/**
|
|
37
38
|
* One browser session of a relying party. The persisted field names, their
|
|
38
39
|
* types and the index declarations are the ones nevermind.finance has stored
|
|
@@ -232,18 +233,36 @@ export const RELYING_PARTY_DEFAULT_COLLECTION_NAMES = {
|
|
|
232
233
|
/**
|
|
233
234
|
* Binds the two relying-party models to one database. Each call declares its
|
|
234
235
|
* own model classes, so several databases — and several applications in one
|
|
235
|
-
* process — never share a binding.
|
|
236
|
+
* process — never share a binding. A deferred `db` is resolved on every use,
|
|
237
|
+
* the way `@Collection(() => db)` resolves it; a use before it answers a
|
|
238
|
+
* `SmartdataDb` throws `RelyingPartyDatabaseUnavailableError`.
|
|
236
239
|
*/
|
|
237
240
|
export const createRelyingPartyModels = (optionsArg) => {
|
|
238
|
-
const
|
|
239
|
-
if (!(
|
|
240
|
-
throw new Error('createRelyingPartyModels needs a SmartdataDb of @lossless.org/client/nosqldb');
|
|
241
|
+
const dbOption = optionsArg.db;
|
|
242
|
+
if (!(dbOption instanceof plugins.nosqldb.SmartdataDb) && typeof dbOption !== 'function') {
|
|
243
|
+
throw new Error('createRelyingPartyModels needs a SmartdataDb of @lossless.org/client/nosqldb or a function answering one');
|
|
241
244
|
}
|
|
245
|
+
const resolveDb = () => {
|
|
246
|
+
if (dbOption instanceof plugins.nosqldb.SmartdataDb) {
|
|
247
|
+
return dbOption;
|
|
248
|
+
}
|
|
249
|
+
let resolved;
|
|
250
|
+
try {
|
|
251
|
+
resolved = dbOption();
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
throw new RelyingPartyDatabaseUnavailableError(error);
|
|
255
|
+
}
|
|
256
|
+
if (!(resolved instanceof plugins.nosqldb.SmartdataDb)) {
|
|
257
|
+
throw new RelyingPartyDatabaseUnavailableError();
|
|
258
|
+
}
|
|
259
|
+
return resolved;
|
|
260
|
+
};
|
|
242
261
|
const sessions = optionsArg.collectionNames?.sessions ?? RELYING_PARTY_DEFAULT_COLLECTION_NAMES.sessions;
|
|
243
262
|
const loginAttempts = optionsArg.collectionNames?.loginAttempts
|
|
244
263
|
?? RELYING_PARTY_DEFAULT_COLLECTION_NAMES.loginAttempts;
|
|
245
264
|
let BoundSessionDoc = (() => {
|
|
246
|
-
let _classDecorators = [plugins.nosqldb.Collection(
|
|
265
|
+
let _classDecorators = [plugins.nosqldb.Collection(resolveDb, { collectionName: sessions })];
|
|
247
266
|
let _classDescriptor;
|
|
248
267
|
let _classExtraInitializers = [];
|
|
249
268
|
let _classThis;
|
|
@@ -261,7 +280,7 @@ export const createRelyingPartyModels = (optionsArg) => {
|
|
|
261
280
|
return BoundSessionDoc = _classThis;
|
|
262
281
|
})();
|
|
263
282
|
let BoundLoginAttemptDoc = (() => {
|
|
264
|
-
let _classDecorators = [plugins.nosqldb.Collection(
|
|
283
|
+
let _classDecorators = [plugins.nosqldb.Collection(resolveDb, { collectionName: loginAttempts })];
|
|
265
284
|
let _classDescriptor;
|
|
266
285
|
let _classExtraInitializers = [];
|
|
267
286
|
let _classThis;
|
|
@@ -280,4 +299,4 @@ export const createRelyingPartyModels = (optionsArg) => {
|
|
|
280
299
|
})();
|
|
281
300
|
return { Session: BoundSessionDoc, LoginAttempt: BoundLoginAttemptDoc };
|
|
282
301
|
};
|
|
283
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
302
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5ycG1vZGVscy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzX3JlbHlpbmdwYXJ0eS9jbGFzc2VzLnJwbW9kZWxzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQUN4QyxPQUFPLEVBQUUsb0NBQW9DLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUUzRTs7Ozs7R0FLRztJQUNVLHNCQUFzQjtzQkFBUyxPQUFPLENBQUMsT0FBTyxDQUFDLGNBQWM7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7aUJBQTdELHNCQUF1QixTQUFRLFdBQThFOzs7OEJBQ3ZILE9BQU8sQ0FBQyxPQUFPLENBQUMsR0FBRyxFQUFFO3FDQUlyQixPQUFPLENBQUMsT0FBTyxDQUFDLEdBQUcsRUFBRTtrQ0FJckIsT0FBTyxDQUFDLE9BQU8sQ0FBQyxJQUFJLEVBQUUsRUFDdEIsT0FBTyxDQUFDLE9BQU8sQ0FBQyxLQUFLLEVBQUU7cUNBR3ZCLE9BQU8sQ0FBQyxPQUFPLENBQUMsSUFBSSxFQUFFO3NDQUd0QixPQUFPLENBQUMsT0FBTyxDQUFDLElBQUksRUFBRTtxQ0FHdEIsT0FBTyxDQUFDLE9BQU8sQ0FBQyxJQUFJLEVBQUU7cUNBSXRCLE9BQU8sQ0FBQyxPQUFPLENBQUMsSUFBSSxFQUFFO3NDQUl0QixPQUFPLENBQUMsT0FBTyxDQUFDLElBQUksRUFBRTswQ0FJdEIsT0FBTyxDQUFDLE9BQU8sQ0FBQyxJQUFJLEVBQUU7NkNBSXRCLE9BQU8sQ0FBQyxPQUFPLENBQUMsSUFBSSxFQUFFOzhDQUd0QixPQUFPLENBQUMsT0FBTyxDQUFDLElBQUksRUFBRTtnREFJdEIsT0FBTyxDQUFDLE9BQU8sQ0FBQyxJQUFJLEVBQUU7NENBSXRCLE9BQU8sQ0FBQyxPQUFPLENBQUMsSUFBSSxFQUFFO1lBNUN2Qix3SkFBTyxFQUFFLDZCQUFGLEVBQUUsK0VBQWM7WUFJdkIsNktBQU8sU0FBUyw2QkFBVCxTQUFTLDZGQUFjO1lBSzlCLG9LQUFPLE1BQU0sNkJBQU4sTUFBTSx1RkFBYztZQUczQiw2S0FBTyxTQUFTLDZCQUFULFNBQVMsNkZBQWE7WUFHN0IsZ0xBQU8sVUFBVSw2QkFBVixVQUFVLCtGQUFhO1lBRzlCLDZLQUFPLFNBQVMsNkJBQVQsU0FBUyw2RkFBYTtZQUk3Qiw2S0FBTyxTQUFTLDZCQUFULFNBQVMsNkZBQVU7WUFJMUIsZ0xBQU8sVUFBVSw2QkFBVixVQUFVLCtGQUFVO1lBSTNCLDRMQUFPLGNBQWMsNkJBQWQsY0FBYyx1R0FBVTtZQUkvQixxTUFBTyxpQkFBaUIsNkJBQWpCLGlCQUFpQiw2R0FBVTtZQUdsQyx3TUFBTyxrQkFBa0IsNkJBQWxCLGtCQUFrQiwrR0FBVTtZQUluQyw4TUFBTyxvQkFBb0IsNkJBQXBCLG9CQUFvQixtSEFBVTtZQUlyQyxrTUFBTyxnQkFBZ0IsNkJBQWhCLGdCQUFnQiwyR0FBVTs7O1FBN0MxQixFQUFFLDZDQUFXLEVBQUUsRUFBQztRQUV2QixtRUFBbUU7UUFFNUQsU0FBUyxxR0FBVyxFQUFFLEdBQUM7UUFFOUIsOERBQThEO1FBR3ZELE1BQU0seUdBQVcsRUFBRSxHQUFDO1FBR3BCLFNBQVMseUdBQVcsQ0FBQyxHQUFDO1FBR3RCLFVBQVUsNkdBQVcsQ0FBQyxHQUFDO1FBR3ZCLFNBQVMsNkdBQVcsQ0FBQyxHQUFDO1FBRTdCLHdDQUF3QztRQUVqQyxTQUFTLHFIQUFVO1FBRTFCLG1HQUFtRztRQUU1RixVQUFVLHNIQUFVO1FBRTNCLHNFQUFzRTtRQUUvRCxjQUFjLDJIQUFVO1FBRS9CLG1GQUFtRjtRQUU1RSxpQkFBaUIsa0lBQVU7UUFHM0Isa0JBQWtCLHNJQUFVO1FBRW5DLHFHQUFxRztRQUU5RixvQkFBb0IseUlBQVU7UUFFckMsaUdBQWlHO1FBRTFGLGdCQUFnQix1SUFBVTs7Ozs7OztTQS9DdEIsc0JBQXNCO0FBa0RuQzs7Ozs7O0dBTUc7SUFDVSwyQkFBMkI7c0JBQVMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxjQUFjOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O2lCQUFsRSwyQkFBNEIsU0FBUSxXQUF3Rjs7OzhCQUN0SSxPQUFPLENBQUMsT0FBTyxDQUFDLEdBQUcsRUFBRTtxQ0FJckIsT0FBTyxDQUFDLE9BQU8sQ0FBQyxHQUFHLEVBQUU7NENBSXJCLE9BQU8sQ0FBQyxPQUFPLENBQUMsR0FBRyxFQUFFOzhDQUdyQixPQUFPLENBQUMsT0FBTyxDQUFDLElBQUksRUFBRTtpQ0FHdEIsT0FBTyxDQUFDLE9BQU8sQ0FBQyxJQUFJLEVBQUU7c0NBSXRCLE9BQU8sQ0FBQyxPQUFPLENBQUMsSUFBSSxFQUFFO3FDQUd0QixPQUFPLENBQUMsT0FBTyxDQUFDLElBQUksRUFBRTtxQ0FHdEIsT0FBTyxDQUFDLE9BQU8sQ0FBQyxJQUFJLEVBQUU7c0NBSXRCLE9BQU8sQ0FBQyxPQUFPLENBQUMsSUFBSSxFQUFFOzRDQUl0QixPQUFPLENBQUMsT0FBTyxDQUFDLElBQUksRUFBRTtZQS9CdkIsd0pBQU8sRUFBRSw2QkFBRixFQUFFLCtFQUFjO1lBSXZCLDZLQUFPLFNBQVMsNkJBQVQsU0FBUyw2RkFBYztZQUk5QixrTUFBTyxnQkFBZ0IsNkJBQWhCLGdCQUFnQiwyR0FBYztZQUdyQyx3TUFBTyxrQkFBa0IsNkJBQWxCLGtCQUFrQiwrR0FBYztZQUd2QyxpS0FBTyxLQUFLLDZCQUFMLEtBQUsscUZBQWM7WUFJMUIsZ0xBQU8sVUFBVSw2QkFBVixVQUFVLCtGQUFlO1lBR2hDLDZLQUFPLFNBQVMsNkJBQVQsU0FBUyw2RkFBYTtZQUc3Qiw2S0FBTyxTQUFTLDZCQUFULFNBQVMsNkZBQWE7WUFJN0IsZ0xBQU8sVUFBVSw2QkFBVixVQUFVLCtGQUFpQjtZQUlsQyxrTUFBTyxnQkFBZ0IsNkJBQWhCLGdCQUFnQiwyR0FBaUI7OztRQWhDakMsRUFBRSw2Q0FBVyxFQUFFLEVBQUM7UUFFdkIsNEVBQTRFO1FBRXJFLFNBQVMscUdBQVcsRUFBRSxHQUFDO1FBRTlCLDJGQUEyRjtRQUVwRixnQkFBZ0IsbUhBQVcsRUFBRSxHQUFDO1FBRzlCLGtCQUFrQiw0SEFBVyxFQUFFLEdBQUM7UUFHaEMsS0FBSyxpSEFBVyxFQUFFLEdBQUM7UUFFMUIsOENBQThDO1FBRXZDLFVBQVUseUdBQVcsR0FBRyxHQUFDO1FBR3pCLFNBQVMsNkdBQVcsQ0FBQyxHQUFDO1FBR3RCLFNBQVMsNEdBQVcsQ0FBQyxHQUFDO1FBRTdCLGdHQUFnRztRQUV6RixVQUFVLHNIQUFpQjtRQUVsQyxxRkFBcUY7UUFFOUUsZ0JBQWdCLDZIQUFpQjs7Ozs7OztTQWxDN0IsMkJBQTJCO0FBcUN4QyxNQUFNLENBQUMsTUFBTSxzQ0FBc0MsR0FBRztJQUNwRCxRQUFRLEVBQUUsd0JBQXdCO0lBQ2xDLGFBQWEsRUFBRSw2QkFBNkI7Q0FDcEMsQ0FBQztBQXlCWDs7Ozs7O0dBTUc7QUFDSCxNQUFNLENBQUMsTUFBTSx3QkFBd0IsR0FBRyxDQUFDLFVBQXNDLEVBQXVCLEVBQUU7SUFDdEcsTUFBTSxRQUFRLEdBQUcsVUFBVSxDQUFDLEVBQUUsQ0FBQztJQUMvQixJQUFJLENBQUMsQ0FBQyxRQUFRLFlBQVksT0FBTyxDQUFDLE9BQU8sQ0FBQyxXQUFXLENBQUMsSUFBSSxPQUFPLFFBQVEsS0FBSyxVQUFVLEVBQUUsQ0FBQztRQUN6RixNQUFNLElBQUksS0FBSyxDQUFDLDBHQUEwRyxDQUFDLENBQUM7SUFDOUgsQ0FBQztJQUNELE1BQU0sU0FBUyxHQUFHLEdBQWdDLEVBQUU7UUFDbEQsSUFBSSxRQUFRLFlBQVksT0FBTyxDQUFDLE9BQU8sQ0FBQyxXQUFXLEVBQUUsQ0FBQztZQUNwRCxPQUFPLFFBQVEsQ0FBQztRQUNsQixDQUFDO1FBQ0QsSUFBSSxRQUFpQixDQUFDO1FBQ3RCLElBQUksQ0FBQztZQUNILFFBQVEsR0FBRyxRQUFRLEVBQUUsQ0FBQztRQUN4QixDQUFDO1FBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztZQUNmLE1BQU0sSUFBSSxvQ0FBb0MsQ0FBQyxLQUFLLENBQUMsQ0FBQztRQUN4RCxDQUFDO1FBQ0QsSUFBSSxDQUFDLENBQUMsUUFBUSxZQUFZLE9BQU8sQ0FBQyxPQUFPLENBQUMsV0FBVyxDQUFDLEVBQUUsQ0FBQztZQUN2RCxNQUFNLElBQUksb0NBQW9DLEVBQUUsQ0FBQztRQUNuRCxDQUFDO1FBQ0QsT0FBTyxRQUFRLENBQUM7SUFDbEIsQ0FBQyxDQUFDO0lBQ0YsTUFBTSxRQUFRLEdBQUcsVUFBVSxDQUFDLGVBQWUsRUFBRSxRQUFRLElBQUksc0NBQXNDLENBQUMsUUFBUSxDQUFDO0lBQ3pHLE1BQU0sYUFBYSxHQUFHLFVBQVUsQ0FBQyxlQUFlLEVBQUUsYUFBYTtXQUMxRCxzQ0FBc0MsQ0FBQyxhQUFhLENBQUM7UUFHcEQsZUFBZTtnQ0FEcEIsT0FBTyxDQUFDLE9BQU8sQ0FBQyxVQUFVLENBQUMsU0FBUyxFQUFFLEVBQUUsY0FBYyxFQUFFLFFBQVEsRUFBRSxDQUFDOzs7OzBCQUN0QyxzQkFBc0I7bUNBQTlCLFNBQVEsV0FBc0I7Ozs7Z0JBQXBELDZLQUF1RDs7O2dCQUFqRCx1REFBZTs7Ozs7UUFHZixvQkFBb0I7Z0NBRHpCLE9BQU8sQ0FBQyxPQUFPLENBQUMsVUFBVSxDQUFDLFNBQVMsRUFBRSxFQUFFLGNBQWMsRUFBRSxhQUFhLEVBQUUsQ0FBQzs7OzswQkFDdEMsMkJBQTJCO3dDQUFuQyxTQUFRLFdBQTJCOzs7O2dCQUE5RCw2S0FBaUU7OztnQkFBM0QsdURBQW9COzs7OztJQUUxQixPQUFPLEVBQUUsT0FBTyxFQUFFLGVBQWUsRUFBRSxZQUFZLEVBQUUsb0JBQW9CLEVBQUUsQ0FBQztBQUMxRSxDQUFDLENBQUMifQ==
|
|
@@ -4,6 +4,10 @@ import type { IRelyingPartySession } from './interfaces.js';
|
|
|
4
4
|
/** The part of a server `TypedSocket` the binding uses. */
|
|
5
5
|
export type TRelyingPartySocketTransport = Pick<plugins.typedsocket.TypedSocket, 'setServerTag' | 'findAllTargetConnectionsByTag'>;
|
|
6
6
|
export type TRelyingPartySocketConnection = plugins.typedsocket.ISmartServeConnectionWrapper;
|
|
7
|
+
/** The SmartServe WebSocket peer underneath a connection, as a TypedSocket server hands it out. */
|
|
8
|
+
export type TRelyingPartySocketPeer = TRelyingPartySocketConnection['peer'];
|
|
9
|
+
/** A connection wrapper or its peer; `TypedSocket.setServerTag` takes either. */
|
|
10
|
+
export type TRelyingPartySocketTarget = TRelyingPartySocketConnection | TRelyingPartySocketPeer;
|
|
7
11
|
/** What a browser connection is pinned to for its whole life; no cookie and no CSRF token is kept. */
|
|
8
12
|
export interface IRelyingPartyPinnedSession {
|
|
9
13
|
sessionId: string;
|
|
@@ -23,8 +27,8 @@ export type TRelyingPartySocketRegistration = {
|
|
|
23
27
|
registered: false;
|
|
24
28
|
refusal: IRelyingPartySocketRefusal;
|
|
25
29
|
};
|
|
26
|
-
export interface IRelyingPartySocketBindingOptions {
|
|
27
|
-
sessions: RelyingPartySessions
|
|
30
|
+
export interface IRelyingPartySocketBindingOptions<TPrincipal extends object | undefined = undefined> {
|
|
31
|
+
sessions: RelyingPartySessions<TPrincipal>;
|
|
28
32
|
/** the transport typedserver created; `undefined` before the server runs */
|
|
29
33
|
transport: () => TRelyingPartySocketTransport | undefined;
|
|
30
34
|
/** the server-owned tag prefix that finds the connections of a session */
|
|
@@ -39,12 +43,12 @@ export interface IRelyingPartySocketBindingOptions {
|
|
|
39
43
|
* request. Connection limits, rate limits and sweeps belong to the
|
|
40
44
|
* application's socket gate.
|
|
41
45
|
*/
|
|
42
|
-
export declare class RelyingPartySocketBinding {
|
|
46
|
+
export declare class RelyingPartySocketBinding<TPrincipal extends object | undefined = undefined> {
|
|
43
47
|
#private;
|
|
44
48
|
readonly tagPrefix: string;
|
|
45
|
-
constructor(optionsArg: IRelyingPartySocketBindingOptions);
|
|
46
|
-
/** The upgrade headers a connection arrived with. */
|
|
47
|
-
upgradeOf(
|
|
49
|
+
constructor(optionsArg: IRelyingPartySocketBindingOptions<TPrincipal>);
|
|
50
|
+
/** The upgrade headers a connection (or its peer) arrived with. */
|
|
51
|
+
upgradeOf(targetArg: TRelyingPartySocketTarget): {
|
|
48
52
|
origin: string | null;
|
|
49
53
|
cookie: string | null;
|
|
50
54
|
};
|
|
@@ -58,16 +62,21 @@ export declare class RelyingPartySocketBinding {
|
|
|
58
62
|
cookie: string | null;
|
|
59
63
|
}): Promise<TRelyingPartySocketRegistration>;
|
|
60
64
|
tagOf(sessionIdArg: string): string;
|
|
61
|
-
/**
|
|
62
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Tags a connection — its wrapper or its peer — with its session; throws
|
|
67
|
+
* when the transport is not running or the connection is gone.
|
|
68
|
+
*/
|
|
69
|
+
tagConnection(targetArg: TRelyingPartySocketTarget, sessionIdArg: string): void;
|
|
63
70
|
/**
|
|
64
71
|
* The pinned session as it stands right now: re-read, live, its principal
|
|
65
|
-
* active and unchanged
|
|
72
|
+
* active and unchanged (and resolved, with `principals.resolve`), touched
|
|
73
|
+
* under the liveness it was read with. The
|
|
66
74
|
* browser keeps proving itself with its CSRF token, compared against the
|
|
67
75
|
* hash the registration kept. Nothing of this is cached.
|
|
68
76
|
*/
|
|
69
77
|
revalidate(pinnedArg: IRelyingPartyPinnedSession): Promise<{
|
|
70
78
|
session: IRelyingPartySession;
|
|
79
|
+
principal: TPrincipal;
|
|
71
80
|
verifyCsrf(candidateArg: unknown): boolean;
|
|
72
81
|
} | null>;
|
|
73
82
|
/** The live connections of one session, for closing them when it ends. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import { sha256, timingSafeEqualString } from './helpers.crypto.js';
|
|
3
|
+
const peerOf = (targetArg) => 'peer' in targetArg ? targetArg.peer : targetArg;
|
|
3
4
|
/**
|
|
4
5
|
* Binds typed socket connections to relying-party browser sessions. A
|
|
5
6
|
* connection registers once with the CSRF token of the page; the proof is the
|
|
@@ -18,9 +19,9 @@ export class RelyingPartySocketBinding {
|
|
|
18
19
|
this.#transport = optionsArg.transport;
|
|
19
20
|
this.tagPrefix = optionsArg.tagPrefix ?? 'idp_rp_session:';
|
|
20
21
|
}
|
|
21
|
-
/** The upgrade headers a connection arrived with. */
|
|
22
|
-
upgradeOf(
|
|
23
|
-
const headers =
|
|
22
|
+
/** The upgrade headers a connection (or its peer) arrived with. */
|
|
23
|
+
upgradeOf(targetArg) {
|
|
24
|
+
const headers = peerOf(targetArg).context.headers;
|
|
24
25
|
return { origin: headers.get('origin'), cookie: headers.get('cookie') };
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
@@ -55,28 +56,33 @@ export class RelyingPartySocketBinding {
|
|
|
55
56
|
tagOf(sessionIdArg) {
|
|
56
57
|
return this.tagPrefix + sessionIdArg;
|
|
57
58
|
}
|
|
58
|
-
/**
|
|
59
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Tags a connection — its wrapper or its peer — with its session; throws
|
|
61
|
+
* when the transport is not running or the connection is gone.
|
|
62
|
+
*/
|
|
63
|
+
tagConnection(targetArg, sessionIdArg) {
|
|
60
64
|
const transport = this.#transport();
|
|
61
65
|
if (!transport) {
|
|
62
66
|
throw new Error('The socket transport is not running');
|
|
63
67
|
}
|
|
64
|
-
transport.setServerTag(
|
|
68
|
+
transport.setServerTag(targetArg, this.tagOf(sessionIdArg));
|
|
65
69
|
}
|
|
66
70
|
/**
|
|
67
71
|
* The pinned session as it stands right now: re-read, live, its principal
|
|
68
|
-
* active and unchanged
|
|
72
|
+
* active and unchanged (and resolved, with `principals.resolve`), touched
|
|
73
|
+
* under the liveness it was read with. The
|
|
69
74
|
* browser keeps proving itself with its CSRF token, compared against the
|
|
70
75
|
* hash the registration kept. Nothing of this is cached.
|
|
71
76
|
*/
|
|
72
77
|
async revalidate(pinnedArg) {
|
|
73
|
-
const
|
|
74
|
-
if (!
|
|
78
|
+
const live = await this.#sessions.revalidateLiveSession(pinnedArg.sessionId);
|
|
79
|
+
if (!live || live.session.principalId !== pinnedArg.principalId) {
|
|
75
80
|
return null;
|
|
76
81
|
}
|
|
77
82
|
const expectedHash = pinnedArg.csrfTokenHash;
|
|
78
83
|
return {
|
|
79
|
-
session,
|
|
84
|
+
session: live.session,
|
|
85
|
+
principal: live.principal,
|
|
80
86
|
verifyCsrf: (candidateArg) => typeof candidateArg === 'string'
|
|
81
87
|
&& expectedHash.length > 0
|
|
82
88
|
&& timingSafeEqualString(expectedHash, sha256(candidateArg)),
|
|
@@ -91,4 +97,4 @@ export class RelyingPartySocketBinding {
|
|
|
91
97
|
return transport.findAllTargetConnectionsByTag(this.tagOf(sessionIdArg));
|
|
92
98
|
}
|
|
93
99
|
}
|
|
94
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
100
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5zb2NrZXRiaW5kaW5nLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHNfcmVseWluZ3BhcnR5L2NsYXNzZXMuc29ja2V0YmluZGluZy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQUV4QyxPQUFPLEVBQUUsTUFBTSxFQUFFLHFCQUFxQixFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFpQnBFLE1BQU0sTUFBTSxHQUFHLENBQUMsU0FBb0MsRUFBMkIsRUFBRSxDQUMvRSxNQUFNLElBQUksU0FBUyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUM7QUEyQm5EOzs7Ozs7OztHQVFHO0FBQ0gsTUFBTSxPQUFPLHlCQUF5QjtJQUMzQixTQUFTLENBQW1DO0lBQzVDLFVBQVUsQ0FBaUQ7SUFDcEQsU0FBUyxDQUFTO0lBRWxDLFlBQVksVUFBeUQ7UUFDbkUsSUFBSSxDQUFDLFNBQVMsR0FBRyxVQUFVLENBQUMsUUFBUSxDQUFDO1FBQ3JDLElBQUksQ0FBQyxVQUFVLEdBQUcsVUFBVSxDQUFDLFNBQVMsQ0FBQztRQUN2QyxJQUFJLENBQUMsU0FBUyxHQUFHLFVBQVUsQ0FBQyxTQUFTLElBQUksaUJBQWlCLENBQUM7SUFDN0QsQ0FBQztJQUVELG1FQUFtRTtJQUM1RCxTQUFTLENBQUMsU0FBb0M7UUFDbkQsTUFBTSxPQUFPLEdBQUcsTUFBTSxDQUFDLFNBQVMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUM7UUFDbEQsT0FBTyxFQUFFLE1BQU0sRUFBRSxPQUFPLENBQUMsR0FBRyxDQUFDLFFBQVEsQ0FBQyxFQUFFLE1BQU0sRUFBRSxPQUFPLENBQUMsR0FBRyxDQUFDLFFBQVEsQ0FBQyxFQUFFLENBQUM7SUFDMUUsQ0FBQztJQUVEOzs7O09BSUc7SUFDSSxLQUFLLENBQUMsMEJBQTBCLENBQ3JDLFVBQW1CLEVBQ25C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
|
|
@@ -11,13 +11,23 @@ export interface IRelyingPartySession {
|
|
|
11
11
|
idpSubject?: string;
|
|
12
12
|
loginAttemptId?: string;
|
|
13
13
|
}
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* A live session with the principal it acts as. `principal` is what
|
|
16
|
+
* `principals.resolve` answered for it in the same read; with `isActive`
|
|
17
|
+
* principals it is `undefined`.
|
|
18
|
+
*/
|
|
19
|
+
export interface IRelyingPartyLiveSession<TPrincipal extends object | undefined = undefined> {
|
|
16
20
|
session: IRelyingPartySession;
|
|
21
|
+
principal: TPrincipal;
|
|
22
|
+
}
|
|
23
|
+
/** A live session behind a cookie, with its principal and the CSRF token derived for it. */
|
|
24
|
+
export interface IResolvedRelyingPartySession<TPrincipal extends object | undefined = undefined> extends IRelyingPartyLiveSession<TPrincipal> {
|
|
17
25
|
csrfToken: string;
|
|
18
26
|
}
|
|
19
27
|
/** A session just opened: the answer, the CSRF token and the `Set-Cookie` value that carries it. */
|
|
20
|
-
export interface IRelyingPartySessionOpening
|
|
28
|
+
export interface IRelyingPartySessionOpening {
|
|
29
|
+
session: IRelyingPartySession;
|
|
30
|
+
csrfToken: string;
|
|
21
31
|
sessionCookie: string;
|
|
22
32
|
}
|
|
23
33
|
/** The redirect that starts a provider login, and the cookie that binds it to this browser. */
|
|
@@ -54,6 +64,25 @@ export interface IRelyingPartyPrincipals {
|
|
|
54
64
|
/** Whether a principal may act right now; a session of an inactive principal resolves to nothing. */
|
|
55
65
|
isActive(principalIdArg: string): Promise<boolean>;
|
|
56
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Principals that answer the principal itself instead of a yes or no, so a
|
|
69
|
+
* request reads its user once: `resolve` answers the principal when it may act
|
|
70
|
+
* right now and `null` otherwise, and every live session carries the answer as
|
|
71
|
+
* `principal`. Give either `isActive` or `resolve`, never both.
|
|
72
|
+
*/
|
|
73
|
+
export interface IRelyingPartyPrincipalResolver<TPrincipal extends object | undefined> {
|
|
74
|
+
admit(completionArg: IOidcCompletion): Promise<{
|
|
75
|
+
principalId: string;
|
|
76
|
+
}>;
|
|
77
|
+
/** The principal object when it may act right now, `null` otherwise; any answer that is not an object (including `false`) resolves the session to nothing. */
|
|
78
|
+
resolve(principalIdArg: string): Promise<TPrincipal | null>;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* What `RelyingPartySessions` accepts as its principals: a resolver whose
|
|
82
|
+
* principal type every live session carries, or — only while that type is
|
|
83
|
+
* left `undefined` — the `isActive` form.
|
|
84
|
+
*/
|
|
85
|
+
export type TRelyingPartyPrincipals<TPrincipal extends object | undefined> = IRelyingPartyPrincipalResolver<TPrincipal> | (undefined extends TPrincipal ? IRelyingPartyPrincipals : never);
|
|
57
86
|
export type TRelyingPartySessionEndReason =
|
|
58
87
|
/** the application or the user ended it */
|
|
59
88
|
'logout'
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -561,19 +561,76 @@ A deployment without a provider (`provider` omitted) keeps the same session
|
|
|
561
561
|
store for sessions it opens itself with `openSession({ principalId })`, for
|
|
562
562
|
example after a local password check.
|
|
563
563
|
|
|
564
|
+
**Building before the database connects.** `db` may be a function that answers
|
|
565
|
+
the database once it exists. It is called on every use of a model, the way
|
|
566
|
+
`@Collection(() => db)` resolves it, and never while the models,
|
|
567
|
+
`RelyingPartySessions`, `RelyingPartyRoutes` or `RelyingPartySocketBinding` are
|
|
568
|
+
constructed — none of them does I/O in its constructor. A use before the
|
|
569
|
+
function answers a `SmartdataDb` (or while it throws) rejects with
|
|
570
|
+
`RelyingPartyDatabaseUnavailableError`, whose `cause` is the function's error:
|
|
571
|
+
|
|
572
|
+
```ts
|
|
573
|
+
const models = createRelyingPartyModels({ db: () => appDatabase.db });
|
|
574
|
+
const sessions = new RelyingPartySessions({ models, /* … */ });
|
|
575
|
+
// later, at startup
|
|
576
|
+
await appDatabase.init();
|
|
577
|
+
await models.Session.ensureInitialized();
|
|
578
|
+
await models.LoginAttempt.ensureInitialized();
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
**Reading the principal once.** `principals.isActive` answers a yes or no, so a
|
|
582
|
+
request that needs the user reads it a second time. Principals may give
|
|
583
|
+
`resolve` instead: it answers the principal when it may act right now and
|
|
584
|
+
`null` otherwise, and every live session carries that answer as `principal` —
|
|
585
|
+
on `resolveCookieSession`, `revalidateLiveSession`, `RelyingPartySocketBinding.revalidate`
|
|
586
|
+
and the routes' `sessionAnswer`. The principal type is inferred from `resolve`;
|
|
587
|
+
with `isActive` it is `undefined`. Give exactly one of the two; the constructor
|
|
588
|
+
refuses both or neither.
|
|
589
|
+
|
|
590
|
+
```ts
|
|
591
|
+
const sessions = new RelyingPartySessions({
|
|
592
|
+
// …
|
|
593
|
+
principals: {
|
|
594
|
+
admit: async (completion) => ({ principalId: await users.upsertFromIdp(completion) }),
|
|
595
|
+
resolve: async (principalId) => {
|
|
596
|
+
const user = await users.get(principalId);
|
|
597
|
+
return user?.status === 'active' ? user : null;
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
const resolved = await sessions.resolveCookieSession(cookieHeader);
|
|
602
|
+
resolved?.principal.displayName; // typed as the user
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
**Recording a logout.** `RelyingPartyRoutes` takes
|
|
606
|
+
`onLogout({ session, endedLiveSession })`. It runs only for a `POST
|
|
607
|
+
<prefix>logout` that passed the origin and CSRF checks, after the session was
|
|
608
|
+
ended, and the answer waits for it, so an audit record is written before the
|
|
609
|
+
browser learns the logout is done. `endedLiveSession` is `true` only when this
|
|
610
|
+
call ended a live session: a logout that lost a race to another end gets
|
|
611
|
+
`false`, and a repeated logout finds no session and does not call it. A hook
|
|
612
|
+
that throws or rejects never keeps the session alive and never changes the
|
|
613
|
+
answer — the session is already over — and is reported as a `hook_failed`
|
|
614
|
+
route event (`hook: 'onLogout'`) through `onRouteEvent`. The hook runs without
|
|
615
|
+
a deadline of its own; bound a slow audit store inside it.
|
|
616
|
+
|
|
617
|
+
**Socket peers.** `RelyingPartySocketBinding.tagConnection` and `upgradeOf` take
|
|
618
|
+
the connection wrapper or the bare SmartServe peer (`TRelyingPartySocketPeer`),
|
|
619
|
+
the two forms `TypedSocket.setServerTag` accepts.
|
|
620
|
+
|
|
564
621
|
### Relying-Party API Surface
|
|
565
622
|
|
|
566
623
|
| API | Description |
|
|
567
624
|
| --- | --- |
|
|
568
625
|
| `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
|
|
626
|
+
| `createRelyingPartyModels()` | Binds the session and login-attempt models to one `SmartdataDb`, or to a function asked for it on every use, optionally under existing collection names. |
|
|
627
|
+
| `RelyingPartySessions<TPrincipal>` | Login start/complete/abandon/finalize, `openSession`, `resolveCookieSession`, `revalidateSession`, `revalidateLiveSession`, `isSessionLive`, `validateCsrf`, `logout`/`endSession`, `endSessionsOfPrincipal`, `liveProviderToken`. Principals give `isActive` or `resolve` (`IRelyingPartyPrincipals`, `IRelyingPartyPrincipalResolver<TPrincipal>`); with `resolve`, live sessions carry `principal`. |
|
|
571
628
|
| `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. |
|
|
629
|
+
| `RelyingPartyRoutes` | `handle(ctx)` for `/auth/login`, `/auth/callback`, `/auth/finalize`, `/auth/session` and `/auth/logout`; `null` for anything else. Takes a typedserver route context. `onLogout` is awaited after a CSRF-checked logout ended the session. |
|
|
573
630
|
| `createActorScope()` | AsyncLocalStorage actor context with a TypedRouter request scope. |
|
|
574
|
-
| `RelyingPartySocketBinding` | TypedSocket browser registration, per-request re-validation and session tags. |
|
|
631
|
+
| `RelyingPartySocketBinding` | TypedSocket browser registration, per-request re-validation and session tags; takes a connection wrapper or its peer. |
|
|
575
632
|
| `SecretBox`, `deriveCsrfToken()`, `decodeSecretKey()`, `parseCookies()`, `serializeCookie()`, `sanitizeReturnPath()` | The primitives underneath. |
|
|
576
|
-
| `RelyingPartyError`, `RelyingPartyRefusal`, `LoginCallbackError`, `AuthorityTokenError`, `AuthorityRefusedError` | Errors with stable codes; none carries token material. |
|
|
633
|
+
| `RelyingPartyError`, `RelyingPartyRefusal`, `LoginCallbackError`, `AuthorityTokenError`, `AuthorityRefusedError`, `RelyingPartyDatabaseUnavailableError` | Errors with stable codes; none carries token material. |
|
|
577
634
|
|
|
578
635
|
## Runtime Notes
|
|
579
636
|
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -67,3 +67,11 @@ export class AuthorityTokenError extends Error {
|
|
|
67
67
|
this.status = code === 'authority_login_required' ? 403 : 503;
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
+
|
|
71
|
+
/** A relying-party model was used before the database it is bound to exists. */
|
|
72
|
+
export class RelyingPartyDatabaseUnavailableError extends Error {
|
|
73
|
+
constructor(causeArg?: unknown) {
|
|
74
|
+
super('The relying-party models were used before their database exists', { cause: causeArg });
|
|
75
|
+
this.name = 'RelyingPartyDatabaseUnavailableError';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import { LoginCallbackError, RelyingPartyError, RelyingPartyRefusal } from './classes.errors.js';
|
|
3
3
|
import type { RelyingPartySessions } from './classes.relyingpartysessions.js';
|
|
4
|
-
import type { IResolvedRelyingPartySession } from './interfaces.js';
|
|
4
|
+
import type { IRelyingPartySession, IResolvedRelyingPartySession } from './interfaces.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The part of an HTTP request context the routes read. `IRequestContext` of
|
|
@@ -16,8 +16,8 @@ export interface IRelyingPartyHttpContext {
|
|
|
16
16
|
readonly request: Request;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export interface IRelyingPartyRoutesOptions {
|
|
20
|
-
sessions: RelyingPartySessions
|
|
19
|
+
export interface IRelyingPartyRoutesOptions<TPrincipal extends object | undefined = undefined> {
|
|
20
|
+
sessions: RelyingPartySessions<TPrincipal>;
|
|
21
21
|
/** where a sign-in that did not finish sends the browser; no provider text is ever echoed into it */
|
|
22
22
|
failureLocation: string;
|
|
23
23
|
/**
|
|
@@ -30,7 +30,17 @@ export interface IRelyingPartyRoutesOptions {
|
|
|
30
30
|
* The body `GET <prefix>session` answers with. Default:
|
|
31
31
|
* `{ session: { sessionId, principalId, expiresAt, csrfToken } | null }`.
|
|
32
32
|
*/
|
|
33
|
-
sessionAnswer?(resolvedArg: IResolvedRelyingPartySession | null): Promise<object>;
|
|
33
|
+
sessionAnswer?(resolvedArg: IResolvedRelyingPartySession<TPrincipal> | null): Promise<object>;
|
|
34
|
+
/**
|
|
35
|
+
* A `POST <prefix>logout` passed the origin and CSRF checks and the session
|
|
36
|
+
* was ended. Awaited before the answer, so an audit record is written
|
|
37
|
+
* before the browser learns the logout is done. `endedLiveSession` is `true`
|
|
38
|
+
* only when this call ended a live session; a logout that lost a race to
|
|
39
|
+
* another end answers `false` and records nothing. The session is already
|
|
40
|
+
* over when this runs: a throw (or a rejection) never keeps it alive and
|
|
41
|
+
* never changes the answer — it is reported as a `hook_failed` route event.
|
|
42
|
+
*/
|
|
43
|
+
onLogout?(eventArg: { session: IRelyingPartySession; endedLiveSession: boolean }): Promise<void>;
|
|
34
44
|
/** the largest logout body read, in bytes; a longer one is cancelled as soon as it passes the limit */
|
|
35
45
|
maxBodyBytes?: number;
|
|
36
46
|
/**
|
|
@@ -39,7 +49,15 @@ export interface IRelyingPartyRoutesOptions {
|
|
|
39
49
|
* event and its failure go to `console.error` instead.
|
|
40
50
|
*/
|
|
41
51
|
onRouteEvent?(eventArg: {
|
|
42
|
-
event:
|
|
52
|
+
event:
|
|
53
|
+
| 'auth_login_start_failed'
|
|
54
|
+
| 'auth_callback_failed'
|
|
55
|
+
| 'auth_callback_declined'
|
|
56
|
+
| 'auth_callback_refused'
|
|
57
|
+
/** an application hook of the routes threw; the step it belonged to is complete */
|
|
58
|
+
| 'hook_failed';
|
|
59
|
+
hook?: 'onLogout';
|
|
60
|
+
sessionId?: string;
|
|
43
61
|
stage?: string;
|
|
44
62
|
code?: string;
|
|
45
63
|
correlationId?: string;
|
|
@@ -78,18 +96,19 @@ const redirect = (locationArg: string, headersArg?: Record<string, string>): Res
|
|
|
78
96
|
* - `GET finalize` releases the return path only to the browser holding the
|
|
79
97
|
* session this attempt created
|
|
80
98
|
* - `GET session` answers the current session
|
|
81
|
-
* - `POST logout` ends the session; it needs this origin and the CSRF token
|
|
99
|
+
* - `POST logout` ends the session; it needs this origin and the CSRF token,
|
|
100
|
+
* and `onLogout` runs before it answers
|
|
82
101
|
*
|
|
83
102
|
* `handle()` answers `null` for every other request, so the application
|
|
84
103
|
* chains its own routes (a local password login) around it. Cookies are only
|
|
85
104
|
* ever set here: a `/typedrequest` answer cannot carry `Set-Cookie`.
|
|
86
105
|
*/
|
|
87
|
-
export class RelyingPartyRoutes {
|
|
88
|
-
readonly #sessions: RelyingPartySessions
|
|
89
|
-
readonly #options: IRelyingPartyRoutesOptions
|
|
106
|
+
export class RelyingPartyRoutes<TPrincipal extends object | undefined = undefined> {
|
|
107
|
+
readonly #sessions: RelyingPartySessions<TPrincipal>;
|
|
108
|
+
readonly #options: IRelyingPartyRoutesOptions<TPrincipal>;
|
|
90
109
|
readonly #maxBodyBytes: number;
|
|
91
110
|
|
|
92
|
-
constructor(optionsArg: IRelyingPartyRoutesOptions) {
|
|
111
|
+
constructor(optionsArg: IRelyingPartyRoutesOptions<TPrincipal>) {
|
|
93
112
|
this.#sessions = optionsArg.sessions;
|
|
94
113
|
this.#options = optionsArg;
|
|
95
114
|
this.#maxBodyBytes = optionsArg.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
@@ -200,10 +219,24 @@ export class RelyingPartyRoutes {
|
|
|
200
219
|
if (!this.#sessions.validateCsrf(resolved, csrfToken)) {
|
|
201
220
|
return jsonResponse({ error: 'forbidden' }, 403);
|
|
202
221
|
}
|
|
203
|
-
|
|
222
|
+
// the session ends first: whatever the hook does, it cannot keep it alive
|
|
223
|
+
const endedLiveSession = await this.#sessions.logout(resolved.session.id);
|
|
224
|
+
if (this.#options.onLogout) {
|
|
225
|
+
try {
|
|
226
|
+
await this.#options.onLogout({ session: resolved.session, endedLiveSession });
|
|
227
|
+
} catch (error) {
|
|
228
|
+
this.emit({
|
|
229
|
+
event: 'hook_failed',
|
|
230
|
+
hook: 'onLogout',
|
|
231
|
+
sessionId: resolved.session.id,
|
|
232
|
+
message: error instanceof Error ? error.message : String(error),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return jsonResponse({ endedLiveSession }, 200, clearSession);
|
|
204
237
|
}
|
|
205
238
|
|
|
206
|
-
private async sessionAnswer(resolvedArg: IResolvedRelyingPartySession | null): Promise<object> {
|
|
239
|
+
private async sessionAnswer(resolvedArg: IResolvedRelyingPartySession<TPrincipal> | null): Promise<object> {
|
|
207
240
|
if (this.#options.sessionAnswer) {
|
|
208
241
|
return this.#options.sessionAnswer(resolvedArg);
|
|
209
242
|
}
|
|
@@ -261,7 +294,7 @@ export class RelyingPartyRoutes {
|
|
|
261
294
|
}
|
|
262
295
|
}
|
|
263
296
|
|
|
264
|
-
private emit(eventArg: Parameters<NonNullable<IRelyingPartyRoutesOptions['onRouteEvent']>>[0]): void {
|
|
297
|
+
private emit(eventArg: Parameters<NonNullable<IRelyingPartyRoutesOptions<TPrincipal>['onRouteEvent']>>[0]): void {
|
|
265
298
|
if (!this.#options.onRouteEvent) {
|
|
266
299
|
console.error(JSON.stringify(eventArg));
|
|
267
300
|
return;
|