@draftlab/auth 0.2.1 → 0.2.3

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/core.d.ts CHANGED
@@ -3,6 +3,7 @@ import { UnknownStateError } from "./error.js";
3
3
  import { Prettify } from "./util.js";
4
4
  import { SubjectPayload, SubjectSchema } from "./subject.js";
5
5
  import { StorageAdapter } from "./storage/storage.js";
6
+ import { Plugin } from "./plugin/types.js";
6
7
  import { Provider } from "./provider/provider.js";
7
8
  import { Theme } from "./themes/theme.js";
8
9
  import { Router } from "@draftlab/auth-router";
@@ -79,6 +80,8 @@ interface IssuerInput<Providers extends Record<string, Provider<unknown>>, Subje
79
80
  error?(error: UnknownStateError, req: Request): Promise<Response>;
80
81
  /** Client authorization check function */
81
82
  allow?(input: AllowCheckInput, req: Request): Promise<boolean>;
83
+ /** Plugin configuration */
84
+ plugins?: Plugin[];
82
85
  /**
83
86
  * Refresh callback for updating user claims.
84
87
  *
package/dist/core.js CHANGED
@@ -5,6 +5,7 @@ import { validatePKCE } from "./pkce.js";
5
5
  import { generateSecureToken } from "./random.js";
6
6
  import { Storage } from "./storage/storage.js";
7
7
  import { encryptionKeys, signingKeys } from "./keys.js";
8
+ import { PluginManager } from "./plugin/manager.js";
8
9
  import { setTheme } from "./themes/theme.js";
9
10
  import { Select } from "./ui/select.js";
10
11
  import { CompactEncrypt, SignJWT, compactDecrypt } from "jose";
@@ -40,8 +41,8 @@ const issuer = (input) => {
40
41
  headers: { "Content-Type": "text/plain" }
41
42
  });
42
43
  });
43
- const ttlAccess = input.ttl?.access ?? 60 * 60 * 24 * 30;
44
- const ttlRefresh = input.ttl?.refresh ?? 60 * 60 * 24 * 365;
44
+ const ttlAccess = input.ttl?.access ?? 3600 * 24 * 30;
45
+ const ttlRefresh = input.ttl?.refresh ?? 3600 * 24 * 365;
45
46
  const ttlRefreshReuse = input.ttl?.reuse ?? 60;
46
47
  const ttlRefreshRetention = input.ttl?.retention ?? 0;
47
48
  if (input.theme) setTheme(input.theme);
@@ -255,6 +256,11 @@ const issuer = (input) => {
255
256
  storage
256
257
  };
257
258
  const app = new Router({ basePath: input.basePath });
259
+ if (input.plugins && input.plugins.length > 0) {
260
+ const manager = new PluginManager(input.storage);
261
+ manager.registerAll(input.plugins);
262
+ manager.setupRoutes(app);
263
+ }
258
264
  for (const [name, value] of Object.entries(input.providers)) {
259
265
  const route = new Router();
260
266
  route.use(async (c, next) => {
@@ -466,7 +472,7 @@ const issuer = (input) => {
466
472
  redirectURI: redirect_uri,
467
473
  audience
468
474
  }, c.request)) throw new UnauthorizedClientError(client_id, redirect_uri);
469
- await auth.set(c, "authorization", 60 * 60 * 24, authorization);
475
+ await auth.set(c, "authorization", 3600 * 24, authorization);
470
476
  if (provider) return c.redirect(`${provider}/authorize`);
471
477
  const availableProviders = Object.keys(input.providers);
472
478
  if (availableProviders.length === 1) return c.redirect(`${availableProviders[0]}/authorize`);
@@ -0,0 +1,10 @@
1
+ import { PluginBuilder } from "./plugin.js";
2
+
3
+ //#region src/plugin/builder.d.ts
4
+
5
+ /**
6
+ * Create a new plugin
7
+ */
8
+ declare const plugin: (id: string) => PluginBuilder;
9
+ //#endregion
10
+ export { plugin };
@@ -0,0 +1,49 @@
1
+ //#region src/plugin/builder.ts
2
+ /**
3
+ * Create a new plugin
4
+ */
5
+ const plugin = (id) => {
6
+ if (!id || typeof id !== "string") throw new Error("Plugin id must be a non-empty string");
7
+ const routes = [];
8
+ const registeredPaths = /* @__PURE__ */ new Set();
9
+ const validatePath = (path) => {
10
+ if (!path || typeof path !== "string") throw new Error("Route path must be a non-empty string");
11
+ if (!path.startsWith("/")) throw new Error("Route path must start with '/'");
12
+ };
13
+ return {
14
+ get(path, handler) {
15
+ validatePath(path);
16
+ const routeKey = `GET ${path}`;
17
+ if (registeredPaths.has(routeKey)) throw new Error(`Route ${routeKey} already registered in plugin '${id}'`);
18
+ registeredPaths.add(routeKey);
19
+ routes.push({
20
+ method: "GET",
21
+ path,
22
+ handler
23
+ });
24
+ return this;
25
+ },
26
+ post(path, handler) {
27
+ validatePath(path);
28
+ const routeKey = `POST ${path}`;
29
+ if (registeredPaths.has(routeKey)) throw new Error(`Route ${routeKey} already registered in plugin '${id}'`);
30
+ registeredPaths.add(routeKey);
31
+ routes.push({
32
+ method: "POST",
33
+ path,
34
+ handler
35
+ });
36
+ return this;
37
+ },
38
+ build() {
39
+ if (routes.length === 0) throw new Error(`Plugin '${id}' has no routes defined`);
40
+ return {
41
+ id,
42
+ routes
43
+ };
44
+ }
45
+ };
46
+ };
47
+
48
+ //#endregion
49
+ export { plugin };
@@ -0,0 +1,33 @@
1
+ import { StorageAdapter } from "../storage/storage.js";
2
+ import { Plugin } from "./types.js";
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
+ * Setup plugin routes on a router
29
+ */
30
+ setupRoutes(router: Router): void;
31
+ }
32
+ //#endregion
33
+ export { PluginManager };
@@ -0,0 +1,67 @@
1
+ import { PluginError } from "./types.js";
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
+ * Setup plugin routes on a router
37
+ */
38
+ setupRoutes(router) {
39
+ const registeredPaths = /* @__PURE__ */ new Set();
40
+ for (const plugin of this.plugins.values()) {
41
+ if (!plugin.routes) continue;
42
+ for (const route of plugin.routes) {
43
+ const fullPath = `/${plugin.id}${route.path}`;
44
+ if (registeredPaths.has(fullPath)) throw new PluginError(`Route conflict: ${fullPath} already registered`, plugin.id);
45
+ registeredPaths.add(fullPath);
46
+ const handler = async (ctx) => {
47
+ const pluginCtx = {
48
+ ...ctx,
49
+ storage: this.storage
50
+ };
51
+ return await route.handler(pluginCtx);
52
+ };
53
+ switch (route.method) {
54
+ case "GET":
55
+ router.get(fullPath, handler);
56
+ break;
57
+ case "POST":
58
+ router.post(fullPath, handler);
59
+ break;
60
+ }
61
+ }
62
+ }
63
+ }
64
+ };
65
+
66
+ //#endregion
67
+ export { PluginManager };
@@ -0,0 +1,17 @@
1
+ import { Plugin, PluginRouteHandler } from "./types.js";
2
+
3
+ //#region src/plugin/plugin.d.ts
4
+
5
+ /**
6
+ * Plugin builder interface
7
+ */
8
+ interface PluginBuilder {
9
+ /** Add GET route */
10
+ get(path: string, handler: PluginRouteHandler): PluginBuilder;
11
+ /** Add POST route */
12
+ post(path: string, handler: PluginRouteHandler): PluginBuilder;
13
+ /** Build the plugin */
14
+ build(): Plugin;
15
+ }
16
+ //#endregion
17
+ export { PluginBuilder };
File without changes
@@ -0,0 +1,40 @@
1
+ import { StorageAdapter } from "../storage/storage.js";
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
+ * Main plugin interface
26
+ */
27
+ interface Plugin {
28
+ /** Unique plugin identifier */
29
+ readonly id: string;
30
+ /** Custom routes added by this plugin */
31
+ readonly routes?: readonly PluginRoute[];
32
+ }
33
+ /**
34
+ * Plugin error types
35
+ */
36
+ declare class PluginError extends Error {
37
+ constructor(message: string, pluginId: string);
38
+ }
39
+ //#endregion
40
+ export { Plugin, PluginContext, PluginError, PluginRoute, PluginRouteHandler };
@@ -0,0 +1,13 @@
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 };
@@ -111,7 +111,7 @@ const CodeProvider = (config) => {
111
111
  * Transitions between authentication states and renders the appropriate UI.
112
112
  */
113
113
  const transition = async (c, nextState, formData, error) => {
114
- await ctx.set(c, "provider", 60 * 60 * 24, nextState);
114
+ await ctx.set(c, "provider", 3600 * 24, nextState);
115
115
  const response = await config.request(c.request, nextState, formData, error);
116
116
  return ctx.forward(c, response);
117
117
  };
@@ -102,7 +102,7 @@ const Oauth2Provider = (config) => {
102
102
  routes.get("/authorize", async (c) => {
103
103
  const state = generateSecureToken();
104
104
  const pkce = config.pkce ? await generatePKCE() : void 0;
105
- await ctx.set(c, "provider", 60 * 10, {
105
+ await ctx.set(c, "provider", 600, {
106
106
  state,
107
107
  redirect: getRelativeUrl(c, "./callback"),
108
108
  codeVerifier: pkce?.verifier
@@ -88,7 +88,7 @@ const PasskeyProvider = (config) => {
88
88
  return {
89
89
  type: "passkey",
90
90
  init(routes, ctx) {
91
- const { rpName, authenticatorSelection, attestationType = "none", timeout = 5 * 60 * 1e3 } = config;
91
+ const { rpName, authenticatorSelection, attestationType = "none", timeout = 300 * 1e3 } = config;
92
92
  const getStoredUserById = async (userId) => {
93
93
  return await Storage.get(ctx.storage, userKey(userId));
94
94
  };
@@ -80,7 +80,7 @@ const PasswordProvider = (config) => {
80
80
  */
81
81
  routes.get("/register", async (c) => {
82
82
  const state = { type: "start" };
83
- await ctx.set(c, "provider", 60 * 60 * 24, state);
83
+ await ctx.set(c, "provider", 3600 * 24, state);
84
84
  return ctx.forward(c, await config.register(c.request, state));
85
85
  });
86
86
  /**
@@ -93,12 +93,12 @@ const PasswordProvider = (config) => {
93
93
  let provider = await ctx.get(c, "provider");
94
94
  if (!provider) {
95
95
  const state = { type: "start" };
96
- await ctx.set(c, "provider", 60 * 60 * 24, state);
96
+ await ctx.set(c, "provider", 3600 * 24, state);
97
97
  if (action === "register") provider = state;
98
98
  else return ctx.forward(c, await config.register(c.request, state));
99
99
  }
100
100
  const transition = async (next, err) => {
101
- await ctx.set(c, "provider", 60 * 60 * 24, next);
101
+ await ctx.set(c, "provider", 3600 * 24, next);
102
102
  return ctx.forward(c, await config.register(c.request, next, formData, err));
103
103
  };
104
104
  if (action === "register" && provider.type === "start") {
@@ -175,7 +175,7 @@ const PasswordProvider = (config) => {
175
175
  type: "start",
176
176
  redirect
177
177
  };
178
- await ctx.set(c, "provider", 60 * 60 * 24, state);
178
+ await ctx.set(c, "provider", 3600 * 24, state);
179
179
  return ctx.forward(c, await config.change(c.request, state));
180
180
  });
181
181
  /**
@@ -187,7 +187,7 @@ const PasswordProvider = (config) => {
187
187
  const provider = await ctx.get(c, "provider");
188
188
  if (!provider) throw new UnknownStateError();
189
189
  const transition = async (next, err) => {
190
- await ctx.set(c, "provider", 60 * 60 * 24, next);
190
+ await ctx.set(c, "provider", 3600 * 24, next);
191
191
  return ctx.forward(c, await config.change(c.request, next, formData, err));
192
192
  };
193
193
  if (action === "code") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@draftlab/auth",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "description": "Core implementation for @draftlab/auth",
6
6
  "author": "Matheus Pergoli",
@@ -37,8 +37,8 @@
37
37
  ],
38
38
  "license": "MIT",
39
39
  "devDependencies": {
40
- "@types/node": "^24.0.14",
41
- "tsdown": "^0.12.9",
40
+ "@types/node": "^24.1.0",
41
+ "tsdown": "^0.13.0",
42
42
  "typescript": "^5.8.3",
43
43
  "@draftlab/tsconfig": "0.1.0"
44
44
  },