@_mustachio/openauth 0.7.0 → 0.8.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.
@@ -66,6 +66,13 @@ function issuer(input) {
66
66
  const allEncryption = lazy(() => encryptionKeys(storage));
67
67
  const signingKey = lazy(() => allSigning().then((all) => all[0]));
68
68
  const encryptionKey = lazy(() => allEncryption().then((all) => all[0]));
69
+ const getBasePath = (req) => {
70
+ if (!input.basePath)
71
+ return "";
72
+ if (typeof input.basePath === "string")
73
+ return input.basePath;
74
+ return input.basePath(req);
75
+ };
69
76
  const auth = {
70
77
  async success(ctx, properties, successOpts) {
71
78
  const authorization = await getAuthorization(ctx);
@@ -125,6 +132,7 @@ function issuer(input) {
125
132
  }, {
126
133
  provider: ctx.get("provider"),
127
134
  tenantId: authorization.tenantId,
135
+ context: ctx.get("requestContext"),
128
136
  ...properties
129
137
  }, ctx.req.raw);
130
138
  },
@@ -135,6 +143,7 @@ function issuer(input) {
135
143
  setCookie(ctx, key, await encrypt(value), {
136
144
  maxAge,
137
145
  httpOnly: true,
146
+ path: input.cookies?.path,
138
147
  ...ctx.req.url.startsWith("https://") ? { secure: true, sameSite: "None" } : {}
139
148
  });
140
149
  },
@@ -147,7 +156,7 @@ function issuer(input) {
147
156
  });
148
157
  },
149
158
  async unset(ctx, key) {
150
- deleteCookie(ctx, key);
159
+ deleteCookie(ctx, key, { path: input.cookies?.path });
151
160
  },
152
161
  async invalidate(subject) {
153
162
  const keys = await Array.fromAsync(Storage.scan(this.storage, ["oauth:refresh", subject]));
@@ -207,9 +216,16 @@ function issuer(input) {
207
216
  return JSON.parse(new TextDecoder().decode(await compactDecrypt(value, await encryptionKey().then((v) => v.private)).then((value2) => value2.plaintext)));
208
217
  }
209
218
  function issuer2(ctx) {
210
- return new URL(getRelativeUrl(ctx, "/")).origin;
219
+ const base = getBasePath(ctx.req.raw);
220
+ return new URL(getRelativeUrl(ctx, base || "/")).origin + base;
211
221
  }
212
222
  const app = new Hono().use(logger());
223
+ app.use(async (c, next) => {
224
+ if (input.context) {
225
+ c.set("requestContext", input.context(c.req.raw));
226
+ }
227
+ await next();
228
+ });
213
229
  const getProviders = async (c) => {
214
230
  if (typeof input.providers === "function") {
215
231
  return input.providers(c);
@@ -434,6 +450,7 @@ function issuer(input) {
434
450
  }
435
451
  }, {
436
452
  provider: provider.toString(),
453
+ context: c.get("requestContext"),
437
454
  ...response
438
455
  }, c.req.raw);
439
456
  }
@@ -480,11 +497,11 @@ function issuer(input) {
480
497
  await auth.set(c, "authorization", 60 * 60 * 24, authorization);
481
498
  c.set("authorization", authorization);
482
499
  if (provider)
483
- return c.redirect(`/${provider}/authorize`);
500
+ return c.redirect(`${getBasePath(c.req.raw)}/${provider}/authorize`);
484
501
  const resolvedProviders = await getProviders(c);
485
502
  const providerNames = Object.keys(resolvedProviders);
486
503
  if (providerNames.length === 1)
487
- return c.redirect(`/${providerNames[0]}/authorize`);
504
+ return c.redirect(`${getBasePath(c.req.raw)}/${providerNames[0]}/authorize`);
488
505
  return auth.forward(c, await select()(Object.fromEntries(Object.entries(resolvedProviders).map(([key, value]) => [
489
506
  key,
490
507
  value.type
@@ -537,11 +554,11 @@ function issuer(input) {
537
554
  await auth.set(c, "authorization", 60 * 60 * 24, authorization);
538
555
  c.set("authorization", authorization);
539
556
  if (provider)
540
- return c.redirect(`/tenant/${tenantId}/${provider}/authorize`);
557
+ return c.redirect(`${getBasePath(c.req.raw)}/tenant/${tenantId}/${provider}/authorize`);
541
558
  const resolvedProviders = await getProviders(c);
542
559
  const providerNames = Object.keys(resolvedProviders);
543
560
  if (providerNames.length === 1)
544
- return c.redirect(`/tenant/${tenantId}/${providerNames[0]}/authorize`);
561
+ return c.redirect(`${getBasePath(c.req.raw)}/tenant/${tenantId}/${providerNames[0]}/authorize`);
545
562
  return auth.forward(c, await select()(Object.fromEntries(Object.entries(resolvedProviders).map(([key, value]) => [
546
563
  key,
547
564
  value.type
@@ -109,6 +109,51 @@
109
109
  * })
110
110
  * ```
111
111
  *
112
+ * #### Advanced multi-tenant configuration
113
+ *
114
+ * For complex multi-tenant scenarios, OpenAuth provides three additional configuration options:
115
+ *
116
+ * - **`basePath`** - Dynamic URL prefix when the issuer is mounted at a variable path
117
+ * - **`cookies.path`** - Set to `"/"` so cookies work across all paths
118
+ * - **`context`** - Extract custom data from requests, available in providers and callbacks
119
+ *
120
+ * ```ts title="issuer.ts"
121
+ * const app = issuer({
122
+ * storage,
123
+ * subjects,
124
+ * // Dynamic base path based on request headers
125
+ * basePath: (req) => `/auth/${req.headers.get("x-org-slug")}`,
126
+ * // Root-level cookies that work across all paths
127
+ * cookies: { path: "/" },
128
+ * // Extract org/app context from each request
129
+ * context: (req) => ({
130
+ * orgSlug: req.headers.get("x-org-slug")!,
131
+ * appSlug: req.headers.get("x-app-slug")!,
132
+ * }),
133
+ * providers: async (ctx) => {
134
+ * const { orgSlug, appSlug } = ctx.get("requestContext") as { orgSlug: string; appSlug: string }
135
+ * const config = await db.getAppConfig(orgSlug, appSlug)
136
+ * return {
137
+ * github: GithubProvider({
138
+ * clientID: config.githubClientId,
139
+ * clientSecret: config.githubClientSecret,
140
+ * })
141
+ * }
142
+ * },
143
+ * success: async (ctx, value) => {
144
+ * const { orgSlug, appSlug } = value.context
145
+ * return ctx.subject("user", {
146
+ * userID: value.email,
147
+ * orgSlug,
148
+ * appSlug,
149
+ * })
150
+ * },
151
+ * })
152
+ *
153
+ * // Mount the issuer at a dynamic route
154
+ * honoApp.route("/auth/:orgSlug/:appSlug", app)
155
+ * ```
156
+ *
112
157
  * #### Deploy
113
158
  *
114
159
  * Since `issuer` is a Hono app, you can deploy it anywhere Hono supports.
@@ -215,9 +260,11 @@ export declare const aws: <E extends import("hono").Env = import("hono").Env, S
215
260
  headers: Record<string, string>;
216
261
  multiValueHeaders?: undefined;
217
262
  })>);
218
- export interface IssuerInput<Providers extends Record<string, Provider<any>>, Subjects extends SubjectSchema, Result = {
263
+ export interface IssuerInput<Providers extends Record<string, Provider<any>>, Subjects extends SubjectSchema, RequestContext = undefined, Result = {
219
264
  [key in keyof Providers]: Prettify<{
220
265
  provider: key;
266
+ tenantId?: string;
267
+ context: RequestContext;
221
268
  } & (Providers[key] extends Provider<infer T> ? T : {})>;
222
269
  }[keyof Providers]> {
223
270
  /**
@@ -492,19 +539,56 @@ export interface IssuerInput<Providers extends Record<string, Provider<any>>, Su
492
539
  * ```
493
540
  */
494
541
  allow?(input: AllowCallbackInput, req: Request): Promise<boolean>;
542
+ /**
543
+ * Cookie configuration options.
544
+ */
545
+ cookies?: {
546
+ /**
547
+ * Path for cookies. Defaults to the current path.
548
+ * Set to "/" for root-level cookies that work across all paths.
549
+ */
550
+ path?: string;
551
+ };
552
+ /**
553
+ * Extract custom context from the request. This context is available
554
+ * in providers via `ctx.get("requestContext")` and in callbacks via `input.context`.
555
+ *
556
+ * @example
557
+ * ```ts
558
+ * context: (req) => ({
559
+ * orgSlug: req.header("x-org-slug")!,
560
+ * appSlug: req.header("x-app-slug")!,
561
+ * }),
562
+ * ```
563
+ */
564
+ context?: (req: Request) => RequestContext;
565
+ /**
566
+ * Base path for all routes. Can be a static string or a function that
567
+ * receives the request and returns the path.
568
+ *
569
+ * @example
570
+ * ```ts
571
+ * basePath: "/auth/acme"
572
+ * // or
573
+ * basePath: (req) => `/auth/${req.headers.get("x-org-slug")}`
574
+ * ```
575
+ */
576
+ basePath?: string | ((req: Request) => string);
495
577
  }
496
578
  /**
497
579
  * Create an OpenAuth server, a Hono app.
498
580
  */
499
- export declare function issuer<Providers extends Record<string, Provider<any>>, Subjects extends SubjectSchema, Result = {
581
+ export declare function issuer<Providers extends Record<string, Provider<any>>, Subjects extends SubjectSchema, RequestContext = undefined, Result = {
500
582
  [key in keyof Providers]: Prettify<{
501
583
  provider: key;
502
584
  tenantId?: string;
585
+ context: RequestContext;
503
586
  } & (Providers[key] extends Provider<infer T> ? T : {})>;
504
- }[keyof Providers]>(input: IssuerInput<Providers, Subjects, Result>): import("hono/hono-base").HonoBase<{
587
+ }[keyof Providers]>(input: IssuerInput<Providers, Subjects, RequestContext, Result>): import("hono/hono-base").HonoBase<{
505
588
  Variables: {
506
589
  authorization: AuthorizationState;
507
590
  tenantId: string;
591
+ requestContext: RequestContext;
508
592
  };
509
593
  }, import("hono/types").BlankSchema, "/", "*">;
510
594
  //# sourceMappingURL=issuer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"issuer.d.ts","sourceRoot":"","sources":["../../src/issuer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"issuer.d.ts","sourceRoot":"","sources":["../../src/issuer.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@_mustachio/openauth",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/src/issuer.ts CHANGED
@@ -109,6 +109,51 @@
109
109
  * })
110
110
  * ```
111
111
  *
112
+ * #### Advanced multi-tenant configuration
113
+ *
114
+ * For complex multi-tenant scenarios, OpenAuth provides three additional configuration options:
115
+ *
116
+ * - **`basePath`** - Dynamic URL prefix when the issuer is mounted at a variable path
117
+ * - **`cookies.path`** - Set to `"/"` so cookies work across all paths
118
+ * - **`context`** - Extract custom data from requests, available in providers and callbacks
119
+ *
120
+ * ```ts title="issuer.ts"
121
+ * const app = issuer({
122
+ * storage,
123
+ * subjects,
124
+ * // Dynamic base path based on request headers
125
+ * basePath: (req) => `/auth/${req.headers.get("x-org-slug")}`,
126
+ * // Root-level cookies that work across all paths
127
+ * cookies: { path: "/" },
128
+ * // Extract org/app context from each request
129
+ * context: (req) => ({
130
+ * orgSlug: req.headers.get("x-org-slug")!,
131
+ * appSlug: req.headers.get("x-app-slug")!,
132
+ * }),
133
+ * providers: async (ctx) => {
134
+ * const { orgSlug, appSlug } = ctx.get("requestContext") as { orgSlug: string; appSlug: string }
135
+ * const config = await db.getAppConfig(orgSlug, appSlug)
136
+ * return {
137
+ * github: GithubProvider({
138
+ * clientID: config.githubClientId,
139
+ * clientSecret: config.githubClientSecret,
140
+ * })
141
+ * }
142
+ * },
143
+ * success: async (ctx, value) => {
144
+ * const { orgSlug, appSlug } = value.context
145
+ * return ctx.subject("user", {
146
+ * userID: value.email,
147
+ * orgSlug,
148
+ * appSlug,
149
+ * })
150
+ * },
151
+ * })
152
+ *
153
+ * // Mount the issuer at a dynamic route
154
+ * honoApp.route("/auth/:orgSlug/:appSlug", app)
155
+ * ```
156
+ *
112
157
  * #### Deploy
113
158
  *
114
159
  * Since `issuer` is a Hono app, you can deploy it anywhere Hono supports.
@@ -241,10 +286,13 @@ export const aws = awsHandle
241
286
  export interface IssuerInput<
242
287
  Providers extends Record<string, Provider<any>>,
243
288
  Subjects extends SubjectSchema,
289
+ RequestContext = undefined,
244
290
  Result = {
245
291
  [key in keyof Providers]: Prettify<
246
292
  {
247
293
  provider: key
294
+ tenantId?: string
295
+ context: RequestContext
248
296
  } & (Providers[key] extends Provider<infer T> ? T : {})
249
297
  >
250
298
  }[keyof Providers],
@@ -529,6 +577,41 @@ export interface IssuerInput<
529
577
  * ```
530
578
  */
531
579
  allow?(input: AllowCallbackInput, req: Request): Promise<boolean>
580
+ /**
581
+ * Cookie configuration options.
582
+ */
583
+ cookies?: {
584
+ /**
585
+ * Path for cookies. Defaults to the current path.
586
+ * Set to "/" for root-level cookies that work across all paths.
587
+ */
588
+ path?: string
589
+ }
590
+ /**
591
+ * Extract custom context from the request. This context is available
592
+ * in providers via `ctx.get("requestContext")` and in callbacks via `input.context`.
593
+ *
594
+ * @example
595
+ * ```ts
596
+ * context: (req) => ({
597
+ * orgSlug: req.header("x-org-slug")!,
598
+ * appSlug: req.header("x-app-slug")!,
599
+ * }),
600
+ * ```
601
+ */
602
+ context?: (req: Request) => RequestContext
603
+ /**
604
+ * Base path for all routes. Can be a static string or a function that
605
+ * receives the request and returns the path.
606
+ *
607
+ * @example
608
+ * ```ts
609
+ * basePath: "/auth/acme"
610
+ * // or
611
+ * basePath: (req) => `/auth/${req.headers.get("x-org-slug")}`
612
+ * ```
613
+ */
614
+ basePath?: string | ((req: Request) => string)
532
615
  }
533
616
 
534
617
  /**
@@ -537,15 +620,17 @@ export interface IssuerInput<
537
620
  export function issuer<
538
621
  Providers extends Record<string, Provider<any>>,
539
622
  Subjects extends SubjectSchema,
623
+ RequestContext = undefined,
540
624
  Result = {
541
625
  [key in keyof Providers]: Prettify<
542
626
  {
543
627
  provider: key
544
628
  tenantId?: string
629
+ context: RequestContext
545
630
  } & (Providers[key] extends Provider<infer T> ? T : {})
546
631
  >
547
632
  }[keyof Providers],
548
- >(input: IssuerInput<Providers, Subjects, Result>) {
633
+ >(input: IssuerInput<Providers, Subjects, RequestContext, Result>) {
549
634
  const error =
550
635
  input.error ??
551
636
  function (err) {
@@ -605,6 +690,12 @@ export function issuer<
605
690
  const signingKey = lazy(() => allSigning().then((all) => all[0]))
606
691
  const encryptionKey = lazy(() => allEncryption().then((all) => all[0]))
607
692
 
693
+ const getBasePath = (req: Request) => {
694
+ if (!input.basePath) return ""
695
+ if (typeof input.basePath === "string") return input.basePath
696
+ return input.basePath(req)
697
+ }
698
+
608
699
  const auth: Omit<ProviderOptions<any>, "name"> = {
609
700
  async success(ctx: Context, properties: any, successOpts) {
610
701
  const authorization = await getAuthorization(ctx)
@@ -681,6 +772,7 @@ export function issuer<
681
772
  {
682
773
  provider: ctx.get("provider"),
683
774
  tenantId: authorization.tenantId,
775
+ context: ctx.get("requestContext"),
684
776
  ...properties,
685
777
  },
686
778
  ctx.req.raw,
@@ -697,6 +789,7 @@ export function issuer<
697
789
  setCookie(ctx, key, await encrypt(value), {
698
790
  maxAge,
699
791
  httpOnly: true,
792
+ path: input.cookies?.path,
700
793
  ...(ctx.req.url.startsWith("https://")
701
794
  ? { secure: true, sameSite: "None" }
702
795
  : {}),
@@ -710,7 +803,7 @@ export function issuer<
710
803
  })
711
804
  },
712
805
  async unset(ctx: Context, key: string) {
713
- deleteCookie(ctx, key)
806
+ deleteCookie(ctx, key, { path: input.cookies?.path })
714
807
  },
715
808
  async invalidate(subject: string) {
716
809
  // Resolve the scan in case modifications interfere with iteration
@@ -828,16 +921,26 @@ export function issuer<
828
921
  }
829
922
 
830
923
  function issuer(ctx: Context) {
831
- return new URL(getRelativeUrl(ctx, "/")).origin
924
+ const base = getBasePath(ctx.req.raw)
925
+ return new URL(getRelativeUrl(ctx, base || "/")).origin + base
832
926
  }
833
927
 
834
928
  const app = new Hono<{
835
929
  Variables: {
836
930
  authorization: AuthorizationState
837
931
  tenantId: string
932
+ requestContext: RequestContext
838
933
  }
839
934
  }>().use(logger())
840
935
 
936
+ // Extract custom context from request if configured
937
+ app.use(async (c, next) => {
938
+ if (input.context) {
939
+ c.set("requestContext", input.context(c.req.raw))
940
+ }
941
+ await next()
942
+ })
943
+
841
944
  const getProviders = async (c: Context): Promise<Providers> => {
842
945
  if (typeof input.providers === "function") {
843
946
  return input.providers(c)
@@ -1169,6 +1272,7 @@ export function issuer<
1169
1272
  },
1170
1273
  {
1171
1274
  provider: provider.toString(),
1275
+ context: c.get("requestContext"),
1172
1276
  ...response,
1173
1277
  },
1174
1278
  c.req.raw,
@@ -1232,11 +1336,14 @@ export function issuer<
1232
1336
  throw new UnauthorizedClientError(client_id, redirect_uri)
1233
1337
  await auth.set(c, "authorization", 60 * 60 * 24, authorization)
1234
1338
  c.set("authorization", authorization)
1235
- if (provider) return c.redirect(`/${provider}/authorize`)
1339
+ if (provider)
1340
+ return c.redirect(`${getBasePath(c.req.raw)}/${provider}/authorize`)
1236
1341
  const resolvedProviders = await getProviders(c)
1237
1342
  const providerNames = Object.keys(resolvedProviders)
1238
1343
  if (providerNames.length === 1)
1239
- return c.redirect(`/${providerNames[0]}/authorize`)
1344
+ return c.redirect(
1345
+ `${getBasePath(c.req.raw)}/${providerNames[0]}/authorize`,
1346
+ )
1240
1347
  return auth.forward(
1241
1348
  c,
1242
1349
  await select()(
@@ -1313,11 +1420,16 @@ export function issuer<
1313
1420
  throw new UnauthorizedClientError(client_id, redirect_uri)
1314
1421
  await auth.set(c, "authorization", 60 * 60 * 24, authorization)
1315
1422
  c.set("authorization", authorization)
1316
- if (provider) return c.redirect(`/tenant/${tenantId}/${provider}/authorize`)
1423
+ if (provider)
1424
+ return c.redirect(
1425
+ `${getBasePath(c.req.raw)}/tenant/${tenantId}/${provider}/authorize`,
1426
+ )
1317
1427
  const resolvedProviders = await getProviders(c)
1318
1428
  const providerNames = Object.keys(resolvedProviders)
1319
1429
  if (providerNames.length === 1)
1320
- return c.redirect(`/tenant/${tenantId}/${providerNames[0]}/authorize`)
1430
+ return c.redirect(
1431
+ `${getBasePath(c.req.raw)}/tenant/${tenantId}/${providerNames[0]}/authorize`,
1432
+ )
1321
1433
  return auth.forward(
1322
1434
  c,
1323
1435
  await select()(