@draftlab/auth 0.10.3 → 0.11.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/dist/client.d.mts +1 -0
- package/dist/client.mjs +1 -1
- package/dist/core.d.mts +0 -3
- package/dist/core.mjs +8 -36
- package/dist/keys.mjs +86 -78
- package/dist/mutex.d.mts +44 -0
- package/dist/mutex.mjs +110 -0
- package/package.json +4 -4
- package/dist/plugin/builder.d.mts +0 -27
- package/dist/plugin/builder.mjs +0 -93
- package/dist/plugin/manager.d.mts +0 -62
- package/dist/plugin/manager.mjs +0 -160
- package/dist/plugin/plugin.d.mts +0 -42
- package/dist/plugin/plugin.mjs +0 -1
- package/dist/plugin/types.d.mts +0 -99
- package/dist/plugin/types.mjs +0 -13
package/dist/client.d.mts
CHANGED
package/dist/client.mjs
CHANGED
|
@@ -203,7 +203,7 @@ const createClient = (input) => {
|
|
|
203
203
|
try {
|
|
204
204
|
const jwtResult = await jwtVerify(token, await getJWKS(), {
|
|
205
205
|
issuer: options?.issuer ?? issuer,
|
|
206
|
-
|
|
206
|
+
audience: options?.audience ?? input.clientID
|
|
207
207
|
});
|
|
208
208
|
const validated = await subjects[jwtResult.payload.type]?.["~standard"].validate(jwtResult.payload.properties);
|
|
209
209
|
if (!validated?.issues && jwtResult.payload.mode === "access") return {
|
package/dist/core.d.mts
CHANGED
|
@@ -3,7 +3,6 @@ import { UnknownStateError } from "./error.mjs";
|
|
|
3
3
|
import { Prettify } from "./util.mjs";
|
|
4
4
|
import { SubjectPayload, SubjectSchema } from "./subject.mjs";
|
|
5
5
|
import { StorageAdapter } from "./storage/storage.mjs";
|
|
6
|
-
import { Plugin } from "./plugin/types.mjs";
|
|
7
6
|
import { Provider } from "./provider/provider.mjs";
|
|
8
7
|
import { Theme } from "./themes/theme.mjs";
|
|
9
8
|
import { AuthorizationState } from "./types.mjs";
|
|
@@ -61,8 +60,6 @@ interface IssuerInput<Providers extends Record<string, Provider<unknown>>, Subje
|
|
|
61
60
|
error?(error: UnknownStateError, req: Request): Promise<Response>;
|
|
62
61
|
/** Client authorization check function */
|
|
63
62
|
allow?(input: AllowCheckInput, req: Request): Promise<boolean>;
|
|
64
|
-
/** Plugin configuration */
|
|
65
|
-
plugins?: Plugin[];
|
|
66
63
|
/**
|
|
67
64
|
* Refresh callback for updating user claims.
|
|
68
65
|
*
|
package/dist/core.mjs
CHANGED
|
@@ -5,7 +5,6 @@ import { validatePKCE } from "./pkce.mjs";
|
|
|
5
5
|
import { generateSecureToken } from "./random.mjs";
|
|
6
6
|
import { Storage } from "./storage/storage.mjs";
|
|
7
7
|
import { encryptionKeys, signingKeys } from "./keys.mjs";
|
|
8
|
-
import { PluginManager } from "./plugin/manager.mjs";
|
|
9
8
|
import { Revocation } from "./revocation.mjs";
|
|
10
9
|
import { setTheme } from "./themes/theme.mjs";
|
|
11
10
|
import { Select } from "./ui/select.mjs";
|
|
@@ -188,15 +187,6 @@ const issuer = (input) => {
|
|
|
188
187
|
const authorization = await getAuthorization(ctx);
|
|
189
188
|
const currentProvider = ctx.get("provider") || "unknown";
|
|
190
189
|
if (!authorization.client_id) throw new Error("client_id is required");
|
|
191
|
-
if (manager) try {
|
|
192
|
-
const subjectProperties = properties && typeof properties === "object" ? properties : {};
|
|
193
|
-
await manager.executeSuccessHooks(authorization.client_id, currentProvider, {
|
|
194
|
-
type: currentProvider,
|
|
195
|
-
properties: subjectProperties
|
|
196
|
-
});
|
|
197
|
-
} catch (error$1) {
|
|
198
|
-
console.error("Plugin success hook failed:", error$1);
|
|
199
|
-
}
|
|
200
190
|
return await input.success({ async subject(type, properties$1, subjectOpts) {
|
|
201
191
|
const subject = subjectOpts?.subject ?? await resolveSubject(type, properties$1);
|
|
202
192
|
await successOpts?.invalidate?.(await resolveSubject(type, properties$1));
|
|
@@ -287,22 +277,6 @@ const issuer = (input) => {
|
|
|
287
277
|
storage
|
|
288
278
|
};
|
|
289
279
|
const app = new Router({ basePath: input.basePath });
|
|
290
|
-
const manager = input.plugins && input.plugins.length > 0 ? new PluginManager(input.storage) : null;
|
|
291
|
-
let pluginsInitialized = false;
|
|
292
|
-
if (manager && input.plugins) {
|
|
293
|
-
manager.registerAll(input.plugins);
|
|
294
|
-
manager.setupRoutes(app);
|
|
295
|
-
app.use(async (c, next) => {
|
|
296
|
-
if (!pluginsInitialized) try {
|
|
297
|
-
await manager.initialize();
|
|
298
|
-
pluginsInitialized = true;
|
|
299
|
-
} catch (error$1) {
|
|
300
|
-
console.error("Plugin initialization failed:", error$1);
|
|
301
|
-
return c.newResponse("Plugin initialization failed", { status: 500 });
|
|
302
|
-
}
|
|
303
|
-
return await next();
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
280
|
for (const [name, value] of Object.entries(input.providers)) {
|
|
307
281
|
const route = new Router();
|
|
308
282
|
route.use(async (c, next) => {
|
|
@@ -396,6 +370,7 @@ const issuer = (input) => {
|
|
|
396
370
|
await Storage.remove(storage, key);
|
|
397
371
|
const response = {
|
|
398
372
|
access_token: tokens.access,
|
|
373
|
+
token_type: "Bearer",
|
|
399
374
|
expires_in: tokens.expiresIn,
|
|
400
375
|
refresh_token: tokens.refresh
|
|
401
376
|
};
|
|
@@ -475,6 +450,7 @@ const issuer = (input) => {
|
|
|
475
450
|
}, { generateRefreshToken });
|
|
476
451
|
const response = {
|
|
477
452
|
access_token: tokens.access,
|
|
453
|
+
token_type: "Bearer",
|
|
478
454
|
refresh_token: tokens.refresh,
|
|
479
455
|
expires_in: tokens.expiresIn
|
|
480
456
|
};
|
|
@@ -574,6 +550,12 @@ const issuer = (input) => {
|
|
|
574
550
|
};
|
|
575
551
|
c.set("authorization", authorization);
|
|
576
552
|
if (!redirect_uri) return c.text("Missing redirect_uri", { status: 400 });
|
|
553
|
+
try {
|
|
554
|
+
const uri = new URL(redirect_uri);
|
|
555
|
+
if (!uri.protocol || !uri.host) return c.text("Invalid redirect_uri format", { status: 400 });
|
|
556
|
+
} catch {
|
|
557
|
+
return c.text("Invalid redirect_uri format", { status: 400 });
|
|
558
|
+
}
|
|
577
559
|
if (!response_type) throw new MissingParameterError("response_type");
|
|
578
560
|
if (!client_id) throw new MissingParameterError("client_id");
|
|
579
561
|
if (input.start) await input.start(c.request);
|
|
@@ -582,10 +564,6 @@ const issuer = (input) => {
|
|
|
582
564
|
redirectURI: redirect_uri,
|
|
583
565
|
audience
|
|
584
566
|
}, c.request)) throw new UnauthorizedClientError(client_id, redirect_uri);
|
|
585
|
-
if (manager) {
|
|
586
|
-
const scopes = scope ? scope.split(" ") : void 0;
|
|
587
|
-
await manager.executeAuthorizeHooks(client_id, provider, scopes);
|
|
588
|
-
}
|
|
589
567
|
await auth.set(c, "authorization", 900, authorization);
|
|
590
568
|
if (provider) return c.redirect(`${provider}/authorize`);
|
|
591
569
|
const availableProviders = Object.keys(input.providers);
|
|
@@ -593,12 +571,6 @@ const issuer = (input) => {
|
|
|
593
571
|
return auth.forward(c, await select()(Object.fromEntries(Object.entries(input.providers).map(([key, value]) => [key, value.type])), c.request));
|
|
594
572
|
});
|
|
595
573
|
app.onError(async (err, c) => {
|
|
596
|
-
if (manager) try {
|
|
597
|
-
const errorObj = err instanceof Error ? err : new Error(String(err));
|
|
598
|
-
await manager.executeErrorHooks(errorObj);
|
|
599
|
-
} catch (hookError) {
|
|
600
|
-
console.error("Plugin error hook failed:", hookError);
|
|
601
|
-
}
|
|
602
574
|
if (err instanceof UnknownStateError) return auth.forward(c, await error(err, c.request));
|
|
603
575
|
try {
|
|
604
576
|
const authorization = await getAuthorization(c);
|
package/dist/keys.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Mutex } from "./mutex.mjs";
|
|
1
2
|
import { generateSecureToken } from "./random.mjs";
|
|
2
3
|
import { Storage } from "./storage/storage.mjs";
|
|
3
4
|
import { exportJWK, exportPKCS8, exportSPKI, generateKeyPair, importPKCS8, importSPKI } from "jose";
|
|
@@ -11,6 +12,9 @@ import { exportJWK, exportPKCS8, exportSPKI, generateKeyPair, importPKCS8, impor
|
|
|
11
12
|
const signingAlg = "ES256";
|
|
12
13
|
/** RSA algorithm used for token encryption operations */
|
|
13
14
|
const encryptionAlg = "RSA-OAEP-512";
|
|
15
|
+
/** Mutex to prevent concurrent key generation (race condition with eventually consistent storage) */
|
|
16
|
+
const signingKeyMutex = new Mutex();
|
|
17
|
+
const encryptionKeyMutex = new Mutex();
|
|
14
18
|
/**
|
|
15
19
|
* Loads or generates signing keys for JWT operations.
|
|
16
20
|
* Returns existing valid keys, or generates new ones if none are available.
|
|
@@ -31,47 +35,49 @@ const encryptionAlg = "RSA-OAEP-512";
|
|
|
31
35
|
* ```
|
|
32
36
|
*/
|
|
33
37
|
const signingKeys = async (storage) => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
return signingKeyMutex.runExclusive(async () => {
|
|
39
|
+
const results = [];
|
|
40
|
+
const scanner = Storage.scan(storage, ["signing:key"]);
|
|
41
|
+
for await (const [, value] of scanner) try {
|
|
42
|
+
const publicKey = await importSPKI(value.publicKey, value.alg, { extractable: true });
|
|
43
|
+
const privateKey = await importPKCS8(value.privateKey, value.alg);
|
|
44
|
+
const jwk$1 = await exportJWK(publicKey);
|
|
45
|
+
jwk$1.kid = value.id;
|
|
46
|
+
jwk$1.use = "sig";
|
|
47
|
+
results.push({
|
|
48
|
+
id: value.id,
|
|
49
|
+
alg: signingAlg,
|
|
50
|
+
created: new Date(value.created),
|
|
51
|
+
expired: value.expired ? new Date(value.expired) : void 0,
|
|
52
|
+
public: publicKey,
|
|
53
|
+
private: privateKey,
|
|
54
|
+
jwk: jwk$1
|
|
55
|
+
});
|
|
56
|
+
} catch {}
|
|
57
|
+
results.sort((a, b) => b.created.getTime() - a.created.getTime());
|
|
58
|
+
if (results.filter((item) => !item.expired).length) return results;
|
|
59
|
+
const key = await generateKeyPair(signingAlg, { extractable: true });
|
|
60
|
+
const serialized = {
|
|
61
|
+
id: generateSecureToken(16),
|
|
62
|
+
publicKey: await exportSPKI(key.publicKey),
|
|
63
|
+
privateKey: await exportPKCS8(key.privateKey),
|
|
64
|
+
created: Date.now(),
|
|
65
|
+
alg: signingAlg
|
|
66
|
+
};
|
|
67
|
+
await Storage.set(storage, ["signing:key", serialized.id], serialized);
|
|
68
|
+
const jwk = await exportJWK(key.publicKey);
|
|
69
|
+
jwk.kid = serialized.id;
|
|
70
|
+
jwk.use = "sig";
|
|
71
|
+
return [{
|
|
72
|
+
id: serialized.id,
|
|
44
73
|
alg: signingAlg,
|
|
45
|
-
created: new Date(
|
|
46
|
-
expired:
|
|
47
|
-
public: publicKey,
|
|
48
|
-
private: privateKey,
|
|
49
|
-
jwk
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
results.sort((a, b) => b.created.getTime() - a.created.getTime());
|
|
53
|
-
if (results.filter((item) => !item.expired).length) return results;
|
|
54
|
-
const key = await generateKeyPair(signingAlg, { extractable: true });
|
|
55
|
-
const serialized = {
|
|
56
|
-
id: generateSecureToken(16),
|
|
57
|
-
publicKey: await exportSPKI(key.publicKey),
|
|
58
|
-
privateKey: await exportPKCS8(key.privateKey),
|
|
59
|
-
created: Date.now(),
|
|
60
|
-
alg: signingAlg
|
|
61
|
-
};
|
|
62
|
-
await Storage.set(storage, ["signing:key", serialized.id], serialized);
|
|
63
|
-
const jwk = await exportJWK(key.publicKey);
|
|
64
|
-
jwk.kid = serialized.id;
|
|
65
|
-
jwk.use = "sig";
|
|
66
|
-
return [{
|
|
67
|
-
id: serialized.id,
|
|
68
|
-
alg: signingAlg,
|
|
69
|
-
created: new Date(serialized.created),
|
|
70
|
-
expired: serialized.expired ? new Date(serialized.expired) : void 0,
|
|
71
|
-
public: key.publicKey,
|
|
72
|
-
private: key.privateKey,
|
|
73
|
-
jwk
|
|
74
|
-
}, ...results];
|
|
74
|
+
created: new Date(serialized.created),
|
|
75
|
+
expired: serialized.expired ? new Date(serialized.expired) : void 0,
|
|
76
|
+
public: key.publicKey,
|
|
77
|
+
private: key.privateKey,
|
|
78
|
+
jwk
|
|
79
|
+
}, ...results];
|
|
80
|
+
});
|
|
75
81
|
};
|
|
76
82
|
/**
|
|
77
83
|
* Loads or generates encryption keys for token encryption operations.
|
|
@@ -93,45 +99,47 @@ const signingKeys = async (storage) => {
|
|
|
93
99
|
* ```
|
|
94
100
|
*/
|
|
95
101
|
const encryptionKeys = async (storage) => {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
return encryptionKeyMutex.runExclusive(async () => {
|
|
103
|
+
const results = [];
|
|
104
|
+
const scanner = Storage.scan(storage, ["encryption:key"]);
|
|
105
|
+
for await (const [, value] of scanner) try {
|
|
106
|
+
const publicKey = await importSPKI(value.publicKey, value.alg, { extractable: true });
|
|
107
|
+
const privateKey = await importPKCS8(value.privateKey, value.alg);
|
|
108
|
+
const jwk$1 = await exportJWK(publicKey);
|
|
109
|
+
jwk$1.kid = value.id;
|
|
110
|
+
results.push({
|
|
111
|
+
id: value.id,
|
|
112
|
+
alg: encryptionAlg,
|
|
113
|
+
created: new Date(value.created),
|
|
114
|
+
expired: value.expired ? new Date(value.expired) : void 0,
|
|
115
|
+
public: publicKey,
|
|
116
|
+
private: privateKey,
|
|
117
|
+
jwk: jwk$1
|
|
118
|
+
});
|
|
119
|
+
} catch {}
|
|
120
|
+
results.sort((a, b) => b.created.getTime() - a.created.getTime());
|
|
121
|
+
if (results.filter((item) => !item.expired).length) return results;
|
|
122
|
+
const key = await generateKeyPair(encryptionAlg, { extractable: true });
|
|
123
|
+
const serialized = {
|
|
124
|
+
id: generateSecureToken(16),
|
|
125
|
+
publicKey: await exportSPKI(key.publicKey),
|
|
126
|
+
privateKey: await exportPKCS8(key.privateKey),
|
|
127
|
+
created: Date.now(),
|
|
128
|
+
alg: encryptionAlg
|
|
129
|
+
};
|
|
130
|
+
await Storage.set(storage, ["encryption:key", serialized.id], serialized);
|
|
131
|
+
const jwk = await exportJWK(key.publicKey);
|
|
132
|
+
jwk.kid = serialized.id;
|
|
133
|
+
return [{
|
|
134
|
+
id: serialized.id,
|
|
105
135
|
alg: encryptionAlg,
|
|
106
|
-
created: new Date(
|
|
107
|
-
expired:
|
|
108
|
-
public: publicKey,
|
|
109
|
-
private: privateKey,
|
|
110
|
-
jwk
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
results.sort((a, b) => b.created.getTime() - a.created.getTime());
|
|
114
|
-
if (results.filter((item) => !item.expired).length) return results;
|
|
115
|
-
const key = await generateKeyPair(encryptionAlg, { extractable: true });
|
|
116
|
-
const serialized = {
|
|
117
|
-
id: generateSecureToken(16),
|
|
118
|
-
publicKey: await exportSPKI(key.publicKey),
|
|
119
|
-
privateKey: await exportPKCS8(key.privateKey),
|
|
120
|
-
created: Date.now(),
|
|
121
|
-
alg: encryptionAlg
|
|
122
|
-
};
|
|
123
|
-
await Storage.set(storage, ["encryption:key", serialized.id], serialized);
|
|
124
|
-
const jwk = await exportJWK(key.publicKey);
|
|
125
|
-
jwk.kid = serialized.id;
|
|
126
|
-
return [{
|
|
127
|
-
id: serialized.id,
|
|
128
|
-
alg: encryptionAlg,
|
|
129
|
-
created: new Date(serialized.created),
|
|
130
|
-
expired: serialized.expired ? new Date(serialized.expired) : void 0,
|
|
131
|
-
public: key.publicKey,
|
|
132
|
-
private: key.privateKey,
|
|
133
|
-
jwk
|
|
134
|
-
}, ...results];
|
|
136
|
+
created: new Date(serialized.created),
|
|
137
|
+
expired: serialized.expired ? new Date(serialized.expired) : void 0,
|
|
138
|
+
public: key.publicKey,
|
|
139
|
+
private: key.privateKey,
|
|
140
|
+
jwk
|
|
141
|
+
}, ...results];
|
|
142
|
+
});
|
|
135
143
|
};
|
|
136
144
|
|
|
137
145
|
//#endregion
|
package/dist/mutex.d.mts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//#region src/mutex.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A Mutex (mutual exclusion lock) for async functions.
|
|
4
|
+
* It allows only one async task to access a critical section at a time.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const mutex = new Mutex();
|
|
8
|
+
*
|
|
9
|
+
* async function criticalSection() {
|
|
10
|
+
* await mutex.acquire();
|
|
11
|
+
* try {
|
|
12
|
+
* // This code section cannot be executed simultaneously
|
|
13
|
+
* } finally {
|
|
14
|
+
* mutex.release();
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
declare class Mutex {
|
|
19
|
+
private semaphore;
|
|
20
|
+
/**
|
|
21
|
+
* Checks if the mutex is currently locked.
|
|
22
|
+
* @returns True if the mutex is locked, false otherwise.
|
|
23
|
+
*/
|
|
24
|
+
get isLocked(): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Acquires the mutex, blocking if necessary until it is available.
|
|
27
|
+
* @returns A promise that resolves when the mutex is acquired.
|
|
28
|
+
*/
|
|
29
|
+
acquire(): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Releases the mutex, allowing another waiting task to proceed.
|
|
32
|
+
*/
|
|
33
|
+
release(): void;
|
|
34
|
+
/**
|
|
35
|
+
* Runs a function while holding the mutex lock.
|
|
36
|
+
* Automatically acquires before and releases after the function execution.
|
|
37
|
+
*
|
|
38
|
+
* @param fn - The function to execute while holding the lock
|
|
39
|
+
* @returns The result of the function
|
|
40
|
+
*/
|
|
41
|
+
runExclusive<T>(fn: () => Promise<T>): Promise<T>;
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
export { Mutex };
|
package/dist/mutex.mjs
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
//#region src/mutex.ts
|
|
2
|
+
/**
|
|
3
|
+
* A counting semaphore for async functions that manages available permits.
|
|
4
|
+
* Semaphores are mainly used to limit the number of concurrent async tasks.
|
|
5
|
+
*
|
|
6
|
+
* Each `acquire` operation takes a permit or waits until one is available.
|
|
7
|
+
* Each `release` operation adds a permit, potentially allowing a waiting task to proceed.
|
|
8
|
+
*
|
|
9
|
+
* The semaphore ensures fairness by maintaining a FIFO (First In, First Out) order for acquirers.
|
|
10
|
+
*/
|
|
11
|
+
var Semaphore = class {
|
|
12
|
+
/**
|
|
13
|
+
* The maximum number of concurrent operations allowed.
|
|
14
|
+
*/
|
|
15
|
+
capacity;
|
|
16
|
+
/**
|
|
17
|
+
* The number of available permits.
|
|
18
|
+
*/
|
|
19
|
+
available;
|
|
20
|
+
deferredTasks = [];
|
|
21
|
+
/**
|
|
22
|
+
* Creates an instance of Semaphore.
|
|
23
|
+
* @param capacity - The maximum number of concurrent operations allowed.
|
|
24
|
+
*/
|
|
25
|
+
constructor(capacity) {
|
|
26
|
+
this.capacity = capacity;
|
|
27
|
+
this.available = capacity;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Acquires a semaphore, blocking if necessary until one is available.
|
|
31
|
+
* @returns A promise that resolves when the semaphore is acquired.
|
|
32
|
+
*/
|
|
33
|
+
async acquire() {
|
|
34
|
+
if (this.available > 0) {
|
|
35
|
+
this.available--;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
this.deferredTasks.push(resolve);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Releases a semaphore, allowing one more operation to proceed.
|
|
44
|
+
*/
|
|
45
|
+
release() {
|
|
46
|
+
const deferredTask = this.deferredTasks.shift();
|
|
47
|
+
if (deferredTask != null) {
|
|
48
|
+
deferredTask();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (this.available < this.capacity) this.available++;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* A Mutex (mutual exclusion lock) for async functions.
|
|
56
|
+
* It allows only one async task to access a critical section at a time.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* const mutex = new Mutex();
|
|
60
|
+
*
|
|
61
|
+
* async function criticalSection() {
|
|
62
|
+
* await mutex.acquire();
|
|
63
|
+
* try {
|
|
64
|
+
* // This code section cannot be executed simultaneously
|
|
65
|
+
* } finally {
|
|
66
|
+
* mutex.release();
|
|
67
|
+
* }
|
|
68
|
+
* }
|
|
69
|
+
*/
|
|
70
|
+
var Mutex = class {
|
|
71
|
+
semaphore = new Semaphore(1);
|
|
72
|
+
/**
|
|
73
|
+
* Checks if the mutex is currently locked.
|
|
74
|
+
* @returns True if the mutex is locked, false otherwise.
|
|
75
|
+
*/
|
|
76
|
+
get isLocked() {
|
|
77
|
+
return this.semaphore.available === 0;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Acquires the mutex, blocking if necessary until it is available.
|
|
81
|
+
* @returns A promise that resolves when the mutex is acquired.
|
|
82
|
+
*/
|
|
83
|
+
async acquire() {
|
|
84
|
+
return this.semaphore.acquire();
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Releases the mutex, allowing another waiting task to proceed.
|
|
88
|
+
*/
|
|
89
|
+
release() {
|
|
90
|
+
this.semaphore.release();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Runs a function while holding the mutex lock.
|
|
94
|
+
* Automatically acquires before and releases after the function execution.
|
|
95
|
+
*
|
|
96
|
+
* @param fn - The function to execute while holding the lock
|
|
97
|
+
* @returns The result of the function
|
|
98
|
+
*/
|
|
99
|
+
async runExclusive(fn) {
|
|
100
|
+
await this.acquire();
|
|
101
|
+
try {
|
|
102
|
+
return await fn();
|
|
103
|
+
} finally {
|
|
104
|
+
this.release();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
//#endregion
|
|
110
|
+
export { Mutex };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@draftlab/auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Core implementation for @draftlab/auth",
|
|
6
6
|
"author": "Matheus Pergoli",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^25.0.3",
|
|
41
41
|
"@types/qrcode": "^1.5.6",
|
|
42
|
-
"tsdown": "^0.18.
|
|
42
|
+
"tsdown": "^0.18.3",
|
|
43
43
|
"typescript": "^5.9.3",
|
|
44
44
|
"@draftlab/tsconfig": "0.1.0"
|
|
45
45
|
},
|
|
@@ -60,10 +60,10 @@
|
|
|
60
60
|
"@standard-schema/spec": "^1.1.0",
|
|
61
61
|
"jose": "^6.1.3",
|
|
62
62
|
"otpauth": "^9.4.1",
|
|
63
|
-
"preact": "^10.28.
|
|
63
|
+
"preact": "^10.28.1",
|
|
64
64
|
"preact-render-to-string": "^6.6.4",
|
|
65
65
|
"qrcode": "^1.5.4",
|
|
66
|
-
"@draftlab/auth-router": "0.
|
|
66
|
+
"@draftlab/auth-router": "0.5.0"
|
|
67
67
|
},
|
|
68
68
|
"engines": {
|
|
69
69
|
"node": ">=18"
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { PluginBuilder } from "./plugin.mjs";
|
|
2
|
-
|
|
3
|
-
//#region src/plugin/builder.d.ts
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Create a new plugin builder.
|
|
7
|
-
* Plugins are built using a fluent API that supports routes and lifecycle hooks.
|
|
8
|
-
*
|
|
9
|
-
* @param id - Unique identifier for the plugin
|
|
10
|
-
* @returns Plugin builder with chainable methods
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```ts
|
|
14
|
-
* const analytics = plugin("analytics")
|
|
15
|
-
* .onSuccess(async (ctx) => {
|
|
16
|
-
* await ctx.storage.set(`success:${ctx.clientID}`, ctx.subject)
|
|
17
|
-
* })
|
|
18
|
-
* .post("/stats", async (ctx) => {
|
|
19
|
-
* const stats = await ctx.pluginStorage.get("stats")
|
|
20
|
-
* return ctx.json(stats)
|
|
21
|
-
* })
|
|
22
|
-
* .build()
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
25
|
-
declare const plugin: (id: string) => PluginBuilder;
|
|
26
|
-
//#endregion
|
|
27
|
-
export { plugin };
|
package/dist/plugin/builder.mjs
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
//#region src/plugin/builder.ts
|
|
2
|
-
/**
|
|
3
|
-
* Create a new plugin builder.
|
|
4
|
-
* Plugins are built using a fluent API that supports routes and lifecycle hooks.
|
|
5
|
-
*
|
|
6
|
-
* @param id - Unique identifier for the plugin
|
|
7
|
-
* @returns Plugin builder with chainable methods
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* ```ts
|
|
11
|
-
* const analytics = plugin("analytics")
|
|
12
|
-
* .onSuccess(async (ctx) => {
|
|
13
|
-
* await ctx.storage.set(`success:${ctx.clientID}`, ctx.subject)
|
|
14
|
-
* })
|
|
15
|
-
* .post("/stats", async (ctx) => {
|
|
16
|
-
* const stats = await ctx.pluginStorage.get("stats")
|
|
17
|
-
* return ctx.json(stats)
|
|
18
|
-
* })
|
|
19
|
-
* .build()
|
|
20
|
-
* ```
|
|
21
|
-
*/
|
|
22
|
-
const plugin = (id) => {
|
|
23
|
-
if (!id || typeof id !== "string") throw new Error("Plugin id must be a non-empty string");
|
|
24
|
-
const routes = [];
|
|
25
|
-
const registeredPaths = /* @__PURE__ */ new Set();
|
|
26
|
-
let initHook;
|
|
27
|
-
let authorizeHook;
|
|
28
|
-
let successHook;
|
|
29
|
-
let errorHook;
|
|
30
|
-
const validatePath = (path) => {
|
|
31
|
-
if (!path || typeof path !== "string") throw new Error("Route path must be a non-empty string");
|
|
32
|
-
if (!path.startsWith("/")) throw new Error("Route path must start with '/'");
|
|
33
|
-
};
|
|
34
|
-
return {
|
|
35
|
-
get(path, handler) {
|
|
36
|
-
validatePath(path);
|
|
37
|
-
const routeKey = `GET ${path}`;
|
|
38
|
-
if (registeredPaths.has(routeKey)) throw new Error(`Route ${routeKey} already registered in plugin '${id}'`);
|
|
39
|
-
registeredPaths.add(routeKey);
|
|
40
|
-
routes.push({
|
|
41
|
-
method: "GET",
|
|
42
|
-
path,
|
|
43
|
-
handler
|
|
44
|
-
});
|
|
45
|
-
return this;
|
|
46
|
-
},
|
|
47
|
-
post(path, handler) {
|
|
48
|
-
validatePath(path);
|
|
49
|
-
const routeKey = `POST ${path}`;
|
|
50
|
-
if (registeredPaths.has(routeKey)) throw new Error(`Route ${routeKey} already registered in plugin '${id}'`);
|
|
51
|
-
registeredPaths.add(routeKey);
|
|
52
|
-
routes.push({
|
|
53
|
-
method: "POST",
|
|
54
|
-
path,
|
|
55
|
-
handler
|
|
56
|
-
});
|
|
57
|
-
return this;
|
|
58
|
-
},
|
|
59
|
-
onInit(handler) {
|
|
60
|
-
if (initHook) throw new Error(`onInit hook already defined for plugin '${id}'`);
|
|
61
|
-
initHook = handler;
|
|
62
|
-
return this;
|
|
63
|
-
},
|
|
64
|
-
onAuthorize(handler) {
|
|
65
|
-
if (authorizeHook) throw new Error(`onAuthorize hook already defined for plugin '${id}'`);
|
|
66
|
-
authorizeHook = handler;
|
|
67
|
-
return this;
|
|
68
|
-
},
|
|
69
|
-
onSuccess(handler) {
|
|
70
|
-
if (successHook) throw new Error(`onSuccess hook already defined for plugin '${id}'`);
|
|
71
|
-
successHook = handler;
|
|
72
|
-
return this;
|
|
73
|
-
},
|
|
74
|
-
onError(handler) {
|
|
75
|
-
if (errorHook) throw new Error(`onError hook already defined for plugin '${id}'`);
|
|
76
|
-
errorHook = handler;
|
|
77
|
-
return this;
|
|
78
|
-
},
|
|
79
|
-
build() {
|
|
80
|
-
return {
|
|
81
|
-
id,
|
|
82
|
-
routes: routes.length > 0 ? routes : void 0,
|
|
83
|
-
onInit: initHook,
|
|
84
|
-
onAuthorize: authorizeHook,
|
|
85
|
-
onSuccess: successHook,
|
|
86
|
-
onError: errorHook
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
//#endregion
|
|
93
|
-
export { plugin };
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { StorageAdapter } from "../storage/storage.mjs";
|
|
2
|
-
import { Plugin } from "./types.mjs";
|
|
3
|
-
import { Router } from "@draftlab/auth-router";
|
|
4
|
-
|
|
5
|
-
//#region src/plugin/manager.d.ts
|
|
6
|
-
|
|
7
|
-
declare class PluginManager {
|
|
8
|
-
private readonly plugins;
|
|
9
|
-
private readonly storage;
|
|
10
|
-
constructor(storage: StorageAdapter);
|
|
11
|
-
/**
|
|
12
|
-
* Register a plugin
|
|
13
|
-
*/
|
|
14
|
-
register(plugin: Plugin): void;
|
|
15
|
-
/**
|
|
16
|
-
* Register multiple plugins at once
|
|
17
|
-
*/
|
|
18
|
-
registerAll(plugins: Plugin[]): void;
|
|
19
|
-
/**
|
|
20
|
-
* Get all registered plugins
|
|
21
|
-
*/
|
|
22
|
-
getAll(): Plugin[];
|
|
23
|
-
/**
|
|
24
|
-
* Get plugin by id
|
|
25
|
-
*/
|
|
26
|
-
get(id: string): Plugin | undefined;
|
|
27
|
-
/**
|
|
28
|
-
* Initialize all plugins.
|
|
29
|
-
* Called once during issuer setup.
|
|
30
|
-
* Plugins can set up initial state or validate configuration.
|
|
31
|
-
*
|
|
32
|
-
* @throws PluginError if any plugin initialization fails
|
|
33
|
-
*/
|
|
34
|
-
initialize(): Promise<void>;
|
|
35
|
-
/**
|
|
36
|
-
* Execute authorize hooks for all plugins.
|
|
37
|
-
* Called before processing an authorization request.
|
|
38
|
-
* Can validate, rate limit, or enhance the request.
|
|
39
|
-
*/
|
|
40
|
-
executeAuthorizeHooks(clientID: string, provider?: string, scopes?: string[]): Promise<void>;
|
|
41
|
-
/**
|
|
42
|
-
* Execute success hooks for all plugins.
|
|
43
|
-
* Called after successful authentication.
|
|
44
|
-
* Runs in parallel for better performance.
|
|
45
|
-
* Plugins cannot modify the response.
|
|
46
|
-
*/
|
|
47
|
-
executeSuccessHooks(clientID: string, provider: string | undefined, subject: {
|
|
48
|
-
type: string;
|
|
49
|
-
properties: Record<string, unknown>;
|
|
50
|
-
}): Promise<void>;
|
|
51
|
-
/**
|
|
52
|
-
* Execute error hooks for all plugins.
|
|
53
|
-
* Called when an authentication error occurs.
|
|
54
|
-
*/
|
|
55
|
-
executeErrorHooks(error: Error, clientID?: string, provider?: string): Promise<void>;
|
|
56
|
-
/**
|
|
57
|
-
* Setup plugin routes on a router
|
|
58
|
-
*/
|
|
59
|
-
setupRoutes(router: Router): void;
|
|
60
|
-
}
|
|
61
|
-
//#endregion
|
|
62
|
-
export { PluginManager };
|
package/dist/plugin/manager.mjs
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { PluginError } from "./types.mjs";
|
|
2
|
-
|
|
3
|
-
//#region src/plugin/manager.ts
|
|
4
|
-
var PluginManager = class {
|
|
5
|
-
plugins = /* @__PURE__ */ new Map();
|
|
6
|
-
storage;
|
|
7
|
-
constructor(storage) {
|
|
8
|
-
this.storage = storage;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Register a plugin
|
|
12
|
-
*/
|
|
13
|
-
register(plugin) {
|
|
14
|
-
if (this.plugins.has(plugin.id)) throw new PluginError(`Plugin already registered`, plugin.id);
|
|
15
|
-
this.plugins.set(plugin.id, plugin);
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Register multiple plugins at once
|
|
19
|
-
*/
|
|
20
|
-
registerAll(plugins) {
|
|
21
|
-
for (const plugin of plugins) this.register(plugin);
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Get all registered plugins
|
|
25
|
-
*/
|
|
26
|
-
getAll() {
|
|
27
|
-
return Array.from(this.plugins.values());
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Get plugin by id
|
|
31
|
-
*/
|
|
32
|
-
get(id) {
|
|
33
|
-
return this.plugins.get(id);
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Initialize all plugins.
|
|
37
|
-
* Called once during issuer setup.
|
|
38
|
-
* Plugins can set up initial state or validate configuration.
|
|
39
|
-
*
|
|
40
|
-
* @throws PluginError if any plugin initialization fails
|
|
41
|
-
*/
|
|
42
|
-
async initialize() {
|
|
43
|
-
for (const plugin of this.plugins.values()) {
|
|
44
|
-
if (!plugin.onInit) continue;
|
|
45
|
-
try {
|
|
46
|
-
const context = {
|
|
47
|
-
pluginId: plugin.id,
|
|
48
|
-
request: new Request("http://internal/init"),
|
|
49
|
-
now: /* @__PURE__ */ new Date(),
|
|
50
|
-
storage: this.storage
|
|
51
|
-
};
|
|
52
|
-
await plugin.onInit(context);
|
|
53
|
-
} catch (error) {
|
|
54
|
-
throw new PluginError(`Initialization failed: ${error instanceof Error ? error.message : String(error)}`, plugin.id);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Execute authorize hooks for all plugins.
|
|
60
|
-
* Called before processing an authorization request.
|
|
61
|
-
* Can validate, rate limit, or enhance the request.
|
|
62
|
-
*/
|
|
63
|
-
async executeAuthorizeHooks(clientID, provider, scopes) {
|
|
64
|
-
for (const plugin of this.plugins.values()) {
|
|
65
|
-
if (!plugin.onAuthorize) continue;
|
|
66
|
-
try {
|
|
67
|
-
const context = {
|
|
68
|
-
pluginId: plugin.id,
|
|
69
|
-
request: new Request("http://internal/authorize"),
|
|
70
|
-
now: /* @__PURE__ */ new Date(),
|
|
71
|
-
storage: this.storage,
|
|
72
|
-
clientID,
|
|
73
|
-
provider,
|
|
74
|
-
scopes
|
|
75
|
-
};
|
|
76
|
-
await plugin.onAuthorize(context);
|
|
77
|
-
} catch (error) {
|
|
78
|
-
throw new PluginError(`Authorization hook failed: ${error instanceof Error ? error.message : String(error)}`, plugin.id);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Execute success hooks for all plugins.
|
|
84
|
-
* Called after successful authentication.
|
|
85
|
-
* Runs in parallel for better performance.
|
|
86
|
-
* Plugins cannot modify the response.
|
|
87
|
-
*/
|
|
88
|
-
async executeSuccessHooks(clientID, provider, subject) {
|
|
89
|
-
const hooks = Array.from(this.plugins.values()).filter((p) => p.onSuccess).map(async (plugin) => {
|
|
90
|
-
const context = {
|
|
91
|
-
pluginId: plugin.id,
|
|
92
|
-
request: new Request("http://internal/success"),
|
|
93
|
-
now: /* @__PURE__ */ new Date(),
|
|
94
|
-
storage: this.storage,
|
|
95
|
-
clientID,
|
|
96
|
-
provider,
|
|
97
|
-
subject
|
|
98
|
-
};
|
|
99
|
-
return plugin.onSuccess?.(context).catch((error) => {
|
|
100
|
-
console.error(`[Plugin: ${plugin.id}] Success hook failed:`, error instanceof Error ? error.message : String(error));
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
await Promise.all(hooks);
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Execute error hooks for all plugins.
|
|
107
|
-
* Called when an authentication error occurs.
|
|
108
|
-
*/
|
|
109
|
-
async executeErrorHooks(error, clientID, provider) {
|
|
110
|
-
for (const plugin of this.plugins.values()) {
|
|
111
|
-
if (!plugin.onError) continue;
|
|
112
|
-
try {
|
|
113
|
-
const context = {
|
|
114
|
-
pluginId: plugin.id,
|
|
115
|
-
request: new Request("http://internal/error"),
|
|
116
|
-
now: /* @__PURE__ */ new Date(),
|
|
117
|
-
storage: this.storage,
|
|
118
|
-
error,
|
|
119
|
-
clientID,
|
|
120
|
-
provider
|
|
121
|
-
};
|
|
122
|
-
await plugin.onError(context);
|
|
123
|
-
} catch (hookError) {
|
|
124
|
-
console.error(`[Plugin: ${plugin.id}] Error hook failed:`, hookError instanceof Error ? hookError.message : String(hookError));
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Setup plugin routes on a router
|
|
130
|
-
*/
|
|
131
|
-
setupRoutes(router) {
|
|
132
|
-
const registeredPaths = /* @__PURE__ */ new Set();
|
|
133
|
-
for (const plugin of this.plugins.values()) {
|
|
134
|
-
if (!plugin.routes) continue;
|
|
135
|
-
for (const route of plugin.routes) {
|
|
136
|
-
const fullPath = `/plugin/${plugin.id}${route.path}`;
|
|
137
|
-
if (registeredPaths.has(fullPath)) throw new PluginError(`Route conflict: ${fullPath} already registered`, plugin.id);
|
|
138
|
-
registeredPaths.add(fullPath);
|
|
139
|
-
const handler = async (ctx) => {
|
|
140
|
-
const pluginCtx = {
|
|
141
|
-
...ctx,
|
|
142
|
-
storage: this.storage
|
|
143
|
-
};
|
|
144
|
-
return await route.handler(pluginCtx);
|
|
145
|
-
};
|
|
146
|
-
switch (route.method) {
|
|
147
|
-
case "GET":
|
|
148
|
-
router.get(fullPath, handler);
|
|
149
|
-
break;
|
|
150
|
-
case "POST":
|
|
151
|
-
router.post(fullPath, handler);
|
|
152
|
-
break;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
//#endregion
|
|
160
|
-
export { PluginManager };
|
package/dist/plugin/plugin.d.mts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { Plugin, PluginAuthorizeHook, PluginErrorHook, PluginInitHook, PluginRouteHandler, PluginSuccessHook } from "./types.mjs";
|
|
2
|
-
|
|
3
|
-
//#region src/plugin/plugin.d.ts
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Plugin builder interface for creating plugins with a fluent API.
|
|
7
|
-
*
|
|
8
|
-
* The builder pattern allows for elegant plugin definition:
|
|
9
|
-
* - Chain route definitions with lifecycle hooks
|
|
10
|
-
* - Each method returns this for chaining
|
|
11
|
-
* - Build finalizes the plugin definition
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* ```ts
|
|
15
|
-
* const myPlugin = plugin("my-plugin")
|
|
16
|
-
* .onInit(async (ctx) => {
|
|
17
|
-
* console.log("Plugin initialized")
|
|
18
|
-
* })
|
|
19
|
-
* .post("/action", async (ctx) => {
|
|
20
|
-
* return ctx.json({ success: true })
|
|
21
|
-
* })
|
|
22
|
-
* .build()
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
25
|
-
interface PluginBuilder {
|
|
26
|
-
/** Register a GET route */
|
|
27
|
-
get(path: string, handler: PluginRouteHandler): PluginBuilder;
|
|
28
|
-
/** Register a POST route */
|
|
29
|
-
post(path: string, handler: PluginRouteHandler): PluginBuilder;
|
|
30
|
-
/** Register initialization hook (called once during issuer setup) */
|
|
31
|
-
onInit(handler: PluginInitHook): PluginBuilder;
|
|
32
|
-
/** Register authorization hook (called before authorization request) */
|
|
33
|
-
onAuthorize(handler: PluginAuthorizeHook): PluginBuilder;
|
|
34
|
-
/** Register success hook (called after successful authentication) */
|
|
35
|
-
onSuccess(handler: PluginSuccessHook): PluginBuilder;
|
|
36
|
-
/** Register error hook (called when authentication fails) */
|
|
37
|
-
onError(handler: PluginErrorHook): PluginBuilder;
|
|
38
|
-
/** Build the final plugin */
|
|
39
|
-
build(): Plugin;
|
|
40
|
-
}
|
|
41
|
-
//#endregion
|
|
42
|
-
export { PluginBuilder };
|
package/dist/plugin/plugin.mjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|
package/dist/plugin/types.d.mts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { StorageAdapter } from "../storage/storage.mjs";
|
|
2
|
-
import { RouterContext } from "@draftlab/auth-router/types";
|
|
3
|
-
|
|
4
|
-
//#region src/plugin/types.d.ts
|
|
5
|
-
|
|
6
|
-
interface PluginContext extends RouterContext {
|
|
7
|
-
storage: StorageAdapter;
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Plugin route handler function
|
|
11
|
-
*/
|
|
12
|
-
type PluginRouteHandler = (context: PluginContext) => Promise<Response>;
|
|
13
|
-
/**
|
|
14
|
-
* Plugin route definition
|
|
15
|
-
*/
|
|
16
|
-
interface PluginRoute {
|
|
17
|
-
/** Route path (e.g., "/admin", "/stats") */
|
|
18
|
-
readonly path: string;
|
|
19
|
-
/** HTTP method */
|
|
20
|
-
readonly method: "GET" | "POST";
|
|
21
|
-
/** Route handler function */
|
|
22
|
-
readonly handler: PluginRouteHandler;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Lifecycle hook context provided to plugin hooks.
|
|
26
|
-
* Contains information about the current operation and access to isolated storage.
|
|
27
|
-
*/
|
|
28
|
-
interface PluginHookContext {
|
|
29
|
-
/** Unique identifier for the plugin */
|
|
30
|
-
pluginId: string;
|
|
31
|
-
/** Raw request object */
|
|
32
|
-
request: Request;
|
|
33
|
-
/** Current time for consistency across hook execution */
|
|
34
|
-
now: Date;
|
|
35
|
-
/** Storage adapter for data persistence */
|
|
36
|
-
storage: StorageAdapter;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Hook called when the issuer is being initialized.
|
|
40
|
-
* Useful for plugins that need to set up initial state or validate configuration.
|
|
41
|
-
* Should complete quickly - takes place during server startup.
|
|
42
|
-
*/
|
|
43
|
-
type PluginInitHook = (context: PluginHookContext) => Promise<void>;
|
|
44
|
-
/**
|
|
45
|
-
* Hook called before an authorization request is processed.
|
|
46
|
-
* Use for validation, rate limiting, or request enhancement.
|
|
47
|
-
*/
|
|
48
|
-
type PluginAuthorizeHook = (context: PluginHookContext & {
|
|
49
|
-
clientID: string;
|
|
50
|
-
provider?: string;
|
|
51
|
-
scopes?: string[];
|
|
52
|
-
}) => Promise<void>;
|
|
53
|
-
/**
|
|
54
|
-
* Hook called after successful authentication.
|
|
55
|
-
* Use for logging, analytics, webhooks, or side effects.
|
|
56
|
-
* Cannot modify the response - hooks run in parallel.
|
|
57
|
-
*/
|
|
58
|
-
type PluginSuccessHook = (context: PluginHookContext & {
|
|
59
|
-
clientID: string;
|
|
60
|
-
provider?: string;
|
|
61
|
-
subject: {
|
|
62
|
-
type: string;
|
|
63
|
-
properties: Record<string, unknown>;
|
|
64
|
-
};
|
|
65
|
-
}) => Promise<void>;
|
|
66
|
-
/**
|
|
67
|
-
* Hook called when an authentication error occurs.
|
|
68
|
-
* Use for error logging, custom error pages, or error transformation.
|
|
69
|
-
*/
|
|
70
|
-
type PluginErrorHook = (context: PluginHookContext & {
|
|
71
|
-
error: Error;
|
|
72
|
-
clientID?: string;
|
|
73
|
-
provider?: string;
|
|
74
|
-
}) => Promise<void>;
|
|
75
|
-
/**
|
|
76
|
-
* Main plugin interface with lifecycle hooks and storage isolation
|
|
77
|
-
*/
|
|
78
|
-
interface Plugin {
|
|
79
|
-
/** Unique plugin identifier */
|
|
80
|
-
readonly id: string;
|
|
81
|
-
/** Custom routes added by this plugin */
|
|
82
|
-
readonly routes?: readonly PluginRoute[];
|
|
83
|
-
/** Called once when the issuer initializes */
|
|
84
|
-
readonly onInit?: PluginInitHook;
|
|
85
|
-
/** Called before authorization request is processed */
|
|
86
|
-
readonly onAuthorize?: PluginAuthorizeHook;
|
|
87
|
-
/** Called after successful authentication */
|
|
88
|
-
readonly onSuccess?: PluginSuccessHook;
|
|
89
|
-
/** Called when an error occurs during authentication */
|
|
90
|
-
readonly onError?: PluginErrorHook;
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Plugin error types
|
|
94
|
-
*/
|
|
95
|
-
declare class PluginError extends Error {
|
|
96
|
-
constructor(message: string, pluginId: string);
|
|
97
|
-
}
|
|
98
|
-
//#endregion
|
|
99
|
-
export { Plugin, PluginAuthorizeHook, PluginContext, PluginError, PluginErrorHook, PluginHookContext, PluginInitHook, PluginRoute, PluginRouteHandler, PluginSuccessHook };
|
package/dist/plugin/types.mjs
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
//#region src/plugin/types.ts
|
|
2
|
-
/**
|
|
3
|
-
* Plugin error types
|
|
4
|
-
*/
|
|
5
|
-
var PluginError = class extends Error {
|
|
6
|
-
constructor(message, pluginId) {
|
|
7
|
-
super(`[Plugin: ${pluginId}] ${message}`);
|
|
8
|
-
this.name = "PluginError";
|
|
9
|
-
}
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
//#endregion
|
|
13
|
-
export { PluginError };
|