@camstack/addon-auth 1.1.3 → 1.1.4

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.
@@ -7052,6 +7052,123 @@ var ConvertResultSchema = object({
7052
7052
  })).readonly()
7053
7053
  });
7054
7054
  /**
7055
+ * Build an `IAddonRouteProvider` from a list of routes. Implements
7056
+ * both the operator-facing `getRoutes` (returning route descriptors
7057
+ * minus the handlers, which can't cross JSON) and the framework-
7058
+ * private `invoke` method that the hub calls when this provider lives
7059
+ * in a forked worker.
7060
+ *
7061
+ * Co-located addons use the returned `getRoutes` directly because
7062
+ * their handlers don't need to cross any wire. The `invoke` method
7063
+ * is present anyway so the bridge code on the hub is uniform — it
7064
+ * doesn't need to switch on "local vs remote provider" at the call
7065
+ * site.
7066
+ *
7067
+ * Example:
7068
+ * const routes: IAddonHttpRoute[] = [
7069
+ * { method: 'GET', path: '/start', access: 'public', handler: this.handleStart },
7070
+ * ]
7071
+ * return [
7072
+ * {
7073
+ * capability: addonRoutesCapability,
7074
+ * provider: buildAddonRouteProvider('auth-oidc', routes),
7075
+ * },
7076
+ * ]
7077
+ */
7078
+ function buildAddonRouteProvider(id, routes) {
7079
+ return {
7080
+ id,
7081
+ getRoutes: () => routes,
7082
+ invoke: async (input) => {
7083
+ const match = matchRoute(routes, input.method, input.path);
7084
+ if (!match) return {
7085
+ status: 404,
7086
+ headers: {},
7087
+ redirectUrl: null,
7088
+ body: { error: `No route matches ${input.method} ${input.path}` }
7089
+ };
7090
+ const envelope = {
7091
+ status: 200,
7092
+ headers: {},
7093
+ redirectUrl: null
7094
+ };
7095
+ const reply = buildCapturingReply(envelope);
7096
+ const request = {
7097
+ params: {
7098
+ ...input.params,
7099
+ ...match.params
7100
+ },
7101
+ query: input.query,
7102
+ body: input.body,
7103
+ headers: input.headers,
7104
+ ...input.user ? { user: input.user } : {},
7105
+ ...input.scopedToken !== void 0 ? { scopedToken: input.scopedToken } : {}
7106
+ };
7107
+ await match.route.handler(request, reply);
7108
+ return envelope;
7109
+ }
7110
+ };
7111
+ }
7112
+ /**
7113
+ * Pattern matcher: same semantics as `AddonRouteRegistry.matchRoute`
7114
+ * but operating on a flat list and bypassing the `/addon/<id>/` prefix
7115
+ * — the bridge sends the post-prefix path directly so we don't need
7116
+ * to round-trip it through normalization.
7117
+ */
7118
+ function matchRoute(routes, method, path) {
7119
+ const normalizedMethod = method.toUpperCase();
7120
+ for (const route of routes) {
7121
+ if (route.method !== normalizedMethod) continue;
7122
+ const params = matchPath(route.path, path);
7123
+ if (params !== null) return {
7124
+ route,
7125
+ params
7126
+ };
7127
+ }
7128
+ return null;
7129
+ }
7130
+ function matchPath(pattern, p) {
7131
+ const patternParts = pattern.split("/").filter(Boolean);
7132
+ const pathParts = p.split("/").filter(Boolean);
7133
+ if (patternParts.length !== pathParts.length) return null;
7134
+ const params = {};
7135
+ for (let i = 0; i < patternParts.length; i++) {
7136
+ const a = patternParts[i];
7137
+ const b = pathParts[i];
7138
+ if (a.startsWith(":")) params[a.slice(1)] = b;
7139
+ else if (a !== b) return null;
7140
+ }
7141
+ return params;
7142
+ }
7143
+ function buildCapturingReply(envelope) {
7144
+ const wrapper = {
7145
+ status(code) {
7146
+ envelope.status = code;
7147
+ return wrapper;
7148
+ },
7149
+ code(code) {
7150
+ envelope.status = code;
7151
+ return wrapper;
7152
+ },
7153
+ send(data) {
7154
+ envelope.body = data;
7155
+ },
7156
+ redirect(url) {
7157
+ envelope.redirectUrl = url;
7158
+ if (envelope.status === 200) envelope.status = 302;
7159
+ },
7160
+ header(name, value) {
7161
+ envelope.headers[name.toLowerCase()] = value;
7162
+ return wrapper;
7163
+ },
7164
+ type(mime) {
7165
+ envelope.contentType = mime;
7166
+ return wrapper;
7167
+ }
7168
+ };
7169
+ return wrapper;
7170
+ }
7171
+ /**
7055
7172
  * Numeric day-of-week: 0 = Sunday … 6 = Saturday (matches `Date.getDay`).
7056
7173
  * Named `RecordingWeekday` to avoid collision with the string-union
7057
7174
  * `Weekday` exported from `interfaces/timezones.ts`.
@@ -23154,6 +23271,12 @@ Object.defineProperty(exports, "authProviderCapability", {
23154
23271
  return authProviderCapability;
23155
23272
  }
23156
23273
  });
23274
+ Object.defineProperty(exports, "buildAddonRouteProvider", {
23275
+ enumerable: true,
23276
+ get: function() {
23277
+ return buildAddonRouteProvider;
23278
+ }
23279
+ });
23157
23280
  Object.defineProperty(exports, "errMsg", {
23158
23281
  enumerable: true,
23159
23282
  get: function() {
@@ -7052,6 +7052,123 @@ var ConvertResultSchema = object({
7052
7052
  })).readonly()
7053
7053
  });
7054
7054
  /**
7055
+ * Build an `IAddonRouteProvider` from a list of routes. Implements
7056
+ * both the operator-facing `getRoutes` (returning route descriptors
7057
+ * minus the handlers, which can't cross JSON) and the framework-
7058
+ * private `invoke` method that the hub calls when this provider lives
7059
+ * in a forked worker.
7060
+ *
7061
+ * Co-located addons use the returned `getRoutes` directly because
7062
+ * their handlers don't need to cross any wire. The `invoke` method
7063
+ * is present anyway so the bridge code on the hub is uniform — it
7064
+ * doesn't need to switch on "local vs remote provider" at the call
7065
+ * site.
7066
+ *
7067
+ * Example:
7068
+ * const routes: IAddonHttpRoute[] = [
7069
+ * { method: 'GET', path: '/start', access: 'public', handler: this.handleStart },
7070
+ * ]
7071
+ * return [
7072
+ * {
7073
+ * capability: addonRoutesCapability,
7074
+ * provider: buildAddonRouteProvider('auth-oidc', routes),
7075
+ * },
7076
+ * ]
7077
+ */
7078
+ function buildAddonRouteProvider(id, routes) {
7079
+ return {
7080
+ id,
7081
+ getRoutes: () => routes,
7082
+ invoke: async (input) => {
7083
+ const match = matchRoute(routes, input.method, input.path);
7084
+ if (!match) return {
7085
+ status: 404,
7086
+ headers: {},
7087
+ redirectUrl: null,
7088
+ body: { error: `No route matches ${input.method} ${input.path}` }
7089
+ };
7090
+ const envelope = {
7091
+ status: 200,
7092
+ headers: {},
7093
+ redirectUrl: null
7094
+ };
7095
+ const reply = buildCapturingReply(envelope);
7096
+ const request = {
7097
+ params: {
7098
+ ...input.params,
7099
+ ...match.params
7100
+ },
7101
+ query: input.query,
7102
+ body: input.body,
7103
+ headers: input.headers,
7104
+ ...input.user ? { user: input.user } : {},
7105
+ ...input.scopedToken !== void 0 ? { scopedToken: input.scopedToken } : {}
7106
+ };
7107
+ await match.route.handler(request, reply);
7108
+ return envelope;
7109
+ }
7110
+ };
7111
+ }
7112
+ /**
7113
+ * Pattern matcher: same semantics as `AddonRouteRegistry.matchRoute`
7114
+ * but operating on a flat list and bypassing the `/addon/<id>/` prefix
7115
+ * — the bridge sends the post-prefix path directly so we don't need
7116
+ * to round-trip it through normalization.
7117
+ */
7118
+ function matchRoute(routes, method, path) {
7119
+ const normalizedMethod = method.toUpperCase();
7120
+ for (const route of routes) {
7121
+ if (route.method !== normalizedMethod) continue;
7122
+ const params = matchPath(route.path, path);
7123
+ if (params !== null) return {
7124
+ route,
7125
+ params
7126
+ };
7127
+ }
7128
+ return null;
7129
+ }
7130
+ function matchPath(pattern, p) {
7131
+ const patternParts = pattern.split("/").filter(Boolean);
7132
+ const pathParts = p.split("/").filter(Boolean);
7133
+ if (patternParts.length !== pathParts.length) return null;
7134
+ const params = {};
7135
+ for (let i = 0; i < patternParts.length; i++) {
7136
+ const a = patternParts[i];
7137
+ const b = pathParts[i];
7138
+ if (a.startsWith(":")) params[a.slice(1)] = b;
7139
+ else if (a !== b) return null;
7140
+ }
7141
+ return params;
7142
+ }
7143
+ function buildCapturingReply(envelope) {
7144
+ const wrapper = {
7145
+ status(code) {
7146
+ envelope.status = code;
7147
+ return wrapper;
7148
+ },
7149
+ code(code) {
7150
+ envelope.status = code;
7151
+ return wrapper;
7152
+ },
7153
+ send(data) {
7154
+ envelope.body = data;
7155
+ },
7156
+ redirect(url) {
7157
+ envelope.redirectUrl = url;
7158
+ if (envelope.status === 200) envelope.status = 302;
7159
+ },
7160
+ header(name, value) {
7161
+ envelope.headers[name.toLowerCase()] = value;
7162
+ return wrapper;
7163
+ },
7164
+ type(mime) {
7165
+ envelope.contentType = mime;
7166
+ return wrapper;
7167
+ }
7168
+ };
7169
+ return wrapper;
7170
+ }
7171
+ /**
7055
7172
  * Numeric day-of-week: 0 = Sunday … 6 = Saturday (matches `Date.getDay`).
7056
7173
  * Named `RecordingWeekday` to avoid collision with the string-union
7057
7174
  * `Weekday` exported from `interfaces/timezones.ts`.
@@ -23124,4 +23241,4 @@ object({
23124
23241
  schemaVersion: literal(1)
23125
23242
  });
23126
23243
  //#endregion
23127
- export { userPasskeysCapability as a, array as c, loginMethodCapability as i, object as l, addonWidgetsSourceCapability as n, errMsg as o, authProviderCapability as r, BaseAddon as s, addonRoutesCapability as t, string as u };
23244
+ export { loginMethodCapability as a, BaseAddon as c, string as d, buildAddonRouteProvider as i, array as l, addonWidgetsSourceCapability as n, userPasskeysCapability as o, authProviderCapability as r, errMsg as s, addonRoutesCapability as t, object as u };
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../chunk-Cek0wNdY.js");
6
- const require_dist = require("../dist-mmJDfwXE.js");
6
+ const require_dist = require("../dist-BLBS0d1z.js");
7
7
  //#region src/magic-link/auth-magic-link.addon.ts
8
8
  /**
9
9
  * Magic-link authentication addon.
@@ -30,6 +30,7 @@ const require_dist = require("../dist-mmJDfwXE.js");
30
30
  * - Auto-provision unknown emails (currently rejected for safety).
31
31
  */
32
32
  var DEFAULT_CONFIG = {
33
+ enabled: false,
33
34
  displayName: "Email magic link",
34
35
  icon: "mail",
35
36
  publicOrigin: "",
@@ -52,7 +53,7 @@ var AuthMagicLinkAddon = class extends require_dist.BaseAddon {
52
53
  throw new Error("Magic link does not use callback — the email link redirects to /api/auth/sso/finish directly");
53
54
  }
54
55
  };
55
- const routes = [
56
+ const routeProvider = require_dist.buildAddonRouteProvider("auth-magic-link", [
56
57
  {
57
58
  method: "GET",
58
59
  path: "/start",
@@ -74,11 +75,7 @@ var AuthMagicLinkAddon = class extends require_dist.BaseAddon {
74
75
  description: "Click target embedded in the magic-link email",
75
76
  handler: async (req, reply) => this.handleLogin(req, reply)
76
77
  }
77
- ];
78
- const routeProvider = {
79
- id: "auth-magic-link",
80
- getRoutes: () => routes
81
- };
78
+ ]);
82
79
  const loginMethodProvider = { getLoginMethods: async () => this.buildLoginMethods() };
83
80
  this.ctx.logger.info("Magic-link auth provider initialized", { meta: { deliveryMode: this.config.deliveryMode } });
84
81
  return [
@@ -97,6 +94,7 @@ var AuthMagicLinkAddon = class extends require_dist.BaseAddon {
97
94
  ];
98
95
  }
99
96
  buildLoginMethods() {
97
+ if (!this.config.enabled) return [];
100
98
  return [{
101
99
  kind: "redirect",
102
100
  id: "auth-magic-link",
@@ -113,6 +111,13 @@ var AuthMagicLinkAddon = class extends require_dist.BaseAddon {
113
111
  description: "Passwordless email login. The user types an email; the hub mails them a one-time link valid for the configured TTL.",
114
112
  columns: 1,
115
113
  fields: [
114
+ this.field({
115
+ type: "boolean",
116
+ key: "enabled",
117
+ label: "Enabled",
118
+ description: "Offer the magic-link button on the login page. Leave off until email delivery is configured.",
119
+ default: DEFAULT_CONFIG.enabled
120
+ }),
116
121
  this.field({
117
122
  type: "text",
118
123
  key: "displayName",
@@ -1,4 +1,4 @@
1
- import { i as loginMethodCapability, o as errMsg, r as authProviderCapability, s as BaseAddon, t as addonRoutesCapability } from "../dist-suALViLT.mjs";
1
+ import { a as loginMethodCapability, c as BaseAddon, i as buildAddonRouteProvider, r as authProviderCapability, s as errMsg, t as addonRoutesCapability } from "../dist-JUeAGU3c.mjs";
2
2
  //#region src/magic-link/auth-magic-link.addon.ts
3
3
  /**
4
4
  * Magic-link authentication addon.
@@ -25,6 +25,7 @@ import { i as loginMethodCapability, o as errMsg, r as authProviderCapability, s
25
25
  * - Auto-provision unknown emails (currently rejected for safety).
26
26
  */
27
27
  var DEFAULT_CONFIG = {
28
+ enabled: false,
28
29
  displayName: "Email magic link",
29
30
  icon: "mail",
30
31
  publicOrigin: "",
@@ -47,7 +48,7 @@ var AuthMagicLinkAddon = class extends BaseAddon {
47
48
  throw new Error("Magic link does not use callback — the email link redirects to /api/auth/sso/finish directly");
48
49
  }
49
50
  };
50
- const routes = [
51
+ const routeProvider = buildAddonRouteProvider("auth-magic-link", [
51
52
  {
52
53
  method: "GET",
53
54
  path: "/start",
@@ -69,11 +70,7 @@ var AuthMagicLinkAddon = class extends BaseAddon {
69
70
  description: "Click target embedded in the magic-link email",
70
71
  handler: async (req, reply) => this.handleLogin(req, reply)
71
72
  }
72
- ];
73
- const routeProvider = {
74
- id: "auth-magic-link",
75
- getRoutes: () => routes
76
- };
73
+ ]);
77
74
  const loginMethodProvider = { getLoginMethods: async () => this.buildLoginMethods() };
78
75
  this.ctx.logger.info("Magic-link auth provider initialized", { meta: { deliveryMode: this.config.deliveryMode } });
79
76
  return [
@@ -92,6 +89,7 @@ var AuthMagicLinkAddon = class extends BaseAddon {
92
89
  ];
93
90
  }
94
91
  buildLoginMethods() {
92
+ if (!this.config.enabled) return [];
95
93
  return [{
96
94
  kind: "redirect",
97
95
  id: "auth-magic-link",
@@ -108,6 +106,13 @@ var AuthMagicLinkAddon = class extends BaseAddon {
108
106
  description: "Passwordless email login. The user types an email; the hub mails them a one-time link valid for the configured TTL.",
109
107
  columns: 1,
110
108
  fields: [
109
+ this.field({
110
+ type: "boolean",
111
+ key: "enabled",
112
+ label: "Enabled",
113
+ description: "Offer the magic-link button on the login page. Leave off until email delivery is configured.",
114
+ default: DEFAULT_CONFIG.enabled
115
+ }),
111
116
  this.field({
112
117
  type: "text",
113
118
  key: "displayName",
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-Cek0wNdY.js");
6
- const require_dist = require("../dist-mmJDfwXE.js");
6
+ const require_dist = require("../dist-BLBS0d1z.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  node_crypto = require_chunk.__toESM(node_crypto);
9
9
  //#region node_modules/jose/dist/webapi/lib/buffer_utils.js
@@ -1237,7 +1237,7 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
1237
1237
  return this.handleCallback(code, state);
1238
1238
  }
1239
1239
  };
1240
- const routes = [{
1240
+ const routeProvider = require_dist.buildAddonRouteProvider("auth-oidc", [{
1241
1241
  method: "GET",
1242
1242
  path: "/:providerId/start",
1243
1243
  access: "public",
@@ -1249,11 +1249,7 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
1249
1249
  access: "public",
1250
1250
  description: "OIDC redirect callback for a configured provider",
1251
1251
  handler: async (req, reply) => this.handleCallbackRoute(req, reply)
1252
- }];
1253
- const routeProvider = {
1254
- id: "auth-oidc",
1255
- getRoutes: () => routes
1256
- };
1252
+ }]);
1257
1253
  const loginMethodProvider = { getLoginMethods: async () => this.buildLoginMethods() };
1258
1254
  this.ctx.logger.info("OIDC auth provider initialized", { meta: { providers: [...this.instances.keys()] } });
1259
1255
  return [
@@ -1,4 +1,4 @@
1
- import { i as loginMethodCapability, o as errMsg, r as authProviderCapability, s as BaseAddon, t as addonRoutesCapability } from "../dist-suALViLT.mjs";
1
+ import { a as loginMethodCapability, c as BaseAddon, i as buildAddonRouteProvider, r as authProviderCapability, s as errMsg, t as addonRoutesCapability } from "../dist-JUeAGU3c.mjs";
2
2
  import * as crypto$1 from "node:crypto";
3
3
  //#region node_modules/jose/dist/webapi/lib/buffer_utils.js
4
4
  var encoder = new TextEncoder();
@@ -1231,7 +1231,7 @@ var AuthOidcAddon = class extends BaseAddon {
1231
1231
  return this.handleCallback(code, state);
1232
1232
  }
1233
1233
  };
1234
- const routes = [{
1234
+ const routeProvider = buildAddonRouteProvider("auth-oidc", [{
1235
1235
  method: "GET",
1236
1236
  path: "/:providerId/start",
1237
1237
  access: "public",
@@ -1243,11 +1243,7 @@ var AuthOidcAddon = class extends BaseAddon {
1243
1243
  access: "public",
1244
1244
  description: "OIDC redirect callback for a configured provider",
1245
1245
  handler: async (req, reply) => this.handleCallbackRoute(req, reply)
1246
- }];
1247
- const routeProvider = {
1248
- id: "auth-oidc",
1249
- getRoutes: () => routes
1250
- };
1246
+ }]);
1251
1247
  const loginMethodProvider = { getLoginMethods: async () => this.buildLoginMethods() };
1252
1248
  this.ctx.logger.info("OIDC auth provider initialized", { meta: { providers: [...this.instances.keys()] } });
1253
1249
  return [
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-Cek0wNdY.js");
6
- const require_dist = require("../dist-mmJDfwXE.js");
6
+ const require_dist = require("../dist-BLBS0d1z.js");
7
7
  let stream = require("stream");
8
8
  stream = require_chunk.__toESM(stream, 1);
9
9
  let http = require("http");
@@ -1,4 +1,4 @@
1
- import { a as userPasskeysCapability, c as array, i as loginMethodCapability, l as object, n as addonWidgetsSourceCapability, s as BaseAddon, u as string } from "../dist-suALViLT.mjs";
1
+ import { a as loginMethodCapability, c as BaseAddon, d as string, l as array, n as addonWidgetsSourceCapability, o as userPasskeysCapability, u as object } from "../dist-JUeAGU3c.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import Stream from "stream";
4
4
  import http from "http";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-auth",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "CamStack authentication bundle — magic-link, OIDC, and WebAuthn/passkey. Multi-entry npm package shipping 3 addons under a single bundle; each addon keeps its own id, capabilities, and runner.",
5
5
  "keywords": [
6
6
  "camstack",