@intelligo-dev/auth 1.0.0-beta.1 → 1.0.0-beta.13

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.
Files changed (81) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +59 -0
  3. package/dist/client.js +6 -16
  4. package/dist/client.js.map +1 -1
  5. package/dist/edge.js +22 -28
  6. package/dist/edge.js.map +1 -1
  7. package/dist/guard-error.js +23 -0
  8. package/dist/guard-error.js.map +1 -0
  9. package/dist/helpers.js +77 -97
  10. package/dist/helpers.js.map +1 -1
  11. package/dist/impersonation.js +46 -18
  12. package/dist/impersonation.js.map +1 -1
  13. package/dist/index.js +21 -27
  14. package/dist/index.js.map +1 -1
  15. package/dist/invitation-links.js +13 -0
  16. package/dist/invitation-links.js.map +1 -0
  17. package/dist/onboarding/errors.js +4 -14
  18. package/dist/onboarding/errors.js.map +1 -1
  19. package/dist/onboarding/schemas.js +3 -15
  20. package/dist/onboarding/schemas.js.map +1 -1
  21. package/dist/onboarding/service.js +14 -68
  22. package/dist/onboarding/service.js.map +1 -1
  23. package/dist/org-api.js +10 -67
  24. package/dist/org-api.js.map +1 -1
  25. package/dist/profile/errors.js +4 -14
  26. package/dist/profile/errors.js.map +1 -1
  27. package/dist/profile/schemas.js +1 -10
  28. package/dist/profile/schemas.js.map +1 -1
  29. package/dist/profile/service.js +12 -53
  30. package/dist/profile/service.js.map +1 -1
  31. package/dist/roles.js +28 -5
  32. package/dist/roles.js.map +1 -1
  33. package/dist/server.js +80 -98
  34. package/dist/server.js.map +1 -1
  35. package/dist/team/errors.js +4 -13
  36. package/dist/team/errors.js.map +1 -1
  37. package/dist/team/schemas.js +4 -18
  38. package/dist/team/schemas.js.map +1 -1
  39. package/dist/team/service.js +80 -157
  40. package/dist/team/service.js.map +1 -1
  41. package/dist/trusted-origins.js +25 -0
  42. package/dist/trusted-origins.js.map +1 -0
  43. package/dist/workspace/errors.js +4 -14
  44. package/dist/workspace/errors.js.map +1 -1
  45. package/dist/workspace/schemas.js +2 -18
  46. package/dist/workspace/schemas.js.map +1 -1
  47. package/dist/workspace/service.js +31 -91
  48. package/dist/workspace/service.js.map +1 -1
  49. package/dist/workspace-bootstrap.js +33 -0
  50. package/dist/workspace-bootstrap.js.map +1 -0
  51. package/dist/workspace-init.js +46 -51
  52. package/dist/workspace-init.js.map +1 -1
  53. package/dist/workspace-slug.js +29 -0
  54. package/dist/workspace-slug.js.map +1 -0
  55. package/package.json +36 -14
  56. package/src/client.ts +6 -16
  57. package/src/edge.ts +22 -28
  58. package/src/guard-error.ts +36 -0
  59. package/src/helpers.ts +99 -114
  60. package/src/impersonation.ts +52 -17
  61. package/src/index.ts +14 -10
  62. package/src/invitation-links.ts +15 -0
  63. package/src/onboarding/errors.ts +5 -17
  64. package/src/onboarding/schemas.ts +3 -15
  65. package/src/onboarding/service.ts +11 -65
  66. package/src/org-api.ts +9 -66
  67. package/src/profile/errors.ts +5 -17
  68. package/src/profile/schemas.ts +1 -10
  69. package/src/profile/service.ts +8 -49
  70. package/src/roles.ts +36 -5
  71. package/src/server.ts +86 -104
  72. package/src/team/errors.ts +4 -13
  73. package/src/team/schemas.ts +4 -18
  74. package/src/team/service.ts +100 -174
  75. package/src/trusted-origins.ts +26 -0
  76. package/src/workspace/errors.ts +4 -14
  77. package/src/workspace/schemas.ts +2 -18
  78. package/src/workspace/service.ts +40 -92
  79. package/src/workspace-bootstrap.ts +57 -0
  80. package/src/workspace-init.ts +55 -56
  81. package/src/workspace-slug.ts +32 -0
package/NOTICE ADDED
@@ -0,0 +1,6 @@
1
+ Intelligo
2
+ Copyright 2026 Turtuvshin Byambaa and the Intelligo contributors
3
+
4
+ This product is licensed under the Apache License, Version 2.0 (see
5
+ LICENSE). The Intelligo name and logo are trademarks; see TRADEMARK.md
6
+ in the source repository, https://github.com/intelligo-dev/intelligo.
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # @intelligo-dev/auth
2
+
3
+ Multi-tenant authentication on Better-Auth: sign-up, workspaces, invitations and role-based access control.
4
+
5
+ Part of [Intelligo](https://intelligo.dev), an application framework and
6
+ operational platform for vertical AI SaaS products. Every `@intelligo-dev/*`
7
+ package is released at one version and shares one database schema;
8
+ `pnpm dlx @intelligo-dev/cli@beta create my-app` installs the set an
9
+ application needs. Documentation:
10
+ [intelligo.dev/docs/packages/auth](https://intelligo.dev/docs/packages/auth).
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pnpm add @intelligo-dev/auth@beta better-auth drizzle-orm zod
16
+ ```
17
+
18
+ `better-auth`, `drizzle-orm` and `zod` are peers: the guards read the
19
+ Better-Auth instance and the database client the application already has.
20
+
21
+ ## What it owns
22
+
23
+ Sessions and accounts, organizations as workspaces, membership roles
24
+ (`owner` / `admin` / `member`), invitations, platform-admin impersonation, and
25
+ the guards every transport starts with: `requireAuth`, `requireWorkspace`,
26
+ `requireRole`, `requirePlatformAdmin`.
27
+
28
+ The team, workspace, profile and onboarding services live here too, behind
29
+ ports the composition root binds — auth never imports billing.
30
+
31
+ ## Use
32
+
33
+ ```ts
34
+ import { requireWorkspace } from "@intelligo-dev/auth";
35
+
36
+ const { user, workspace, membership } = await requireWorkspace();
37
+ ```
38
+
39
+ A guard that refuses throws an `AuthGuardError`; `isAuthGuardError(error)`
40
+ narrows it and `error.code` picks the status: `unauthenticated` (no session,
41
+ 401), `no_workspace` (signed in, member of no workspace) or `forbidden`
42
+ (lacking the role, 403).
43
+
44
+ `impersonateUser` checks that the caller is a platform admin and that the
45
+ target is not one before it swaps the session. Products call
46
+ `startImpersonation` from `@intelligo-dev/admin`, which adds the audit event
47
+ and the mandatory reason.
48
+
49
+ `hasSessionCookie(request)` from `@intelligo-dev/auth/edge` is for an optional
50
+ optimistic redirect in a consumer's `proxy.ts`. It reads cookie presence only
51
+ and is never authorization; the scaffold does not use it.
52
+
53
+ Platform admin is a row (`users.role`), not an environment variable. The
54
+ allowlist in `PLATFORM_ADMIN_EMAILS` is promoted into that column on first use,
55
+ so the plugin and the guard cannot disagree.
56
+
57
+ ## Licence
58
+
59
+ Apache-2.0
package/dist/client.js CHANGED
@@ -1,25 +1,15 @@
1
1
  /**
2
- * Better-Auth Client
3
- *
4
- * Client-side authentication utilities for React components.
5
- * This module uses "better-auth/react" which is a client-side module.
6
- *
7
- * IMPORTANT: Only import this in "use client" components.
8
- * For server-side auth, use @intelligo-dev/auth (server.ts)
2
+ * The Better-Auth React client. Import only from "use client" components;
3
+ * server code uses `@intelligo-dev/auth`.
9
4
  */
10
5
  import { createAuthClient } from "better-auth/react";
11
6
  import { organizationClient } from "better-auth/client/plugins";
12
- // No `baseURL`: the client and the Better-Auth server always live on the
13
- // same origin (this app's own `/api/auth/*` routes), so Better-Auth's
14
- // client defaults to `window.location.origin` and every request is
15
- // same-origin. Do not reintroduce an absolute `NEXT_PUBLIC_APP_URL`
16
- // baseURL here — the dev port has drifted before (3000 -> 3001 -> 4000)
17
- // and an absolute cross-origin URL is blocked by the app's CSP
18
- // (`connect-src 'self'`) whenever it doesn't match the port the app is
19
- // actually served on.
7
+ // No `baseURL`: the client and server share an origin (`/api/auth/*`), so
8
+ // Better-Auth defaults to `window.location.origin`. An absolute URL is
9
+ // blocked by the app's CSP (`connect-src 'self'`) whenever its port differs
10
+ // from the one the app is served on.
20
11
  export const authClient = createAuthClient({
21
12
  plugins: [organizationClient()],
22
13
  });
23
- // Export commonly used hooks and methods for convenience
24
14
  export const { useSession, signIn, signUp, signOut } = authClient;
25
15
  //# sourceMappingURL=client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAEhE,yEAAyE;AACzE,sEAAsE;AACtE,mEAAmE;AACnE,oEAAoE;AACpE,wEAAwE;AACxE,+DAA+D;AAC/D,uEAAuE;AACvE,sBAAsB;AACtB,MAAM,CAAC,MAAM,UAAU,GAAG,gBAAgB,CAAC;IACzC,OAAO,EAAE,CAAC,kBAAkB,EAAE,CAAC;CAChC,CAAC,CAAC;AAEH,yDAAyD;AACzD,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,UAAU,CAAC"}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAEhE,0EAA0E;AAC1E,uEAAuE;AACvE,4EAA4E;AAC5E,qCAAqC;AACrC,MAAM,CAAC,MAAM,UAAU,GAAG,gBAAgB,CAAC;IACzC,OAAO,EAAE,CAAC,kBAAkB,EAAE,CAAC;CAChC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,UAAU,CAAC"}
package/dist/edge.js CHANGED
@@ -1,37 +1,31 @@
1
1
  /**
2
- * Edge-safe session presence check.
3
- *
4
- * Middleware runs in the Edge Runtime, where no TCP Postgres driver is
5
- * available. The previous version of this module solved that by
6
- * instantiating Better-Auth against the Neon HTTP driver — which made
7
- * middleware silently Neon-only. Against any other Postgres the session
8
- * query failed with `TypeError: fetch failed`, Better-Auth turned that
9
- * into `APIError: Failed to get session`, and the middleware threw: every
10
- * request to the app returned HTTP 500. The old code hid this behind a
11
- * `NODE_ENV === "development"` bypass that skipped auth entirely, so the
12
- * failure only appeared in a production build on a non-Neon database.
13
- *
14
- * The fix is to stop querying the database from the edge at all.
15
- * Middleware performs an OPTIMISTIC check: it reads the session cookie
16
- * and decides where to send the request. It never asserts that the
17
- * session is valid.
18
- *
19
- * The authoritative check stays server-side, where it always was —
20
- * `requireAuth`/`requireWorkspace`/`requireRole` and the authenticated
21
- * layout's own `getAuthSession()` redirect. A forged or expired cookie
22
- * gets past middleware and is then rejected by the page or action that
23
- * actually reads data. This is the pattern Better-Auth documents for
24
- * Next.js middleware, and it is what makes the guard work on any
25
- * Postgres, in any runtime, with one code path in every environment.
2
+ * Edge-safe session presence check. Middleware runs where no TCP Postgres
3
+ * driver exists, so it never queries the database: it reads the cookie and
4
+ * picks a redirect. A forged or expired cookie gets past it and is rejected
5
+ * server-side by `requireAuth`/`requireWorkspace`/`requireRole`.
26
6
  */
27
7
  import { getSessionCookie } from "better-auth/cookies";
28
8
  /**
29
9
  * True when the request carries a Better-Auth session cookie.
30
10
  *
31
- * Presence only — the cookie's signature is NOT verified and no
32
- * database read happens. Never use this to authorize access to data;
33
- * use it to choose a redirect. Authorization belongs to `requireAuth`
34
- * and friends, which run in the Node runtime against the real session.
11
+ * For one thing only: an optional optimistic redirect in a consumer's
12
+ * `proxy.ts` / `middleware.ts`, sending a visitor with no cookie to the
13
+ * login page before the page renders. The scaffold does not use it; its
14
+ * session redirect runs server-side in the `(app)` layout.
15
+ *
16
+ * Presence only — the cookie's signature is NOT verified and no database
17
+ * read happens, so this is never authorization. A forged or expired cookie
18
+ * passes it and is rejected by `requireAuth` and friends, which every
19
+ * layout, action and route handler still calls.
20
+ *
21
+ * @example
22
+ * const intl = createIntlMiddleware(routing);
23
+ * export default function proxy(request: NextRequest) {
24
+ * const isApp = /^\/(?:[a-z]{2}\/)?dashboard(?:\/|$)/.test(request.nextUrl.pathname);
25
+ * if (isApp && !hasSessionCookie(request))
26
+ * return NextResponse.redirect(new URL("/login", request.url));
27
+ * return intl(request);
28
+ * }
35
29
  */
36
30
  export function hasSessionCookie(request) {
37
31
  return getSessionCookie(request) !== null;
package/dist/edge.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"edge.js","sourceRoot":"","sources":["../src/edge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAgB;IAC/C,OAAO,gBAAgB,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;AAC5C,CAAC"}
1
+ {"version":3,"file":"edge.js","sourceRoot":"","sources":["../src/edge.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAgB;IAC/C,OAAO,gBAAgB,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Thrown by `requireAuth`, `requireWorkspace`, `requireRole` and
3
+ * `requirePlatformAdmin`. A transport picks its status from `code`; the
4
+ * message is for logs and never reaches a user.
5
+ */
6
+ const MESSAGES = {
7
+ unauthenticated: "Unauthorized",
8
+ no_workspace: "No active workspace",
9
+ forbidden: "Insufficient permissions",
10
+ };
11
+ export class AuthGuardError extends Error {
12
+ constructor(code, message = MESSAGES[code]) {
13
+ super(message);
14
+ this.name = "AuthGuardError";
15
+ this.code = code;
16
+ // Extending built-ins loses `instanceof` on some transpilation targets.
17
+ Object.setPrototypeOf(this, AuthGuardError.prototype);
18
+ }
19
+ }
20
+ export function isAuthGuardError(error) {
21
+ return error instanceof AuthGuardError;
22
+ }
23
+ //# sourceMappingURL=guard-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guard-error.js","sourceRoot":"","sources":["../src/guard-error.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAUH,MAAM,QAAQ,GAAuC;IACnD,eAAe,EAAE,cAAc;IAC/B,YAAY,EAAE,qBAAqB;IACnC,SAAS,EAAE,0BAA0B;CACtC,CAAC;AAEF,MAAM,OAAO,cAAe,SAAQ,KAAK;IAGvC,YAAY,IAAwB,EAAE,UAAkB,QAAQ,CAAC,IAAI,CAAC;QACpE,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QAEjB,wEAAwE;QACxE,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,cAAc,CAAC,SAAS,CAAC,CAAC;IACxD,CAAC;CACF;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,OAAO,KAAK,YAAY,cAAc,CAAC;AACzC,CAAC"}
package/dist/helpers.js CHANGED
@@ -1,34 +1,22 @@
1
1
  /**
2
- * Server-Side Auth Helpers
3
- *
4
- * Utilities for server components and server actions to check authentication.
5
- * These helpers use Better-Auth's session API with Next.js headers.
6
- *
7
- * Usage in server actions (TECH-10 pattern):
8
- * ```typescript
9
- * export async function myServerAction() {
10
- * const { user } = await requireAuth();
11
- * // ... proceed with authenticated user
12
- * }
13
- * ```
2
+ * Session and workspace guards for server components, Server Actions and
3
+ * route handlers. Headers come from `@intelligo-dev/core/request-context`.
14
4
  */
15
- import { headers } from "next/headers";
16
- import { auth } from "./server";
5
+ import { getRequestHeaders } from "@intelligo-dev/core/request-context";
6
+ import { auth } from "./server.js";
17
7
  import { createLogger } from "@intelligo-dev/core/logger";
18
8
  import { db } from "@intelligo-dev/core/db";
19
9
  import { users } from "@intelligo-dev/core/db/schema";
20
10
  import { eq } from "drizzle-orm";
21
- import { PLATFORM_ADMIN_ROLE } from "./roles";
11
+ import { AuthGuardError } from "./guard-error.js";
12
+ import { PLATFORM_ADMIN_ROLE, platformAdminStanding } from "./roles.js";
22
13
  const log = createLogger("Auth");
23
14
  /**
24
- * Get current auth session from request headers.
25
- *
26
- * Returns session and user if authenticated, null otherwise.
27
- * Use this in server components that handle both authenticated and unauthenticated states.
15
+ * The current session and user, or null when unauthenticated.
28
16
  */
29
17
  export async function getAuthSession() {
30
18
  const session = await auth.api.getSession({
31
- headers: await headers(),
19
+ headers: await getRequestHeaders(),
32
20
  });
33
21
  if (!session?.user) {
34
22
  return null;
@@ -39,25 +27,21 @@ export async function getAuthSession() {
39
27
  };
40
28
  }
41
29
  /**
42
- * Require authentication.
30
+ * The current session and user.
43
31
  *
44
- * Throws "Unauthorized" error if no valid session.
45
- * Use this in server actions to enforce authentication (TECH-10 pattern).
46
- *
47
- * @throws Error with "Unauthorized" message if not authenticated
32
+ * @throws AuthGuardError `unauthenticated` ("Unauthorized") when there is
33
+ * no valid session.
48
34
  */
49
35
  export async function requireAuth() {
50
36
  const result = await getAuthSession();
51
37
  if (!result) {
52
- throw new Error("Unauthorized");
38
+ throw new AuthGuardError("unauthenticated");
53
39
  }
54
40
  return result;
55
41
  }
56
42
  /**
57
- * Get workspace context by organization ID.
58
- * Use this when you know the org ID (e.g., just after creating/activating a workspace).
59
- *
60
- * @param organizationId - The organization ID to get context for
43
+ * Workspace context for a given organization id, e.g. just after creating
44
+ * or activating one. Null when unauthenticated or not a member.
61
45
  */
62
46
  export async function getWorkspaceContextById(organizationId) {
63
47
  const authResult = await getAuthSession();
@@ -66,16 +50,14 @@ export async function getWorkspaceContextById(organizationId) {
66
50
  return null;
67
51
  }
68
52
  log.debug("getWorkspaceContextById: fetching org");
69
- // Get organization by ID directly (not from session)
70
53
  const org = await auth.api.getFullOrganization({
71
- headers: await headers(),
54
+ headers: await getRequestHeaders(),
72
55
  query: { organizationId },
73
56
  });
74
57
  if (!org) {
75
58
  log.debug("getWorkspaceContextById: org not found");
76
59
  return null;
77
60
  }
78
- // Find user's membership
79
61
  const membership = org.members?.find((m) => m.userId === authResult.user.id);
80
62
  if (!membership) {
81
63
  log.debug("getWorkspaceContextById: user not a member");
@@ -97,37 +79,39 @@ export async function getWorkspaceContextById(organizationId) {
97
79
  };
98
80
  }
99
81
  /**
100
- * Get workspace context from the active organization in session.
101
- * Returns null if no active organization set.
102
- *
103
- * Use this in server components that need workspace context but can handle
104
- * the absence of an active workspace (e.g., workspace switcher UI).
82
+ * The workspace a session acts in: its active organization, read by explicit
83
+ * id, falling back to the user's first workspace. Null when the user has none.
105
84
  */
106
- export async function getWorkspaceContext() {
107
- const authResult = await getAuthSession();
108
- if (!authResult) {
109
- log.debug("getWorkspaceContext: no auth session");
110
- return null;
111
- }
112
- log.debug("getWorkspaceContext: session found");
113
- // Get active organization from session
114
- let activeOrg = await auth.api.getFullOrganization({
115
- headers: await headers(),
116
- });
85
+ async function resolveWorkspaceContext(authResult) {
86
+ const activeOrganizationId = authResult.session.activeOrganizationId;
87
+ // Better-Auth throws FORBIDDEN when the active organization no longer
88
+ // admits this user (removed member); treat that as "no active workspace"
89
+ // so the fallback below runs.
90
+ let activeOrg = activeOrganizationId
91
+ ? await auth.api
92
+ .getFullOrganization({
93
+ headers: await getRequestHeaders(),
94
+ query: { organizationId: activeOrganizationId },
95
+ })
96
+ .catch((error) => {
97
+ log.debug("getWorkspaceContext: active org not readable", {
98
+ error: error instanceof Error ? error.message : String(error),
99
+ });
100
+ return null;
101
+ })
102
+ : null;
117
103
  log.debug("getWorkspaceContext: active org", { hasOrg: !!activeOrg });
118
- // Fallback: If no active org in session, auto-select first available workspace
119
104
  if (!activeOrg) {
120
105
  log.debug("getWorkspaceContext: no active org, checking workspaces");
121
106
  const orgs = await auth.api.listOrganizations({
122
- headers: await headers(),
107
+ headers: await getRequestHeaders(),
123
108
  });
124
109
  if (orgs && orgs.length > 0) {
125
110
  log.debug("getWorkspaceContext: found workspaces", {
126
111
  count: String(orgs.length),
127
112
  });
128
- // Fetch full organization details for the first workspace
129
113
  activeOrg = await auth.api.getFullOrganization({
130
- headers: await headers(),
114
+ headers: await getRequestHeaders(),
131
115
  query: { organizationId: orgs[0].id },
132
116
  });
133
117
  }
@@ -136,7 +120,6 @@ export async function getWorkspaceContext() {
136
120
  log.debug("getWorkspaceContext: no workspaces found");
137
121
  return null;
138
122
  }
139
- // Find user's membership in the active org
140
123
  const activeMember = activeOrg.members.find((m) => m.userId === authResult.user.id);
141
124
  log.debug("getWorkspaceContext: membership check", {
142
125
  hasMembership: !!activeMember,
@@ -161,78 +144,75 @@ export async function getWorkspaceContext() {
161
144
  };
162
145
  }
163
146
  /**
164
- * Require workspace context. Use in server actions that need workspace scope (TECH-10).
165
- * Throws if not authenticated OR no active workspace selected.
166
- *
167
- * Use this in server actions that operate on workspace-scoped resources
168
- * (conversations, knowledge bases, usage logs, etc.).
147
+ * Workspace context from the session's active organization, falling back to
148
+ * the user's first workspace. Null when unauthenticated or when the user has
149
+ * none.
150
+ */
151
+ export async function getWorkspaceContext() {
152
+ const authResult = await getAuthSession();
153
+ if (!authResult) {
154
+ log.debug("getWorkspaceContext: no auth session");
155
+ return null;
156
+ }
157
+ return resolveWorkspaceContext(authResult);
158
+ }
159
+ /**
160
+ * Workspace context for workspace-scoped operations.
169
161
  *
170
- * @throws Error with "No active workspace" message if no workspace selected
162
+ * @throws AuthGuardError `unauthenticated` ("Unauthorized") when there is
163
+ * no valid session, `no_workspace` ("No active workspace") when the user
164
+ * has none.
171
165
  */
172
166
  export async function requireWorkspace() {
173
- const context = await getWorkspaceContext();
167
+ const context = await resolveWorkspaceContext(await requireAuth());
174
168
  if (!context) {
175
- throw new Error("No active workspace");
169
+ throw new AuthGuardError("no_workspace");
176
170
  }
177
171
  return context;
178
172
  }
179
173
  /**
180
- * Require specific role in active workspace.
181
- * Use for admin/owner-only server actions (TEAM-08).
174
+ * Workspace context when the member holds one of `allowedRoles`, e.g.
175
+ * `requireRole(["owner", "admin"])`.
182
176
  *
183
- * Example: requireRole(["owner", "admin"]) for workspace settings actions.
184
- *
185
- * @param allowedRoles - Array of role names that are permitted
186
- * @throws Error with "Insufficient permissions" message if user lacks required role
177
+ * @throws AuthGuardError `forbidden` ("Insufficient permissions") when the
178
+ * member lacks the role, and whatever `requireWorkspace` throws.
187
179
  */
188
180
  export async function requireRole(allowedRoles) {
189
181
  const context = await requireWorkspace();
190
- if (!allowedRoles.includes(context.membership.role)) {
191
- throw new Error("Insufficient permissions");
182
+ // Better-Auth stores roles as a comma-separated string, so a member
183
+ // may hold more than one; any of them may satisfy the check.
184
+ const held = String(context.membership.role)
185
+ .split(",")
186
+ .map((r) => r.trim());
187
+ if (!held.some((r) => allowedRoles.includes(r))) {
188
+ throw new AuthGuardError("forbidden");
192
189
  }
193
190
  return context;
194
191
  }
195
192
  /**
196
- * Require PLATFORM admin — distinct from workspace roles.
197
- *
198
- * Workspace `owner` is a per-tenant role: any user who creates a
199
- * workspace owns it. Platform-level surfaces (cross-workspace
200
- * analytics, the operational console, impersonation) must never be
201
- * gated on it.
202
- *
203
- * The authority is `users.role`. PLATFORM_ADMIN_EMAILS is the
204
- * bootstrap: an allowlisted user is promoted into the column the first
205
- * time they pass through here, so a fresh deployment has a way in and
206
- * every later check — including Better-Auth's admin plugin, which can
207
- * only read the row — agrees with this one. Two gates that can
208
- * disagree is how the cross-tenant analytics leak happened in the
209
- * first place.
193
+ * Require a PLATFORM admin, distinct from workspace roles: workspace
194
+ * `owner` is per-tenant, so platform surfaces must never be gated on it.
210
195
  *
196
+ * The authority is `users.role`. PLATFORM_ADMIN_EMAILS is the bootstrap:
197
+ * an allowlisted user is promoted into the column on first use, so every
198
+ * later check, Better-Auth's admin plugin included, reads the same row.
211
199
  * Closed by default: no allowlist and no role means no admin.
212
200
  *
213
- * @throws Error("Insufficient permissions") when the user is not a
201
+ * @throws AuthGuardError `unauthenticated` when there is no session,
202
+ * `forbidden` ("Insufficient permissions") when the user is not a
214
203
  * platform admin.
215
204
  */
216
205
  export async function requirePlatformAdmin() {
217
206
  const { session, user } = await requireAuth();
218
- const allowlist = (process.env.PLATFORM_ADMIN_EMAILS ?? "")
219
- .split(",")
220
- .map((e) => e.trim().toLowerCase())
221
- .filter(Boolean);
222
- const email = user.email?.toLowerCase();
223
- const allowlisted = !!email && allowlist.includes(email);
224
- const roles = (user.role ?? "")
225
- .split(",")
226
- .map((r) => r.trim());
227
- const hasRole = roles.includes(PLATFORM_ADMIN_ROLE);
207
+ const { allowlisted, hasRole, roles } = platformAdminStanding(user);
228
208
  if (!allowlisted && !hasRole) {
229
- throw new Error("Insufficient permissions");
209
+ throw new AuthGuardError("forbidden");
230
210
  }
231
211
  // Promote on first use so the row, not the environment, is what
232
212
  // every other check reads. A failure here must not lock an admin out
233
213
  // mid-incident — they are already authorized by the allowlist.
234
214
  if (allowlisted && !hasRole) {
235
- const next = [...roles.filter(Boolean), PLATFORM_ADMIN_ROLE].join(",");
215
+ const next = [...roles, PLATFORM_ADMIN_ROLE].join(",");
236
216
  try {
237
217
  await db.update(users).set({ role: next }).where(eq(users.id, user.id));
238
218
  }
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,EAAE,EAAE,MAAM,wBAAwB,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,+BAA+B,CAAC;AACtD,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEjC,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAK9C,MAAM,GAAG,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;AAEjC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc;IAIlC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC;QACxC,OAAO,EAAE,MAAM,OAAO,EAAE;KACzB,CAAC,CAAC;IAEH,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;QACnB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO;QACL,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,IAAI,EAAE,OAAO,CAAC,IAAI;KACnB,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW;IAI/B,MAAM,MAAM,GAAG,MAAM,cAAc,EAAE,CAAC;IAEtC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,cAAc,CAAC,CAAC;IAClC,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,cAAsB;IAMlE,MAAM,UAAU,GAAG,MAAM,cAAc,EAAE,CAAC;IAC1C,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,GAAG,CAAC,KAAK,CAAC,0CAA0C,CAAC,CAAC;QACtD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,GAAG,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAEnD,qDAAqD;IACrD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,mBAAmB,CAAC;QAC7C,OAAO,EAAE,MAAM,OAAO,EAAE;QACxB,KAAK,EAAE,EAAE,cAAc,EAAE;KAC1B,CAAC,CAAC;IAEH,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,GAAG,CAAC,KAAK,CAAC,wCAAwC,CAAC,CAAC;QACpD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,yBAAyB;IACzB,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,EAAE,IAAI,CAClC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,UAAU,CAAC,IAAI,CAAC,EAAE,CAC5C,CAAC;IAEF,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,GAAG,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;QACxD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO;QACL,OAAO,EAAE,UAAU,CAAC,OAAO;QAC3B,IAAI,EAAE,UAAU,CAAC,IAAI;QACrB,SAAS,EAAE;YACT,EAAE,EAAE,GAAG,CAAC,EAAE;YACV,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,IAAI,EAAE,GAAG,CAAC,IAAI;SACf;QACD,UAAU,EAAE;YACV,EAAE,EAAE,UAAU,CAAC,EAAE;YACjB,IAAI,EAAE,UAAU,CAAC,IAAI;SACtB;KACF,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB;IAMvC,MAAM,UAAU,GAAG,MAAM,cAAc,EAAE,CAAC;IAC1C,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,GAAG,CAAC,KAAK,CAAC,sCAAsC,CAAC,CAAC;QAClD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,GAAG,CAAC,KAAK,CAAC,oCAAoC,CAAC,CAAC;IAEhD,uCAAuC;IACvC,IAAI,SAAS,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,mBAAmB,CAAC;QACjD,OAAO,EAAE,MAAM,OAAO,EAAE;KACzB,CAAC,CAAC;IAEH,GAAG,CAAC,KAAK,CAAC,iCAAiC,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC;IAEtE,+EAA+E;IAC/E,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,GAAG,CAAC,KAAK,CAAC,yDAAyD,CAAC,CAAC;QACrE,MAAM,IAAI,GAAQ,MAAM,IAAI,CAAC,GAAG,CAAC,iBAAiB,CAAC;YACjD,OAAO,EAAE,MAAM,OAAO,EAAE;SACzB,CAAC,CAAC;QAEH,IAAI,IAAI,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC5B,GAAG,CAAC,KAAK,CAAC,uCAAuC,EAAE;gBACjD,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;aAC3B,CAAC,CAAC;YACH,0DAA0D;YAC1D,SAAS,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,mBAAmB,CAAC;gBAC7C,OAAO,EAAE,MAAM,OAAO,EAAE;gBACxB,KAAK,EAAE,EAAE,cAAc,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE;aACtC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,GAAG,CAAC,KAAK,CAAC,0CAA0C,CAAC,CAAC;QACtD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,2CAA2C;IAC3C,MAAM,YAAY,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CACzC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,UAAU,CAAC,IAAI,CAAC,EAAE,CAC5C,CAAC;IAEF,GAAG,CAAC,KAAK,CAAC,uCAAuC,EAAE;QACjD,aAAa,EAAE,CAAC,CAAC,YAAY;KAC9B,CAAC,CAAC;IAEH,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,GAAG,CAAC,KAAK,CAAC,sDAAsD,CAAC,CAAC;QAClE,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO;QACL,OAAO,EAAE,UAAU,CAAC,OAAO;QAC3B,IAAI,EAAE,UAAU,CAAC,IAAI;QACrB,SAAS,EAAE;YACT,EAAE,EAAE,SAAS,CAAC,EAAE;YAChB,IAAI,EAAE,SAAS,CAAC,IAAI;YACpB,IAAI,EAAE,SAAS,CAAC,IAAI;YACpB,IAAI,EAAE,SAAS,CAAC,IAAI;SACrB;QACD,UAAU,EAAE;YACV,EAAE,EAAE,YAAY,CAAC,EAAE;YACnB,IAAI,EAAE,YAAY,CAAC,IAAI;SACxB;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB;IAMpC,MAAM,OAAO,GAAG,MAAM,mBAAmB,EAAE,CAAC;IAC5C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,YAA6B;IAM7D,MAAM,OAAO,GAAG,MAAM,gBAAgB,EAAE,CAAC;IACzC,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAC9C,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB;IAIxC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,MAAM,WAAW,EAAE,CAAC;IAE9C,MAAM,SAAS,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,EAAE,CAAC;SACxD,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;SAClC,MAAM,CAAC,OAAO,CAAC,CAAC;IAEnB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE,WAAW,EAAE,CAAC;IACxC,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,IAAI,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACzD,MAAM,KAAK,GAAG,CAAE,IAAiC,CAAC,IAAI,IAAI,EAAE,CAAC;SAC1D,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACxB,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC;IAEpD,IAAI,CAAC,WAAW,IAAI,CAAC,OAAO,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAC9C,CAAC;IAED,gEAAgE;IAChE,qEAAqE;IACrE,+DAA+D;IAC/D,IAAI,WAAW,IAAI,CAAC,OAAO,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,mBAAmB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACvE,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;QAC1E,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,GAAG,CAAC,KAAK,CAAC,8CAA8C,EAAE;gBACxD,MAAM,EAAE,IAAI,CAAC,EAAE;gBACf,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;aAC9D,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC3B,CAAC"}
1
+ {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AACxE,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,EAAE,EAAE,MAAM,wBAAwB,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,+BAA+B,CAAC;AACtD,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEjC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAKrE,MAAM,GAAG,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;AAEjC;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc;IAIlC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC;QACxC,OAAO,EAAE,MAAM,iBAAiB,EAAE;KACnC,CAAC,CAAC;IAEH,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;QACnB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO;QACL,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,IAAI,EAAE,OAAO,CAAC,IAAI;KACnB,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW;IAI/B,MAAM,MAAM,GAAG,MAAM,cAAc,EAAE,CAAC;IAEtC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,cAAc,CAAC,iBAAiB,CAAC,CAAC;IAC9C,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,cAAsB;IAMlE,MAAM,UAAU,GAAG,MAAM,cAAc,EAAE,CAAC;IAC1C,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,GAAG,CAAC,KAAK,CAAC,0CAA0C,CAAC,CAAC;QACtD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,GAAG,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAEnD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,mBAAmB,CAAC;QAC7C,OAAO,EAAE,MAAM,iBAAiB,EAAE;QAClC,KAAK,EAAE,EAAE,cAAc,EAAE;KAC1B,CAAC,CAAC;IAEH,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,GAAG,CAAC,KAAK,CAAC,wCAAwC,CAAC,CAAC;QACpD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,EAAE,IAAI,CAClC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,UAAU,CAAC,IAAI,CAAC,EAAE,CAC5C,CAAC;IAEF,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,GAAG,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;QACxD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO;QACL,OAAO,EAAE,UAAU,CAAC,OAAO;QAC3B,IAAI,EAAE,UAAU,CAAC,IAAI;QACrB,SAAS,EAAE;YACT,EAAE,EAAE,GAAG,CAAC,EAAE;YACV,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,IAAI,EAAE,GAAG,CAAC,IAAI;SACf;QACD,UAAU,EAAE;YACV,EAAE,EAAE,UAAU,CAAC,EAAE;YACjB,IAAI,EAAE,UAAU,CAAC,IAAI;SACtB;KACF,CAAC;AACJ,CAAC;AASD;;;GAGG;AACH,KAAK,UAAU,uBAAuB,CAAC,UAGtC;IACC,MAAM,oBAAoB,GACxB,UAAU,CAAC,OACZ,CAAC,oBAAoB,CAAC;IAEvB,sEAAsE;IACtE,yEAAyE;IACzE,8BAA8B;IAC9B,IAAI,SAAS,GAAG,oBAAoB;QAClC,CAAC,CAAC,MAAM,IAAI,CAAC,GAAG;aACX,mBAAmB,CAAC;YACnB,OAAO,EAAE,MAAM,iBAAiB,EAAE;YAClC,KAAK,EAAE,EAAE,cAAc,EAAE,oBAAoB,EAAE;SAChD,CAAC;aACD,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;YACxB,GAAG,CAAC,KAAK,CAAC,8CAA8C,EAAE;gBACxD,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;aAC9D,CAAC,CAAC;YACH,OAAO,IAAI,CAAC;QACd,CAAC,CAAC;QACN,CAAC,CAAC,IAAI,CAAC;IAET,GAAG,CAAC,KAAK,CAAC,iCAAiC,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC;IAEtE,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,GAAG,CAAC,KAAK,CAAC,yDAAyD,CAAC,CAAC;QACrE,MAAM,IAAI,GAAQ,MAAM,IAAI,CAAC,GAAG,CAAC,iBAAiB,CAAC;YACjD,OAAO,EAAE,MAAM,iBAAiB,EAAE;SACnC,CAAC,CAAC;QAEH,IAAI,IAAI,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC5B,GAAG,CAAC,KAAK,CAAC,uCAAuC,EAAE;gBACjD,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;aAC3B,CAAC,CAAC;YACH,SAAS,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,mBAAmB,CAAC;gBAC7C,OAAO,EAAE,MAAM,iBAAiB,EAAE;gBAClC,KAAK,EAAE,EAAE,cAAc,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE;aACtC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,GAAG,CAAC,KAAK,CAAC,0CAA0C,CAAC,CAAC;QACtD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,YAAY,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CACzC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,UAAU,CAAC,IAAI,CAAC,EAAE,CAC5C,CAAC;IAEF,GAAG,CAAC,KAAK,CAAC,uCAAuC,EAAE;QACjD,aAAa,EAAE,CAAC,CAAC,YAAY;KAC9B,CAAC,CAAC;IAEH,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,GAAG,CAAC,KAAK,CAAC,sDAAsD,CAAC,CAAC;QAClE,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO;QACL,OAAO,EAAE,UAAU,CAAC,OAAO;QAC3B,IAAI,EAAE,UAAU,CAAC,IAAI;QACrB,SAAS,EAAE;YACT,EAAE,EAAE,SAAS,CAAC,EAAE;YAChB,IAAI,EAAE,SAAS,CAAC,IAAI;YACpB,IAAI,EAAE,SAAS,CAAC,IAAI;YACpB,IAAI,EAAE,SAAS,CAAC,IAAI;SACrB;QACD,UAAU,EAAE;YACV,EAAE,EAAE,YAAY,CAAC,EAAE;YACnB,IAAI,EAAE,YAAY,CAAC,IAAI;SACxB;KACF,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB;IACvC,MAAM,UAAU,GAAG,MAAM,cAAc,EAAE,CAAC;IAC1C,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,GAAG,CAAC,KAAK,CAAC,sCAAsC,CAAC,CAAC;QAClD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,uBAAuB,CAAC,UAAU,CAAC,CAAC;AAC7C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,MAAM,OAAO,GAAG,MAAM,uBAAuB,CAAC,MAAM,WAAW,EAAE,CAAC,CAAC;IACnE,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,cAAc,CAAC,cAAc,CAAC,CAAC;IAC3C,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,YAA6B;IAE7B,MAAM,OAAO,GAAG,MAAM,gBAAgB,EAAE,CAAC;IACzC,oEAAoE;IACpE,6DAA6D;IAC7D,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC;SACzC,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAoB,CAAC;IAC3C,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAChD,MAAM,IAAI,cAAc,CAAC,WAAW,CAAC,CAAC;IACxC,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB;IAIxC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,MAAM,WAAW,EAAE,CAAC;IAE9C,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,qBAAqB,CAC3D,IAIC,CACF,CAAC;IAEF,IAAI,CAAC,WAAW,IAAI,CAAC,OAAO,EAAE,CAAC;QAC7B,MAAM,IAAI,cAAc,CAAC,WAAW,CAAC,CAAC;IACxC,CAAC;IAED,gEAAgE;IAChE,qEAAqE;IACrE,+DAA+D;IAC/D,IAAI,WAAW,IAAI,CAAC,OAAO,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,CAAC,GAAG,KAAK,EAAE,mBAAmB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACvD,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;QAC1E,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,GAAG,CAAC,KAAK,CAAC,8CAA8C,EAAE;gBACxD,MAAM,EAAE,IAAI,CAAC,EAAE;gBACf,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;aAC9D,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC3B,CAAC"}
@@ -1,31 +1,52 @@
1
1
  import "server-only";
2
2
  /**
3
- * Session-level impersonation.
4
- *
5
- * This module owns only the session mechanics, because that is what
6
- * @intelligo-dev/auth owns: it holds the Better-Auth instance and the
7
- * request headers. The *policy* — who may do it, that it is audited or
8
- * refused, that a reason is mandatory — lives in @intelligo-dev/admin, and
9
- * these functions must not be called without going through it.
10
- *
11
- * Nothing here re-checks authorization. Splitting the check from the
12
- * act would give two places that can disagree about who is an admin,
13
- * which is the mistake that put cross-tenant analytics behind a
14
- * workspace role.
3
+ * Impersonation's session mechanics, gated on the platform admin role. The
4
+ * rest of the policy (an audit event that must be durable first, a mandatory
5
+ * reason) lives in `@intelligo-dev/admin`; a product calls
6
+ * `startImpersonation` / `stopImpersonation` there, never these directly.
15
7
  */
16
- import { headers } from "next/headers";
17
- import { auth } from "./server";
8
+ import { eq } from "drizzle-orm";
9
+ import { db } from "@intelligo-dev/core/db";
10
+ import { users } from "@intelligo-dev/core/db/schema";
11
+ import { getRequestHeaders } from "@intelligo-dev/core/request-context";
12
+ import { AuthGuardError } from "./guard-error.js";
13
+ import { requirePlatformAdmin } from "./helpers.js";
14
+ import { platformAdminStanding } from "./roles.js";
15
+ import { auth } from "./server.js";
18
16
  /** Default matching `impersonationSessionDuration` in server.ts. */
19
17
  const FALLBACK_DURATION_MS = 30 * 60000;
20
18
  /**
21
19
  * Swap the caller's session cookie for one belonging to `targetUserId`.
22
20
  *
23
- * Better-Auth refuses if the target is themselves a platform admin.
21
+ * The caller must be a platform admin and the target must not be one, by
22
+ * role or by the PLATFORM_ADMIN_EMAILS allowlist: both are checked here,
23
+ * before Better-Auth's own checks, so no import path reaches the session
24
+ * swap without them.
25
+ *
26
+ * @throws AuthGuardError `unauthenticated` with no session, `forbidden` when
27
+ * the caller is not a platform admin or the target is one.
24
28
  */
25
29
  export async function impersonateUser(targetUserId) {
30
+ await requirePlatformAdmin();
31
+ const [target] = await db
32
+ .select({ email: users.email, role: users.role })
33
+ .from(users)
34
+ .where(eq(users.id, targetUserId))
35
+ .limit(1);
36
+ if (target) {
37
+ // Protective here, so the allowlist counts whether or not the
38
+ // target has verified the address yet.
39
+ const { allowlisted, hasRole } = platformAdminStanding({
40
+ ...target,
41
+ emailVerified: true,
42
+ });
43
+ if (allowlisted || hasRole) {
44
+ throw new AuthGuardError("forbidden", "A platform admin cannot be impersonated");
45
+ }
46
+ }
26
47
  const result = (await auth.api.impersonateUser({
27
48
  body: { userId: targetUserId },
28
- headers: await headers(),
49
+ headers: await getRequestHeaders(),
29
50
  }));
30
51
  const expiresAt = result.session?.expiresAt;
31
52
  return {
@@ -35,8 +56,15 @@ export async function impersonateUser(targetUserId) {
35
56
  : new Date(Date.now() + FALLBACK_DURATION_MS),
36
57
  };
37
58
  }
38
- /** Restore the admin's own session. */
59
+ /**
60
+ * Restore the admin's own session.
61
+ *
62
+ * Not gated on the platform admin role: while impersonating, the caller's
63
+ * session is the target's. Better-Auth refuses a session that carries no
64
+ * `impersonatedBy`, and restores only the admin session its signed cookie
65
+ * names.
66
+ */
39
67
  export async function stopImpersonating() {
40
- await auth.api.stopImpersonating({ headers: await headers() });
68
+ await auth.api.stopImpersonating({ headers: await getRequestHeaders() });
41
69
  }
42
70
  //# sourceMappingURL=impersonation.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"impersonation.js","sourceRoot":"","sources":["../src/impersonation.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAErB;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAQhC,oEAAoE;AACpE,MAAM,oBAAoB,GAAG,EAAE,GAAG,KAAM,CAAC;AAEzC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,YAAoB;IAEpB,MAAM,MAAM,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,eAAe,CAAC;QAC7C,IAAI,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE;QAC9B,OAAO,EAAE,MAAM,OAAO,EAAE;KACzB,CAAC,CAAgD,CAAC;IAEnD,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC;IAE5C,OAAO;QACL,YAAY;QACZ,SAAS,EAAE,SAAS;YAClB,CAAC,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC;YACrB,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,oBAAoB,CAAC;KAChD,CAAC;AACJ,CAAC;AAED,uCAAuC;AACvC,MAAM,CAAC,KAAK,UAAU,iBAAiB;IACrC,MAAM,IAAI,CAAC,GAAG,CAAC,iBAAiB,CAAC,EAAE,OAAO,EAAE,MAAM,OAAO,EAAE,EAAE,CAAC,CAAC;AACjE,CAAC"}
1
+ {"version":3,"file":"impersonation.js","sourceRoot":"","sources":["../src/impersonation.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAErB;;;;;GAKG;AAEH,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEjC,OAAO,EAAE,EAAE,EAAE,MAAM,wBAAwB,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,+BAA+B,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AAExE,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAQhC,oEAAoE;AACpE,MAAM,oBAAoB,GAAG,EAAE,GAAG,KAAM,CAAC;AAEzC;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,YAAoB;IAEpB,MAAM,oBAAoB,EAAE,CAAC;IAE7B,MAAM,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE;SACtB,MAAM,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;SAChD,IAAI,CAAC,KAAK,CAAC;SACX,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC;SACjC,KAAK,CAAC,CAAC,CAAC,CAAC;IAEZ,IAAI,MAAM,EAAE,CAAC;QACX,8DAA8D;QAC9D,uCAAuC;QACvC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,qBAAqB,CAAC;YACrD,GAAG,MAAM;YACT,aAAa,EAAE,IAAI;SACpB,CAAC,CAAC;QACH,IAAI,WAAW,IAAI,OAAO,EAAE,CAAC;YAC3B,MAAM,IAAI,cAAc,CACtB,WAAW,EACX,yCAAyC,CAC1C,CAAC;QACJ,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,eAAe,CAAC;QAC7C,IAAI,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE;QAC9B,OAAO,EAAE,MAAM,iBAAiB,EAAE;KACnC,CAAC,CAAgD,CAAC;IAEnD,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC;IAE5C,OAAO;QACL,YAAY;QACZ,SAAS,EAAE,SAAS;YAClB,CAAC,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC;YACrB,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,oBAAoB,CAAC;KAChD,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB;IACrC,MAAM,IAAI,CAAC,GAAG,CAAC,iBAAiB,CAAC,EAAE,OAAO,EAAE,MAAM,iBAAiB,EAAE,EAAE,CAAC,CAAC;AAC3E,CAAC"}