@snokam/auth 5.0.2 → 5.2.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,18 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [5.2.0](https://github.com/snokam/components/compare/@snokam/auth@5.0.0...@snokam/auth@5.2.0) (2026-10-14)
7
+
8
+ ### Features
9
+
10
+ - company-first Vis som picker (test identity P3) ([#2917](https://github.com/snokam/components/issues/2917)) ([6098d44](https://github.com/snokam/components/commit/6098d446d91f630a360633615e6c4c0e81c9985f))
11
+
12
+ # [5.1.0](https://github.com/snokam/components/compare/@snokam/auth@5.0.0...@snokam/auth@5.1.0) (2026-10-14)
13
+
14
+ ### Features
15
+
16
+ - company-first Vis som picker (test identity P3) ([#2917](https://github.com/snokam/components/issues/2917)) ([6098d44](https://github.com/snokam/components/commit/6098d446d91f630a360633615e6c4c0e81c9985f))
17
+
6
18
  ## [5.0.2](https://github.com/snokam/components/compare/@snokam/auth@5.0.0...@snokam/auth@5.0.2) (2026-10-14)
7
19
 
8
20
  **Note:** Version bump only for package @snokam/auth
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@snokam/auth",
3
3
  "description": "Snøkam auth components",
4
- "version": "5.0.2",
4
+ "version": "5.2.0",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
7
7
  "files": [
@@ -26,12 +26,12 @@
26
26
  "@snokam/employees-client": "^1.3.0",
27
27
  "next-auth": "4.24.15",
28
28
  "path-to-regexp": "^8.4.2",
29
- "@snokam/sanity-page-content": "6.2.0",
30
- "@snokam/tenant": "4.0.0",
31
- "@snokam/ui": "6.0.1",
32
- "@snokam/platform-client": "5.0.0",
33
29
  "@snokam/identity": "1.1.0",
34
- "@snokam/react-query": "1.1.0"
30
+ "@snokam/platform-client": "5.2.0",
31
+ "@snokam/react-query": "1.1.0",
32
+ "@snokam/sanity-page-content": "6.4.0",
33
+ "@snokam/tenant": "4.2.0",
34
+ "@snokam/ui": "6.0.3"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "next": "16.3.8",
@@ -33,7 +33,6 @@ export {
33
33
  useImpersonation,
34
34
  } from "../impersonation/context";
35
35
  export type { ImpersonationActive } from "../impersonation/cookies";
36
- export { FOREIGN_BUSINESS_ERROR } from "../impersonation/required";
37
36
  export { isAllowedRedirect } from "../impersonation/redirects";
38
37
  export { signIn, SessionProvider } from "next-auth/react";
39
38
  export {
@@ -6,7 +6,7 @@ import { Alert, Avatar } from "@snokam/ui";
6
6
  import { SystemPage } from "@snokam/ui/system-page";
7
7
  import { ThemeScope } from "@snokam/ui/theme";
8
8
  import { useAuth } from "../../hooks";
9
- import { useOptionalConfig } from "@snokam/tenant";
9
+ import { useTenantEntitlements } from "@snokam/tenant";
10
10
 
11
11
  export interface ErrorPageProps {
12
12
  message?: ReactNode;
@@ -23,8 +23,7 @@ export const Error = ({
23
23
  }: ErrorPageProps) => {
24
24
  const { session, signOut } = useAuth();
25
25
 
26
- const config = useOptionalConfig();
27
- const isProd = config?.NEXT_PUBLIC_ENVIRONMENT === "p";
26
+ const tenant = useTenantEntitlements();
28
27
 
29
28
  return (
30
29
  <SystemPage>
@@ -50,7 +49,7 @@ export const Error = ({
50
49
  <br />({session.user.email})
51
50
  </Styled.User>
52
51
  )}
53
- {!isProd && (
52
+ {tenant?.personaMode && (
54
53
  <Alert severity="warning">
55
54
  Du er i test! Husk at det kun er mulig å logge inn <br />
56
55
  med en av våre testbrukere som du finner i 1Password.
@@ -22,5 +22,10 @@ export async function hostOwner(url: string): Promise<HostOwner> {
22
22
  platformApiBase: process.env.NEXT_PUBLIC_PLATFORM_API_BASE_URL ?? "",
23
23
  authToken: await getManagedBearer(),
24
24
  });
25
- return tenant?.businessId ? { businessId: tenant.businessId } : UNKNOWN_HOST;
25
+ return tenant?.businessId
26
+ ? {
27
+ businessId: tenant.businessId,
28
+ displayName: tenant.displayName ?? undefined,
29
+ }
30
+ : UNKNOWN_HOST;
26
31
  }
@@ -2,7 +2,9 @@ export const PLATFORM_HOST = "platform";
2
2
  export const UNKNOWN_HOST = "unknown";
3
3
 
4
4
  export type HostOwner =
5
- typeof PLATFORM_HOST | typeof UNKNOWN_HOST | { businessId: string };
5
+ | typeof PLATFORM_HOST
6
+ | typeof UNKNOWN_HOST
7
+ | { businessId: string; displayName?: string };
6
8
 
7
9
  export function maySealFor(
8
10
  owner: HostOwner,
@@ -20,6 +20,7 @@ export interface StartImpersonationInput {
20
20
  targetName?: string;
21
21
  targetImage?: string;
22
22
  reason: string;
23
+ businessId?: string;
23
24
  redirectTo?: string;
24
25
  returnTo?: string;
25
26
  landingUrl?: string;
@@ -20,6 +20,7 @@ interface StartBody {
20
20
  targetName?: string;
21
21
  targetImage?: string;
22
22
  reason?: string;
23
+ businessId?: string;
23
24
  redirectTo?: string;
24
25
  returnTo?: string;
25
26
  landingUrl?: string;
@@ -85,6 +86,7 @@ export function createImpersonationStartHandler(authConfig: Auth.Config) {
85
86
  targetEmail: body.targetEmail,
86
87
  targetName: body.targetName,
87
88
  reason: body.reason,
89
+ businessId: body.businessId,
88
90
  },
89
91
  })
90
92
  );
@@ -0,0 +1,92 @@
1
+ import type { ProtectedEmployee } from "@snokam/platform-client";
2
+ import { Auth } from "../types";
3
+ import { resolveAccessToken } from "../route-handler/token";
4
+ import { platformCall, platformClient, platformNotConfigured } from "./request";
5
+ import { hostOwner } from "../handoff/business";
6
+
7
+ function persona(employee: ProtectedEmployee) {
8
+ return {
9
+ name: employee.name,
10
+ email: employee.email,
11
+ position: employee.position,
12
+ about: employee.about,
13
+ active: employee.active,
14
+ isBot: employee.isBot,
15
+ image: { asset: { url: employee.image?.asset?.url } },
16
+ scopeGrants: employee.scopeGrants,
17
+ groups: (employee.groups ?? []).map((group) => ({
18
+ name: group.name,
19
+ scopeGrants: group.scopeGrants,
20
+ })),
21
+ };
22
+ }
23
+
24
+ interface ReturnBusiness {
25
+ id: string;
26
+ displayName: string | null;
27
+ }
28
+
29
+ async function returnBusiness(
30
+ request: Request
31
+ ): Promise<ReturnBusiness | null> {
32
+ const returnTo = new URL(request.url).searchParams.get("returnTo");
33
+ if (!returnTo || !URL.canParse(returnTo)) return null;
34
+ try {
35
+ const owner = await hostOwner(returnTo);
36
+ return typeof owner === "object"
37
+ ? { id: owner.businessId, displayName: owner.displayName ?? null }
38
+ : null;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ async function callerClient(authConfig: Auth.Config, request: Request) {
45
+ const caller = await resolveAccessToken(authConfig, request);
46
+ if ("error" in caller) return { error: caller.error };
47
+ const client = platformClient(caller.accessToken);
48
+ return client ? { client } : { error: platformNotConfigured() };
49
+ }
50
+
51
+ export function createPickerBusinessesHandler(authConfig: Auth.Config) {
52
+ return async function GET(request: Request) {
53
+ const caller = await callerClient(authConfig, request);
54
+ if ("error" in caller) return caller.error;
55
+
56
+ const [businesses, apps] = await Promise.all([
57
+ platformCall(caller.client.listImpersonationBusinesses()),
58
+ platformCall(caller.client.getMeApps()),
59
+ ]);
60
+ if ("error" in businesses) return businesses.error;
61
+ if ("error" in apps) return apps.error;
62
+ if (businesses.value.length === 0) {
63
+ return new Response("Not found", { status: 404 });
64
+ }
65
+
66
+ return Response.json({
67
+ businesses: businesses.value,
68
+ apps: apps.value,
69
+ returnBusiness: await returnBusiness(request),
70
+ });
71
+ };
72
+ }
73
+
74
+ export function createPickerPeopleHandler(authConfig: Auth.Config) {
75
+ return async function GET(request: Request) {
76
+ const businessId = new URL(request.url).searchParams.get("businessId");
77
+ if (!businessId) return new Response("Missing businessId", { status: 400 });
78
+
79
+ const caller = await callerClient(authConfig, request);
80
+ if ("error" in caller) return caller.error;
81
+
82
+ const people = await platformCall(
83
+ caller.client.listImpersonationPeople({ businessId })
84
+ );
85
+ if ("error" in people) return people.error;
86
+
87
+ return Response.json({
88
+ kind: people.value.kind,
89
+ people: (people.value.people ?? []).map(persona),
90
+ });
91
+ };
92
+ }
@@ -2,8 +2,6 @@ import type { TokenClaims } from "./claims";
2
2
 
3
3
  export const IMPERSONATION_REQUIRED_ERROR = "impersonation_required";
4
4
 
5
- export const FOREIGN_BUSINESS_ERROR = "foreign_business";
6
-
7
5
  export const IMPERSONATION_PICKER_PATH = "/velg-bruker";
8
6
 
9
7
  const RENEW_MARGIN_SECONDS = 120;
@@ -5,7 +5,10 @@ import {
5
5
  createImpersonationAdoptHandler,
6
6
  } from "./handlers";
7
7
  import { createImpersonationRenewHandler } from "./renew";
8
- import { createImpersonationPersonasHandler } from "./personas";
8
+ import {
9
+ createPickerBusinessesHandler,
10
+ createPickerPeopleHandler,
11
+ } from "./picker";
9
12
 
10
13
  type OpContext = { params: Promise<{ op: string }> };
11
14
 
@@ -14,13 +17,15 @@ export function createImpersonationRouteHandlers(authConfig: Auth.Config) {
14
17
  const end = createImpersonationEndHandler(authConfig);
15
18
  const adopt = createImpersonationAdoptHandler(authConfig);
16
19
  const renew = createImpersonationRenewHandler(authConfig);
17
- const personas = createImpersonationPersonasHandler(authConfig);
20
+ const businesses = createPickerBusinessesHandler(authConfig);
21
+ const people = createPickerPeopleHandler(authConfig);
18
22
 
19
23
  return {
20
24
  async GET(request: Request, ctx: OpContext) {
21
25
  const { op } = await ctx.params;
22
26
  if (op === "adopt") return adopt(request);
23
- if (op === "personas") return personas(request);
27
+ if (op === "businesses") return businesses(request);
28
+ if (op === "people") return people(request);
24
29
  return new Response("Not found", { status: 404 });
25
30
  },
26
31
  async POST(request: Request, ctx: OpContext) {
@@ -1,64 +0,0 @@
1
- import type { ProtectedEmployee } from "@snokam/platform-client";
2
- import { Auth } from "../types";
3
- import { resolveAccessToken } from "../route-handler/token";
4
- import { tokenClaims } from "./claims";
5
- import { platformCall, platformClient, platformNotConfigured } from "./request";
6
- import { FOREIGN_BUSINESS_ERROR, personaRequired } from "./required";
7
- import { hostOwner } from "../handoff/business";
8
- import { maySealFor } from "../handoff/seal";
9
-
10
- function persona(employee: ProtectedEmployee) {
11
- return {
12
- name: employee.name,
13
- email: employee.email,
14
- position: employee.position,
15
- about: employee.about,
16
- active: employee.active,
17
- isBot: employee.isBot,
18
- image: { asset: { url: employee.image?.asset?.url } },
19
- scopeGrants: employee.scopeGrants,
20
- groups: (employee.groups ?? []).map((group) => ({
21
- name: group.name,
22
- scopeGrants: group.scopeGrants,
23
- })),
24
- };
25
- }
26
-
27
- async function isForeignReturn(
28
- request: Request,
29
- callerBusiness: string | undefined
30
- ): Promise<boolean> {
31
- const returnTo = new URL(request.url).searchParams.get("returnTo");
32
- if (!returnTo || !URL.canParse(returnTo)) return false;
33
- return !maySealFor(await hostOwner(returnTo), callerBusiness);
34
- }
35
-
36
- export function createImpersonationPersonasHandler(authConfig: Auth.Config) {
37
- return async function GET(request: Request) {
38
- const caller = await resolveAccessToken(authConfig, request);
39
- if ("error" in caller) return caller.error;
40
-
41
- const claims = tokenClaims(caller.accessToken);
42
- if (await isForeignReturn(request, claims.biz)) {
43
- return Response.json({ error: FOREIGN_BUSINESS_ERROR }, { status: 403 });
44
- }
45
- if (!personaRequired(claims)) {
46
- return new Response("Not found", { status: 404 });
47
- }
48
-
49
- const client = platformClient(caller.accessToken);
50
- if (!client) return platformNotConfigured();
51
-
52
- const [personas, apps] = await Promise.all([
53
- platformCall(client.listTestPersonas()),
54
- platformCall(client.getMeApps()),
55
- ]);
56
- if ("error" in personas) return personas.error;
57
- if ("error" in apps) return apps.error;
58
-
59
- return Response.json({
60
- personas: personas.value.map(persona),
61
- apps: apps.value,
62
- });
63
- };
64
- }