@draftlab/auth 0.7.0 → 0.9.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.mjs +4 -1
- package/dist/core.d.mts +2 -21
- package/dist/core.mjs +78 -6
- package/dist/plugin/builder.d.mts +18 -1
- package/dist/plugin/builder.mjs +47 -3
- package/dist/plugin/manager.d.mts +29 -0
- package/dist/plugin/manager.mjs +94 -1
- package/dist/plugin/plugin.d.mts +30 -5
- package/dist/plugin/types.d.mts +61 -2
- package/dist/provider/oauth2.mjs +7 -0
- package/package.json +3 -3
package/dist/client.mjs
CHANGED
|
@@ -201,7 +201,10 @@ const createClient = (input) => {
|
|
|
201
201
|
},
|
|
202
202
|
async verify(subjects, token, options) {
|
|
203
203
|
try {
|
|
204
|
-
const jwtResult = await jwtVerify(token, await getJWKS(), {
|
|
204
|
+
const jwtResult = await jwtVerify(token, await getJWKS(), {
|
|
205
|
+
issuer: options?.issuer ?? issuer,
|
|
206
|
+
...options?.audience && { audience: options.audience }
|
|
207
|
+
});
|
|
205
208
|
const validated = await subjects[jwtResult.payload.type]?.["~standard"].validate(jwtResult.payload.properties);
|
|
206
209
|
if (!validated?.issues && jwtResult.payload.mode === "access") return {
|
|
207
210
|
success: true,
|
package/dist/core.d.mts
CHANGED
|
@@ -6,6 +6,7 @@ import { StorageAdapter } from "./storage/storage.mjs";
|
|
|
6
6
|
import { Plugin } from "./plugin/types.mjs";
|
|
7
7
|
import { Provider } from "./provider/provider.mjs";
|
|
8
8
|
import { Theme } from "./themes/theme.mjs";
|
|
9
|
+
import { AuthorizationState } from "./types.mjs";
|
|
9
10
|
import { Router } from "@draftlab/auth-router";
|
|
10
11
|
|
|
11
12
|
//#region src/core.d.ts
|
|
@@ -27,26 +28,6 @@ interface OnSuccessResponder<T$1 extends {
|
|
|
27
28
|
subject?: string;
|
|
28
29
|
}): Promise<Response>;
|
|
29
30
|
}
|
|
30
|
-
/**
|
|
31
|
-
* Authorization state for OAuth 2.0 flows.
|
|
32
|
-
*/
|
|
33
|
-
interface AuthorizationState {
|
|
34
|
-
/** OAuth redirect URI */
|
|
35
|
-
redirect_uri: string;
|
|
36
|
-
/** OAuth response type */
|
|
37
|
-
response_type: string;
|
|
38
|
-
/** OAuth state parameter for CSRF protection */
|
|
39
|
-
state: string;
|
|
40
|
-
/** OAuth client identifier */
|
|
41
|
-
client_id: string;
|
|
42
|
-
/** OAuth audience parameter */
|
|
43
|
-
audience: string;
|
|
44
|
-
/** PKCE challenge data for code verification */
|
|
45
|
-
pkce?: {
|
|
46
|
-
challenge: string;
|
|
47
|
-
method: "S256";
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
31
|
/**
|
|
51
32
|
* Main issuer input configuration interface.
|
|
52
33
|
*/
|
|
@@ -129,4 +110,4 @@ declare const issuer: <Providers extends Record<string, Provider<unknown>>, Subj
|
|
|
129
110
|
};
|
|
130
111
|
}>;
|
|
131
112
|
//#endregion
|
|
132
|
-
export {
|
|
113
|
+
export { OnSuccessResponder, issuer };
|
package/dist/core.mjs
CHANGED
|
@@ -139,7 +139,8 @@ const issuer = (input) => {
|
|
|
139
139
|
clientID: value.clientID,
|
|
140
140
|
subject: value.subject,
|
|
141
141
|
ttl: value.ttl,
|
|
142
|
-
nextToken: generateSecureToken()
|
|
142
|
+
nextToken: generateSecureToken(),
|
|
143
|
+
timeUsed: value.timeUsed
|
|
143
144
|
};
|
|
144
145
|
const refreshKey = [
|
|
145
146
|
"oauth:refresh",
|
|
@@ -187,6 +188,15 @@ const issuer = (input) => {
|
|
|
187
188
|
const authorization = await getAuthorization(ctx);
|
|
188
189
|
const currentProvider = ctx.get("provider") || "unknown";
|
|
189
190
|
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
|
+
}
|
|
190
200
|
return await input.success({ async subject(type, properties$1, subjectOpts) {
|
|
191
201
|
const subject = subjectOpts?.subject ?? await resolveSubject(type, properties$1);
|
|
192
202
|
await successOpts?.invalidate?.(await resolveSubject(type, properties$1));
|
|
@@ -222,6 +232,7 @@ const issuer = (input) => {
|
|
|
222
232
|
subject,
|
|
223
233
|
redirectURI: authorization.redirect_uri,
|
|
224
234
|
clientID: authorization.client_id,
|
|
235
|
+
scopes: authorization.scopes,
|
|
225
236
|
pkce: authorization.pkce,
|
|
226
237
|
ttl: {
|
|
227
238
|
access: subjectOpts?.ttl?.access ?? ttlAccess,
|
|
@@ -276,10 +287,21 @@ const issuer = (input) => {
|
|
|
276
287
|
storage
|
|
277
288
|
};
|
|
278
289
|
const app = new Router({ basePath: input.basePath });
|
|
279
|
-
|
|
280
|
-
|
|
290
|
+
const manager = input.plugins && input.plugins.length > 0 ? new PluginManager(input.storage) : null;
|
|
291
|
+
let pluginsInitialized = false;
|
|
292
|
+
if (manager && input.plugins) {
|
|
281
293
|
manager.registerAll(input.plugins);
|
|
282
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
|
+
});
|
|
283
305
|
}
|
|
284
306
|
for (const [name, value] of Object.entries(input.providers)) {
|
|
285
307
|
const route = new Router();
|
|
@@ -472,13 +494,51 @@ const issuer = (input) => {
|
|
|
472
494
|
credentials: false
|
|
473
495
|
})],
|
|
474
496
|
handler: async (c) => {
|
|
475
|
-
const
|
|
497
|
+
const form = await c.formData();
|
|
498
|
+
const token = form.get("token")?.toString();
|
|
499
|
+
const tokenTypeHint = form.get("token_type_hint")?.toString();
|
|
476
500
|
if (!token) {
|
|
477
501
|
const error$1 = new OauthError("invalid_request", "Missing token parameter");
|
|
478
502
|
return c.json(error$1.toJSON(), { status: 400 });
|
|
479
503
|
}
|
|
480
504
|
try {
|
|
481
|
-
|
|
505
|
+
if (tokenTypeHint === "refresh_token") {
|
|
506
|
+
const splits$1 = token.split(":");
|
|
507
|
+
const tokenPart$1 = splits$1.pop();
|
|
508
|
+
if (tokenPart$1 && splits$1.length > 0) {
|
|
509
|
+
const key = [
|
|
510
|
+
"oauth:refresh",
|
|
511
|
+
splits$1.join(":"),
|
|
512
|
+
tokenPart$1
|
|
513
|
+
];
|
|
514
|
+
if (await Storage.get(storage, key)) {
|
|
515
|
+
await Storage.remove(storage, key);
|
|
516
|
+
const expiresAt$1 = Date.now() + ttlRefreshRetention * 1e3;
|
|
517
|
+
await Revocation.revoke(storage, token, expiresAt$1);
|
|
518
|
+
return c.json({});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
} else if (tokenTypeHint === "access_token") {
|
|
522
|
+
const expiresAt$1 = Date.now() + ttlAccess * 1e3;
|
|
523
|
+
await Revocation.revoke(storage, token, expiresAt$1);
|
|
524
|
+
return c.json({});
|
|
525
|
+
}
|
|
526
|
+
const splits = token.split(":");
|
|
527
|
+
const tokenPart = splits.pop();
|
|
528
|
+
if (tokenPart && splits.length > 0) {
|
|
529
|
+
const key = [
|
|
530
|
+
"oauth:refresh",
|
|
531
|
+
splits.join(":"),
|
|
532
|
+
tokenPart
|
|
533
|
+
];
|
|
534
|
+
if (await Storage.get(storage, key)) {
|
|
535
|
+
await Storage.remove(storage, key);
|
|
536
|
+
const expiresAt$1 = Date.now() + ttlRefreshRetention * 1e3;
|
|
537
|
+
await Revocation.revoke(storage, token, expiresAt$1);
|
|
538
|
+
return c.json({});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const expiresAt = Date.now() + ttlAccess * 1e3;
|
|
482
542
|
await Revocation.revoke(storage, token, expiresAt);
|
|
483
543
|
return c.json({});
|
|
484
544
|
} catch (_err) {
|
|
@@ -498,13 +558,15 @@ const issuer = (input) => {
|
|
|
498
558
|
const audience = c.query("audience");
|
|
499
559
|
const code_challenge = c.query("code_challenge");
|
|
500
560
|
const code_challenge_method = c.query("code_challenge_method");
|
|
561
|
+
const scope = c.query("scope");
|
|
501
562
|
const authorization = {
|
|
502
563
|
response_type,
|
|
503
564
|
redirect_uri,
|
|
504
565
|
state,
|
|
505
566
|
client_id,
|
|
506
567
|
audience,
|
|
507
|
-
scope
|
|
568
|
+
scope,
|
|
569
|
+
scopes: scope ? scope.split(" ").filter(Boolean) : void 0,
|
|
508
570
|
...code_challenge && code_challenge_method && { pkce: {
|
|
509
571
|
challenge: code_challenge,
|
|
510
572
|
method: code_challenge_method
|
|
@@ -520,6 +582,10 @@ const issuer = (input) => {
|
|
|
520
582
|
redirectURI: redirect_uri,
|
|
521
583
|
audience
|
|
522
584
|
}, 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
|
+
}
|
|
523
589
|
await auth.set(c, "authorization", 900, authorization);
|
|
524
590
|
if (provider) return c.redirect(`${provider}/authorize`);
|
|
525
591
|
const availableProviders = Object.keys(input.providers);
|
|
@@ -527,6 +593,12 @@ const issuer = (input) => {
|
|
|
527
593
|
return auth.forward(c, await select()(Object.fromEntries(Object.entries(input.providers).map(([key, value]) => [key, value.type])), c.request));
|
|
528
594
|
});
|
|
529
595
|
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
|
+
}
|
|
530
602
|
if (err instanceof UnknownStateError) return auth.forward(c, await error(err, c.request));
|
|
531
603
|
try {
|
|
532
604
|
const authorization = await getAuthorization(c);
|
|
@@ -3,7 +3,24 @@ import { PluginBuilder } from "./plugin.mjs";
|
|
|
3
3
|
//#region src/plugin/builder.d.ts
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Create a new plugin
|
|
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
|
+
* ```
|
|
7
24
|
*/
|
|
8
25
|
declare const plugin: (id: string) => PluginBuilder;
|
|
9
26
|
//#endregion
|
package/dist/plugin/builder.mjs
CHANGED
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
//#region src/plugin/builder.ts
|
|
2
2
|
/**
|
|
3
|
-
* Create a new plugin
|
|
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
|
+
* ```
|
|
4
21
|
*/
|
|
5
22
|
const plugin = (id) => {
|
|
6
23
|
if (!id || typeof id !== "string") throw new Error("Plugin id must be a non-empty string");
|
|
7
24
|
const routes = [];
|
|
8
25
|
const registeredPaths = /* @__PURE__ */ new Set();
|
|
26
|
+
let initHook;
|
|
27
|
+
let authorizeHook;
|
|
28
|
+
let successHook;
|
|
29
|
+
let errorHook;
|
|
9
30
|
const validatePath = (path) => {
|
|
10
31
|
if (!path || typeof path !== "string") throw new Error("Route path must be a non-empty string");
|
|
11
32
|
if (!path.startsWith("/")) throw new Error("Route path must start with '/'");
|
|
@@ -35,11 +56,34 @@ const plugin = (id) => {
|
|
|
35
56
|
});
|
|
36
57
|
return this;
|
|
37
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
|
+
},
|
|
38
79
|
build() {
|
|
39
|
-
if (routes.length === 0) throw new Error(`Plugin '${id}' has no routes defined`);
|
|
40
80
|
return {
|
|
41
81
|
id,
|
|
42
|
-
routes
|
|
82
|
+
routes: routes.length > 0 ? routes : void 0,
|
|
83
|
+
onInit: initHook,
|
|
84
|
+
onAuthorize: authorizeHook,
|
|
85
|
+
onSuccess: successHook,
|
|
86
|
+
onError: errorHook
|
|
43
87
|
};
|
|
44
88
|
}
|
|
45
89
|
};
|
|
@@ -24,6 +24,35 @@ declare class PluginManager {
|
|
|
24
24
|
* Get plugin by id
|
|
25
25
|
*/
|
|
26
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>;
|
|
27
56
|
/**
|
|
28
57
|
* Setup plugin routes on a router
|
|
29
58
|
*/
|
package/dist/plugin/manager.mjs
CHANGED
|
@@ -33,6 +33,99 @@ var PluginManager = class {
|
|
|
33
33
|
return this.plugins.get(id);
|
|
34
34
|
}
|
|
35
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
|
+
/**
|
|
36
129
|
* Setup plugin routes on a router
|
|
37
130
|
*/
|
|
38
131
|
setupRoutes(router) {
|
|
@@ -40,7 +133,7 @@ var PluginManager = class {
|
|
|
40
133
|
for (const plugin of this.plugins.values()) {
|
|
41
134
|
if (!plugin.routes) continue;
|
|
42
135
|
for (const route of plugin.routes) {
|
|
43
|
-
const fullPath =
|
|
136
|
+
const fullPath = `/plugin/${plugin.id}${route.path}`;
|
|
44
137
|
if (registeredPaths.has(fullPath)) throw new PluginError(`Route conflict: ${fullPath} already registered`, plugin.id);
|
|
45
138
|
registeredPaths.add(fullPath);
|
|
46
139
|
const handler = async (ctx) => {
|
package/dist/plugin/plugin.d.mts
CHANGED
|
@@ -1,16 +1,41 @@
|
|
|
1
|
-
import { Plugin, PluginRouteHandler } from "./types.mjs";
|
|
1
|
+
import { Plugin, PluginAuthorizeHook, PluginErrorHook, PluginInitHook, PluginRouteHandler, PluginSuccessHook } from "./types.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/plugin/plugin.d.ts
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Plugin builder interface
|
|
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
|
+
* ```
|
|
7
24
|
*/
|
|
8
25
|
interface PluginBuilder {
|
|
9
|
-
/**
|
|
26
|
+
/** Register a GET route */
|
|
10
27
|
get(path: string, handler: PluginRouteHandler): PluginBuilder;
|
|
11
|
-
/**
|
|
28
|
+
/** Register a POST route */
|
|
12
29
|
post(path: string, handler: PluginRouteHandler): PluginBuilder;
|
|
13
|
-
/**
|
|
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 */
|
|
14
39
|
build(): Plugin;
|
|
15
40
|
}
|
|
16
41
|
//#endregion
|
package/dist/plugin/types.d.mts
CHANGED
|
@@ -22,13 +22,72 @@ interface PluginRoute {
|
|
|
22
22
|
readonly handler: PluginRouteHandler;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
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
|
|
26
77
|
*/
|
|
27
78
|
interface Plugin {
|
|
28
79
|
/** Unique plugin identifier */
|
|
29
80
|
readonly id: string;
|
|
30
81
|
/** Custom routes added by this plugin */
|
|
31
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;
|
|
32
91
|
}
|
|
33
92
|
/**
|
|
34
93
|
* Plugin error types
|
|
@@ -37,4 +96,4 @@ declare class PluginError extends Error {
|
|
|
37
96
|
constructor(message: string, pluginId: string);
|
|
38
97
|
}
|
|
39
98
|
//#endregion
|
|
40
|
-
export { Plugin, PluginContext, PluginError, PluginRoute, PluginRouteHandler };
|
|
99
|
+
export { Plugin, PluginAuthorizeHook, PluginContext, PluginError, PluginErrorHook, PluginHookContext, PluginInitHook, PluginRoute, PluginRouteHandler, PluginSuccessHook };
|
package/dist/provider/oauth2.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { getRelativeUrl } from "../util.mjs";
|
|
|
2
2
|
import { OauthError } from "../error.mjs";
|
|
3
3
|
import { generatePKCE } from "../pkce.mjs";
|
|
4
4
|
import { generateSecureToken, timingSafeCompare } from "../random.mjs";
|
|
5
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
5
6
|
|
|
6
7
|
//#region src/provider/oauth2.ts
|
|
7
8
|
/**
|
|
@@ -70,6 +71,12 @@ const Oauth2Provider = (config) => {
|
|
|
70
71
|
if (!response.ok) throw new Error(`Token request failed with status ${response.status}`);
|
|
71
72
|
const tokenData = await response.json();
|
|
72
73
|
if (tokenData.error) throw new OauthError(tokenData.error, tokenData.error_description || "");
|
|
74
|
+
if (tokenData.id_token && config.endpoint.jwks) try {
|
|
75
|
+
const jwks = createRemoteJWKSet(new URL(config.endpoint.jwks));
|
|
76
|
+
await jwtVerify(tokenData.id_token, jwks, { issuer: config.endpoint.authorization.split("/").slice(0, 3).join("/") });
|
|
77
|
+
} catch (error) {
|
|
78
|
+
throw new OauthError("invalid_request", `ID token validation failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
79
|
+
}
|
|
73
80
|
return await ctx.success(c, {
|
|
74
81
|
clientID: config.clientID,
|
|
75
82
|
tokenset: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@draftlab/auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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": "^24.10.1",
|
|
41
41
|
"@types/qrcode": "^1.5.6",
|
|
42
|
-
"tsdown": "^0.16.
|
|
42
|
+
"tsdown": "^0.16.7",
|
|
43
43
|
"typescript": "^5.9.3",
|
|
44
44
|
"@draftlab/tsconfig": "0.1.0"
|
|
45
45
|
},
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"preact": "^10.27.2",
|
|
64
64
|
"preact-render-to-string": "^6.6.3",
|
|
65
65
|
"qrcode": "^1.5.4",
|
|
66
|
-
"@draftlab/auth-router": "0.
|
|
66
|
+
"@draftlab/auth-router": "0.2.0"
|
|
67
67
|
},
|
|
68
68
|
"engines": {
|
|
69
69
|
"node": ">=18"
|