@adonis-agora/authkit-client 0.17.3 → 0.18.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.
@@ -0,0 +1,25 @@
1
+ import type { AuthkitClientManager } from './providers/authkit_client_provider.js';
2
+ /**
3
+ * SINGLE SOURCE of this package's container bindings.
4
+ *
5
+ * Deliberately separate from `types.ts`. `types.ts` augments
6
+ * `HttpContext.auth`, and that augmentation is the HOST'S to choose: an app that
7
+ * types its own `auth: Authenticator<AppUser>` cannot also receive the library's
8
+ * non-parameterised `auth: Authenticator` without the two merging into a
9
+ * conflict — one that `skipLibCheck` (on in every AdonisJS app) hides, leaving
10
+ * whichever declaration wins by include order.
11
+ *
12
+ * The container binding has no such problem: it names a key only this package
13
+ * owns. So it lives here, where the provider and the middleware can pull it in
14
+ * without dragging `HttpContext.auth` into the consumer's program.
15
+ *
16
+ * If you add an augmentation here, ask first whether a host could legitimately
17
+ * want to declare it differently. If yes, it belongs in `types.ts`, behind the
18
+ * host's explicit opt-in.
19
+ */
20
+ declare module '@adonisjs/core/types' {
21
+ interface ContainerBindings {
22
+ 'authkit.client': AuthkitClientManager;
23
+ }
24
+ }
25
+ export type { AuthkitClientManager };
@@ -0,0 +1 @@
1
+ export {};
package/build/index.d.ts CHANGED
@@ -19,6 +19,8 @@ export { getAuthkit } from './src/host/request_authenticator.js';
19
19
  export { isAuthkitAdmin, authkitDashboardAuthorize, authkitDashboardMiddleware, } from './src/host/dashboard_guard.js';
20
20
  export type { DashboardGuardOptions } from './src/host/dashboard_guard.js';
21
21
  export { default as AuthkitContextMiddleware } from './src/middleware/authkit_context_middleware.js';
22
+ export { default as RequireOrgMiddleware } from './src/middleware/require_org_middleware.js';
23
+ export type { RequireOrgMiddlewareOptions } from './src/middleware/require_org_middleware.js';
22
24
  export { lucidMirror } from './src/lucid_mirror.js';
23
25
  export type { LucidMirrorOptions } from './src/lucid_mirror.js';
24
26
  export { registerOidcClient } from './src/register_oidc_client.js';
package/build/index.js CHANGED
@@ -11,6 +11,7 @@ export { authkitClientGuard, AuthkitClientGuard } from './src/host/authkit_clien
11
11
  export { getAuthkit } from './src/host/request_authenticator.js';
12
12
  export { isAuthkitAdmin, authkitDashboardAuthorize, authkitDashboardMiddleware, } from './src/host/dashboard_guard.js';
13
13
  export { default as AuthkitContextMiddleware } from './src/middleware/authkit_context_middleware.js';
14
+ export { default as RequireOrgMiddleware } from './src/middleware/require_org_middleware.js';
14
15
  export { lucidMirror } from './src/lucid_mirror.js';
15
16
  export { registerOidcClient } from './src/register_oidc_client.js';
16
17
  export { validateLogoutToken, InvalidLogoutTokenError, InMemorySessionIndex, BACKCHANNEL_LOGOUT_EVENT, } from './src/backchannel_logout.js';
@@ -1,3 +1,4 @@
1
+ import '../container_bindings.js';
1
2
  import type { HttpContext } from '@adonisjs/core/http';
2
3
  import type { ApplicationService } from '@adonisjs/core/types';
3
4
  import { Authenticator } from '../src/authenticator.js';
@@ -107,11 +108,6 @@ export declare class AuthkitClientManager {
107
108
  startSession(ctx: HttpContext, tokenSet: TokenSet): void;
108
109
  createAuthenticator(ctx: HttpContext): Promise<Authenticator>;
109
110
  }
110
- declare module '@adonisjs/core/types' {
111
- interface ContainerBindings {
112
- 'authkit.client': AuthkitClientManager;
113
- }
114
- }
115
111
  export default class AuthkitClientProvider {
116
112
  protected app: ApplicationService;
117
113
  constructor(app: ApplicationService);
@@ -1,3 +1,6 @@
1
+ // Single source of this package's container bindings (see container_bindings.ts).
2
+ // A bare side-effect import so the declaration survives into the emitted `.d.ts`.
3
+ import '../container_bindings.js';
1
4
  import { randomUUID } from 'node:crypto';
2
5
  import { configProvider } from '@adonisjs/core';
3
6
  import { RuntimeException } from '@adonisjs/core/exceptions';
@@ -0,0 +1,6 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ import type { Authenticator } from '../authenticator.js';
3
+ /** Read the authenticator the middleware placed on the context. */
4
+ export declare function ctxAuth(ctx: HttpContext): Authenticator;
5
+ /** Place the authenticator on the context. */
6
+ export declare function setCtxAuth(ctx: HttpContext, authenticator: Authenticator): void;
@@ -0,0 +1,8 @@
1
+ /** Read the authenticator the middleware placed on the context. */
2
+ export function ctxAuth(ctx) {
3
+ return ctx.auth;
4
+ }
5
+ /** Place the authenticator on the context. */
6
+ export function setCtxAuth(ctx, authenticator) {
7
+ ctx.auth = authenticator;
8
+ }
@@ -1,10 +1,11 @@
1
+ import { ctxAuth } from '../host/ctx_auth.js';
1
2
  /**
2
3
  * Middleware de autenticação do AuthKit: exige sessão válida. Autorização por role saiu daqui — use
3
4
  * `@adonis-agora/authz` (ou Bouncer) na rota. AuthKit só autentica.
4
5
  */
5
6
  export default class AuthkitAuthMiddleware {
6
7
  async handle(ctx, next, options = {}) {
7
- const ok = await ctx.auth.check();
8
+ const ok = await ctxAuth(ctx).check();
8
9
  if (!ok)
9
10
  return ctx.response.redirect(options.redirectTo ?? '/auth/login');
10
11
  return next();
@@ -1,4 +1,4 @@
1
- import '../../types.js';
1
+ import '../../container_bindings.js';
2
2
  import type { HttpContext } from '@adonisjs/core/http';
3
3
  import type { NextFn } from '@adonisjs/core/types/http';
4
4
  export default class AuthkitMiddleware {
@@ -1,22 +1,24 @@
1
- // Side-effect import: pulls the `HttpContext.auth` module augmentation (declared
2
- // in the package root `types.ts`) into this module's declaration graph, so an app
3
- // that registers this middleware gets `ctx.auth` typed as AuthKit's `Authenticator`.
4
- // It lives here — and not in `index.ts` — because this middleware is what actually
5
- // assigns `ctx.auth`. Apps that instead hand `ctx.auth` to `@adonisjs/auth` via
6
- // `authkitClientGuard` never register this middleware, so the two owners of
7
- // `ctx.auth` can never collide. Same pattern as `@adonisjs/auth`, which augments
8
- // `HttpContext` from its own `initialize_auth_middleware`.
9
- // It must be a bare side-effect import, not `import type {} from`: the type-only form
10
- // is erased from the emitted `.d.ts` as well, which would drop the augmentation again.
11
- // The runtime cost is nil — `build/types.js` is this package's own file and compiles
12
- // down to `export {}`.
13
- import '../../types.js';
1
+ // Side-effect import: carries this package's `ContainerBindings` declaration into
2
+ // the emitted `.d.ts`, so `containerResolver.make('authkit.client')` is typed in a
3
+ // consumer's program. It must be a bare side-effect import — the
4
+ // `import type {} from` form is erased from the declaration output.
5
+ //
6
+ // It points at `container_bindings.js`, NOT at the root `types.js`. `types.js`
7
+ // also augments `HttpContext.auth`, and importing it here pushed that
8
+ // augmentation into every app that registers this middleware, where it collided
9
+ // with the app's own `auth: Authenticator<AppUser>`. `skipLibCheck` hid the
10
+ // conflict and the library's non-parameterised declaration won, collapsing
11
+ // `auth.getUser()` to `unknown` across the consumer's codebase. Typing `ctx.auth`
12
+ // is the host's decision, taken by importing
13
+ // `@adonis-agora/authkit-client/types`; it is not this middleware's to impose.
14
+ import '../../container_bindings.js';
15
+ import { setCtxAuth } from '../host/ctx_auth.js';
14
16
  export default class AuthkitMiddleware {
15
17
  async handle(ctx, next) {
16
18
  const manager = await ctx.containerResolver.make('authkit.client');
17
19
  // Renova proativamente o TokenSet (rotação de refresh token) antes de resolver.
18
20
  await manager.maybeRefresh(ctx);
19
- ctx.auth = await manager.createAuthenticator(ctx);
21
+ setCtxAuth(ctx, await manager.createAuthenticator(ctx));
20
22
  return next();
21
23
  }
22
24
  }
@@ -0,0 +1,47 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ import type { NextFn } from '@adonisjs/core/types/http';
3
+ export interface RequireOrgMiddlewareOptions {
4
+ /**
5
+ * Para onde mandar quem não tem org ativa. Default: `/account/orgs` — a tela
6
+ * de seleção que o host-kit do authkit-server já serve.
7
+ */
8
+ redirectTo?: string;
9
+ /**
10
+ * `web` (default) redireciona para {@link redirectTo}; `api` responde 403 com
11
+ * um corpo JSON — rotas de API não têm para onde redirecionar.
12
+ */
13
+ mode?: 'web' | 'api';
14
+ /**
15
+ * Restringe às orgs listadas. Útil para rotas que só existem dentro de um
16
+ * conjunto conhecido de tenants. Ausente → qualquer org ativa serve.
17
+ */
18
+ oneOf?: string[];
19
+ }
20
+ /**
21
+ * Exige que a sessão tenha uma **organização ativa** (claim `org_id`, exposta
22
+ * como `identity.orgId`).
23
+ *
24
+ * Existe porque num app multi-tenant "estar autenticado" e "estar dentro de um
25
+ * tenant" são coisas diferentes: sem esta barreira, uma request de um usuário
26
+ * logado mas sem org ativa chega no controller com `tenantId` indefinido, e daí
27
+ * o escopo por tenant do `@adonis-agora/authz` cai no escopo GLOBAL — que é o
28
+ * pior lugar para se cair por acidente. Fail-closed: sem sessão ou sem org, não
29
+ * passa.
30
+ *
31
+ * Registre como named middleware e encadeie DEPOIS do de auth:
32
+ *
33
+ * ```ts title="start/kernel.ts"
34
+ * export const middleware = router.named({
35
+ * auth: () => import('@adonis-agora/authkit-client/auth_middleware'),
36
+ * requireOrg: () => import('@adonis-agora/authkit-client/require_org_middleware'),
37
+ * })
38
+ * ```
39
+ *
40
+ * ```ts title="start/routes.ts"
41
+ * router.group(() => { … }).use([middleware.auth(), middleware.requireOrg()])
42
+ * router.group(() => { … }).use([middleware.auth(), middleware.requireOrg({ mode: 'api' })])
43
+ * ```
44
+ */
45
+ export default class RequireOrgMiddleware {
46
+ handle(ctx: HttpContext, next: NextFn, options?: RequireOrgMiddlewareOptions): Promise<any>;
47
+ }
@@ -0,0 +1,39 @@
1
+ import { ctxAuth } from '../host/ctx_auth.js';
2
+ /**
3
+ * Exige que a sessão tenha uma **organização ativa** (claim `org_id`, exposta
4
+ * como `identity.orgId`).
5
+ *
6
+ * Existe porque num app multi-tenant "estar autenticado" e "estar dentro de um
7
+ * tenant" são coisas diferentes: sem esta barreira, uma request de um usuário
8
+ * logado mas sem org ativa chega no controller com `tenantId` indefinido, e daí
9
+ * o escopo por tenant do `@adonis-agora/authz` cai no escopo GLOBAL — que é o
10
+ * pior lugar para se cair por acidente. Fail-closed: sem sessão ou sem org, não
11
+ * passa.
12
+ *
13
+ * Registre como named middleware e encadeie DEPOIS do de auth:
14
+ *
15
+ * ```ts title="start/kernel.ts"
16
+ * export const middleware = router.named({
17
+ * auth: () => import('@adonis-agora/authkit-client/auth_middleware'),
18
+ * requireOrg: () => import('@adonis-agora/authkit-client/require_org_middleware'),
19
+ * })
20
+ * ```
21
+ *
22
+ * ```ts title="start/routes.ts"
23
+ * router.group(() => { … }).use([middleware.auth(), middleware.requireOrg()])
24
+ * router.group(() => { … }).use([middleware.auth(), middleware.requireOrg({ mode: 'api' })])
25
+ * ```
26
+ */
27
+ export default class RequireOrgMiddleware {
28
+ async handle(ctx, next, options = {}) {
29
+ const identity = await ctxAuth(ctx).getIdentity();
30
+ const orgId = identity?.orgId ?? null;
31
+ const allowed = orgId !== null && (!options.oneOf || options.oneOf.includes(orgId));
32
+ if (allowed)
33
+ return next();
34
+ if (options.mode === 'api') {
35
+ return ctx.response.abort({ error: 'no_active_organization', message: 'This route requires an active organization.' }, 403);
36
+ }
37
+ return ctx.response.redirect(options.redirectTo ?? '/account/orgs');
38
+ }
39
+ }
@@ -1,3 +1,4 @@
1
+ import { ctxAuth } from '../host/ctx_auth.js';
1
2
  /**
2
3
  * Resolve a identidade da sessão (popula `ctx.auth`) SEM exigir login — útil em
3
4
  * rotas públicas que mostram estado logado/deslogado. Nunca redireciona.
@@ -8,7 +9,7 @@
8
9
  */
9
10
  export default class AuthkitSilentAuthMiddleware {
10
11
  async handle(ctx, next) {
11
- await ctx.auth.check();
12
+ await ctxAuth(ctx).check();
12
13
  return next();
13
14
  }
14
15
  }
@@ -1,3 +1,4 @@
1
+ import { deriveOrgId } from '@adonis-agora/authkit-core';
1
2
  /**
2
3
  * Ponte estrutural (best-effort) entre a sessão resolvida do authkit e o
3
4
  * contexto do Agora. Lê o slot global `@agora/context:set` de forma ESTRUTURAL —
@@ -6,26 +7,14 @@
6
7
  */
7
8
  const SET_SLOT = Symbol.for('@agora/context:set');
8
9
  /**
9
- * Claims OIDC candidatas a "organização/tenant ativo". A primeira presente (e
10
- * string não vazia) vira o `tenantId`. Não falha se nenhuma existir.
10
+ * Deriva o tenant ativo da identidade. Prefere o `orgId` já resolvido (o caminho
11
+ * normal — `buildIdentityFromClaims` o preenche); cai para as claims cruas
12
+ * quando a `Identity` veio de um `SessionResolver` customizado que não o
13
+ * populou. A tabela de claims aceitas vive em `@adonis-agora/authkit-core`
14
+ * (`ORG_ID_CLAIMS`) para não divergir entre a identidade e esta ponte.
11
15
  */
12
- const TENANT_CLAIMS = [
13
- 'active_organization_id',
14
- 'org_id',
15
- 'organization_id',
16
- 'tenant_id',
17
- 'tid',
18
- ];
19
- /** Deriva o tenant ativo das claims cruas, ou `undefined` quando ausente. */
20
- function deriveTenant(raw) {
21
- if (!raw)
22
- return undefined;
23
- for (const claim of TENANT_CLAIMS) {
24
- const value = raw[claim];
25
- if (typeof value === 'string' && value.length > 0)
26
- return value;
27
- }
28
- return undefined;
16
+ function deriveTenant(identity) {
17
+ return identity.orgId ?? deriveOrgId(identity.raw) ?? undefined;
29
18
  }
30
19
  /**
31
20
  * Popula o contexto do Agora a partir de uma identidade recém-resolvida:
@@ -38,7 +27,7 @@ export function populateContext(identity) {
38
27
  const set = globalThis[SET_SLOT];
39
28
  if (!set)
40
29
  return;
41
- const tenant = deriveTenant(identity.raw);
30
+ const tenant = deriveTenant(identity);
42
31
  set({
43
32
  userRef: { type: 'user', id: identity.userId },
44
33
  globalRoles: identity.globalRoles,
@@ -8,7 +8,9 @@ type FetchImpl = (url: string, init: any) => Promise<{
8
8
  * resolvers `jwt`, `pat` e `opaque`. Espelha exatamente o que o `JwtResolver`
9
9
  * produzia (a referência canônica): `sub`→userId, `email`, papéis na
10
10
  * `globalRolesClaim`, `name`/`picture`→profile, `sid`→sessionId, `iat`/`exp`
11
- * (segundos→ms, fallback 0) e as claims cruas em `raw`.
11
+ * (segundos→ms, fallback 0), a org/tenant ativo (`orgId`/`orgSlug`/`orgRole`,
12
+ * ver `deriveOrgId` — vale para o authkit E para IdPs de terceiros) e as claims
13
+ * cruas em `raw`.
12
14
  */
13
15
  export declare function buildIdentityFromClaims(claims: Record<string, unknown>, globalRolesClaim: string): Identity;
14
16
  export interface IntrospectAuth {
@@ -1,9 +1,12 @@
1
+ import { deriveOrgId, deriveOrgRole, deriveOrgSlug } from '@adonis-agora/authkit-core';
1
2
  /**
2
3
  * Mapeamento canônico de claims OIDC validadas → Identity, compartilhado pelos
3
4
  * resolvers `jwt`, `pat` e `opaque`. Espelha exatamente o que o `JwtResolver`
4
5
  * produzia (a referência canônica): `sub`→userId, `email`, papéis na
5
6
  * `globalRolesClaim`, `name`/`picture`→profile, `sid`→sessionId, `iat`/`exp`
6
- * (segundos→ms, fallback 0) e as claims cruas em `raw`.
7
+ * (segundos→ms, fallback 0), a org/tenant ativo (`orgId`/`orgSlug`/`orgRole`,
8
+ * ver `deriveOrgId` — vale para o authkit E para IdPs de terceiros) e as claims
9
+ * cruas em `raw`.
7
10
  */
8
11
  export function buildIdentityFromClaims(claims, globalRolesClaim) {
9
12
  const roles = claims[globalRolesClaim];
@@ -16,6 +19,9 @@ export function buildIdentityFromClaims(claims, globalRolesClaim) {
16
19
  avatarUrl: typeof claims.picture === 'string' ? claims.picture : undefined,
17
20
  },
18
21
  sessionId: typeof claims.sid === 'string' ? claims.sid : undefined,
22
+ orgId: deriveOrgId(claims),
23
+ orgSlug: deriveOrgSlug(claims),
24
+ orgRole: deriveOrgRole(claims),
19
25
  issuedAt: typeof claims.iat === 'number' ? claims.iat * 1000 : 0,
20
26
  expiresAt: typeof claims.exp === 'number' ? claims.exp * 1000 : 0,
21
27
  raw: claims,
package/build/types.d.ts CHANGED
@@ -1,13 +1,43 @@
1
+ import './container_bindings.js';
1
2
  import type { AuthkitClientManager } from './providers/authkit_client_provider.js';
2
3
  import type { Authenticator } from './src/authenticator.js';
4
+ /**
5
+ * OPT-IN typing for `ctx.auth`.
6
+ *
7
+ * Reference this module from your app when you want `ctx.auth` typed as
8
+ * AuthKit's `Authenticator` and you are NOT declaring the property yourself:
9
+ *
10
+ * ```ts
11
+ * // any file in your app, e.g. app/types.ts
12
+ * import '@adonis-agora/authkit-client/types'
13
+ * ```
14
+ *
15
+ * NOTHING in this package imports this module on your behalf, and that is the
16
+ * point. `Authenticator` is generic (`Authenticator<TUser>`), and most apps
17
+ * declare their own augmentation so `auth.getUser()` returns their user model:
18
+ *
19
+ * ```ts
20
+ * declare module '@adonisjs/core/http' {
21
+ * interface HttpContext {
22
+ * auth: Authenticator<AppUser>
23
+ * }
24
+ * }
25
+ * ```
26
+ *
27
+ * Two declarations of the same property with different types do not merge —
28
+ * they conflict, and because `skipLibCheck` is on in every AdonisJS app the
29
+ * conflict is never reported: one simply wins by include order. When the
30
+ * library's won, `auth.getUser()` collapsed to `unknown` across the whole app.
31
+ * So the library ships the declaration and lets you decide whether to load it,
32
+ * rather than forcing it through a middleware import.
33
+ *
34
+ * The container bindings come along with it (see `container_bindings.ts`); they
35
+ * are also available on their own, which is what the provider and the middleware
36
+ * use.
37
+ */
3
38
  declare module '@adonisjs/core/http' {
4
39
  interface HttpContext {
5
40
  auth: Authenticator;
6
41
  }
7
42
  }
8
- declare module '@adonisjs/core/types' {
9
- interface ContainerBindings {
10
- 'authkit.client': AuthkitClientManager;
11
- }
12
- }
13
43
  export type { Authenticator, AuthkitClientManager };
package/build/types.js CHANGED
@@ -1 +1 @@
1
- export {};
1
+ import './container_bindings.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-client",
3
- "version": "0.17.3",
3
+ "version": "0.18.0",
4
4
  "description": "AdonisJS OIDC/OAuth2 relying-party (client) toolkit: session-based authentication against an OpenID Connect identity provider, with JWT/PAT user resolvers and OpenTelemetry metrics.",
5
5
  "license": "MIT",
6
6
  "author": "dudousxd",
@@ -36,6 +36,7 @@
36
36
  "exports": {
37
37
  ".": "./build/index.js",
38
38
  "./types": "./build/types.js",
39
+ "./container_bindings": "./build/container_bindings.js",
39
40
  "./services/main": "./build/services/main.js",
40
41
  "./authkit_client_provider": "./build/providers/authkit_client_provider.js",
41
42
  "./auth": "./build/src/host/authkit_client_guard.js",
@@ -44,6 +45,7 @@
44
45
  "./authkit_context_middleware": "./build/src/middleware/authkit_context_middleware.js",
45
46
  "./backchannel_revocation_middleware": "./build/src/middleware/backchannel_revocation_middleware.js",
46
47
  "./auth_middleware": "./build/src/middleware/auth_middleware.js",
48
+ "./require_org_middleware": "./build/src/middleware/require_org_middleware.js",
47
49
  "./silent_auth_middleware": "./build/src/middleware/silent_auth_middleware.js",
48
50
  "./commands": "./build/commands/configure.js"
49
51
  },
@@ -76,7 +78,7 @@
76
78
  },
77
79
  "dependencies": {
78
80
  "jose": "5.10.0",
79
- "@adonis-agora/authkit-core": "0.7.1"
81
+ "@adonis-agora/authkit-core": "0.8.0"
80
82
  },
81
83
  "devDependencies": {
82
84
  "@adonisjs/auth": "10.1.0",
@@ -91,7 +93,7 @@
91
93
  "ioredis-mock": "8.13.1",
92
94
  "@types/node": "25.9.1",
93
95
  "typescript": "6.0.3",
94
- "@adonis-agora/authkit-server": "0.58.1"
96
+ "@adonis-agora/authkit-server": "0.59.0"
95
97
  },
96
98
  "scripts": {
97
99
  "build": "tsc && node -e \"require('node:fs').cpSync('stubs','build/stubs',{recursive:true,filter:(s)=>!s.endsWith('.ts')})\"",