@snokam/auth 3.5.0 → 3.6.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 +6 -0
- package/package.json +4 -4
- package/src/app-sign-in/handlers.ts +17 -13
- package/src/app-sign-in/platform.ts +33 -22
- package/src/impersonation/claims.ts +16 -0
- package/src/impersonation/handlers.ts +35 -37
- package/src/impersonation/personas.ts +31 -27
- package/src/impersonation/renew.ts +30 -50
- package/src/impersonation/request.ts +38 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
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
|
+
# [3.6.0](https://github.com/snokam/components/compare/@snokam/auth@3.5.0...@snokam/auth@3.6.0) (2026-10-06)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- **snosky:** no prices, no terms page, unlinked /status in Snøkam look, better persona flow ([#2681](https://github.com/snokam/components/issues/2681)) ([b6dd5af](https://github.com/snokam/components/commit/b6dd5af68258c31f717888372fc7258d233c9e2e))
|
|
11
|
+
|
|
6
12
|
# [3.5.0](https://github.com/snokam/components/compare/@snokam/auth@3.4.0...@snokam/auth@3.5.0) (2026-10-06)
|
|
7
13
|
|
|
8
14
|
### Features
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snokam/auth",
|
|
3
3
|
"description": "Snøkam auth components",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.6.0",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
7
7
|
"files": [
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
"path-to-regexp": "^8.4.2",
|
|
29
29
|
"@snokam/identity": "1.1.0",
|
|
30
30
|
"@snokam/platform-client": "3.6.0",
|
|
31
|
-
"@snokam/tenant": "2.1.13",
|
|
32
|
-
"@snokam/ui": "4.0.0",
|
|
33
31
|
"@snokam/react-query": "1.1.0",
|
|
34
|
-
"@snokam/sanity-page-content": "4.4.2"
|
|
32
|
+
"@snokam/sanity-page-content": "4.4.2",
|
|
33
|
+
"@snokam/tenant": "2.1.13",
|
|
34
|
+
"@snokam/ui": "4.0.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"next": "16.3.3",
|
|
@@ -6,10 +6,8 @@ import {
|
|
|
6
6
|
requestOrigin,
|
|
7
7
|
} from "../session";
|
|
8
8
|
import { resolveAccessToken } from "../route-handler/token";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
readImpersonationSession,
|
|
12
|
-
} from "../impersonation/cookies";
|
|
9
|
+
import { readImpersonationSession } from "../impersonation/cookies";
|
|
10
|
+
import { tokenClaims } from "../impersonation/claims";
|
|
13
11
|
import {
|
|
14
12
|
impersonationPickerUrl,
|
|
15
13
|
impersonationRequired,
|
|
@@ -58,13 +56,18 @@ interface Signer {
|
|
|
58
56
|
accessToken: string;
|
|
59
57
|
}
|
|
60
58
|
|
|
61
|
-
function
|
|
59
|
+
function sameEmail(a: string | undefined, b: string): boolean {
|
|
60
|
+
return !!a && a.toLowerCase() === b.toLowerCase();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function personaSigner(request: Request, operator: string): Signer | null {
|
|
62
64
|
const session = readImpersonationSession(request);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
if (!session || session.expiresOn * 1000 <= Date.now()) return null;
|
|
66
|
+
|
|
67
|
+
const claims = tokenClaims(session.accessToken);
|
|
68
|
+
if (!claims.email || !sameEmail(claims.act, operator)) return null;
|
|
69
|
+
|
|
70
|
+
return { email: claims.email, accessToken: session.accessToken };
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
async function ownSigner(
|
|
@@ -82,7 +85,7 @@ function signerFor(
|
|
|
82
85
|
email: string
|
|
83
86
|
): Promise<Signer | null> {
|
|
84
87
|
if (impersonationRequired(authConfig.environment)) {
|
|
85
|
-
return Promise.resolve(personaSigner(request));
|
|
88
|
+
return Promise.resolve(personaSigner(request, email));
|
|
86
89
|
}
|
|
87
90
|
return ownSigner(authConfig, request, email);
|
|
88
91
|
}
|
|
@@ -175,7 +178,8 @@ export function createAppSignInAuthorizeHandler(authConfig: Auth.Config) {
|
|
|
175
178
|
const ticket = sealConsent(
|
|
176
179
|
{
|
|
177
180
|
...params,
|
|
178
|
-
|
|
181
|
+
clientName: preview.value.clientName ?? "",
|
|
182
|
+
businessName: preview.value.businessName ?? "",
|
|
179
183
|
business: new URL(params.resource).hostname,
|
|
180
184
|
state: query.get("state") ?? "",
|
|
181
185
|
email: signer.email,
|
|
@@ -227,7 +231,7 @@ export function createAppSignInApproveHandler(authConfig: Auth.Config) {
|
|
|
227
231
|
consent.state,
|
|
228
232
|
"error" in approved
|
|
229
233
|
? { error: approved.error }
|
|
230
|
-
: { code: approved.value.code }
|
|
234
|
+
: { code: approved.value.code ?? "" }
|
|
231
235
|
);
|
|
232
236
|
};
|
|
233
237
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { getManagedBearer } from "@snokam/identity";
|
|
2
|
+
import {
|
|
3
|
+
Configuration,
|
|
4
|
+
PlatformClient,
|
|
5
|
+
ResponseError,
|
|
6
|
+
type AuthorizationPreview,
|
|
7
|
+
type IssuedAuthorizationCode,
|
|
8
|
+
} from "@snokam/platform-client";
|
|
2
9
|
import type { AuthorizationParams } from "./consent";
|
|
3
10
|
|
|
4
|
-
export interface AuthorizationPreview {
|
|
5
|
-
clientName: string;
|
|
6
|
-
businessName: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
11
|
type Outcome<T> = { value: T } | { error: string };
|
|
10
12
|
|
|
11
13
|
const UNAVAILABLE = "temporarily_unavailable";
|
|
@@ -17,36 +19,45 @@ function apiBase(): string {
|
|
|
17
19
|
);
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
async function
|
|
21
|
-
|
|
22
|
-
params: AuthorizationParams,
|
|
23
|
-
userToken: string
|
|
22
|
+
async function asLoginSite<T>(
|
|
23
|
+
call: (client: PlatformClient) => Promise<T>
|
|
24
24
|
): Promise<Outcome<T>> {
|
|
25
25
|
const loginSite = await getManagedBearer();
|
|
26
26
|
if (!loginSite) return { error: UNAVAILABLE };
|
|
27
27
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
const client = new PlatformClient(
|
|
29
|
+
new Configuration({
|
|
30
|
+
basePath: apiBase(),
|
|
31
|
+
accessToken: async () => loginSite,
|
|
32
|
+
})
|
|
33
|
+
);
|
|
34
|
+
try {
|
|
35
|
+
return { value: await call(client) };
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (!(error instanceof ResponseError)) throw error;
|
|
38
|
+
const body = await error.response.json().catch(() => ({}));
|
|
39
|
+
return { error: String(body.error ?? "invalid_request") };
|
|
40
|
+
}
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
export function previewAuthorization(
|
|
41
44
|
params: AuthorizationParams,
|
|
42
45
|
userToken: string
|
|
43
46
|
): Promise<Outcome<AuthorizationPreview>> {
|
|
44
|
-
return
|
|
47
|
+
return asLoginSite((client) =>
|
|
48
|
+
client.previewOAuthAuthorization({
|
|
49
|
+
authorizationRequest: { ...params, userToken },
|
|
50
|
+
})
|
|
51
|
+
);
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
export function approveAuthorization(
|
|
48
55
|
params: AuthorizationParams,
|
|
49
56
|
userToken: string
|
|
50
|
-
): Promise<Outcome<
|
|
51
|
-
return
|
|
57
|
+
): Promise<Outcome<IssuedAuthorizationCode>> {
|
|
58
|
+
return asLoginSite((client) =>
|
|
59
|
+
client.approveOAuthAuthorization({
|
|
60
|
+
authorizationRequest: { ...params, userToken },
|
|
61
|
+
})
|
|
62
|
+
);
|
|
52
63
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface TokenClaims {
|
|
2
|
+
biz?: string;
|
|
3
|
+
email?: string;
|
|
4
|
+
act?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function tokenClaims(accessToken: string): TokenClaims {
|
|
8
|
+
try {
|
|
9
|
+
const payload = accessToken.split(".")[1] ?? "";
|
|
10
|
+
return JSON.parse(
|
|
11
|
+
Buffer.from(payload, "base64url").toString("utf8")
|
|
12
|
+
) as TokenClaims;
|
|
13
|
+
} catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -7,7 +7,12 @@ import {
|
|
|
7
7
|
} from "./cookies";
|
|
8
8
|
import { sealImpersonationTicket, openImpersonationTicket } from "./ticket";
|
|
9
9
|
import { requestOrigin, resolveCookieDomain } from "../session";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
platformCall,
|
|
12
|
+
platformClient,
|
|
13
|
+
platformNotConfigured,
|
|
14
|
+
requestHost,
|
|
15
|
+
} from "./request";
|
|
11
16
|
|
|
12
17
|
function registrable(host: string): string {
|
|
13
18
|
return host.split(":")[0].toLowerCase().split(".").slice(-2).join(".");
|
|
@@ -64,17 +69,12 @@ function handsOffToTarget(request: Request, target: string): boolean {
|
|
|
64
69
|
*/
|
|
65
70
|
export function createImpersonationStartHandler(authConfig: Auth.Config) {
|
|
66
71
|
return async function POST(request: Request) {
|
|
67
|
-
const base = platformBase();
|
|
68
|
-
if (!base) {
|
|
69
|
-
return new Response(
|
|
70
|
-
"NEXT_PUBLIC_PLATFORM_API_BASE_URL is not configured",
|
|
71
|
-
{ status: 500 }
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
72
|
const caller = await resolveAccessToken(authConfig, request);
|
|
76
73
|
if ("error" in caller) return caller.error;
|
|
77
74
|
|
|
75
|
+
const client = platformClient(caller.accessToken);
|
|
76
|
+
if (!client) return platformNotConfigured();
|
|
77
|
+
|
|
78
78
|
let body: {
|
|
79
79
|
targetEmail?: string;
|
|
80
80
|
targetName?: string;
|
|
@@ -91,23 +91,26 @@ export function createImpersonationStartHandler(authConfig: Auth.Config) {
|
|
|
91
91
|
return new Response("Invalid request body", { status: 400 });
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
const minted = await
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
94
|
+
const minted = await platformCall(
|
|
95
|
+
client.startImpersonation({
|
|
96
|
+
startImpersonationRequest: {
|
|
97
|
+
targetEmail: body.targetEmail,
|
|
98
|
+
targetName: body.targetName,
|
|
99
|
+
reason: body.reason,
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
);
|
|
103
|
+
if ("error" in minted) return minted.error;
|
|
104
|
+
|
|
105
|
+
const result = {
|
|
106
|
+
accessToken: minted.value.accessToken ?? "",
|
|
107
|
+
expiresOn: minted.value.expiresOn ?? 0,
|
|
108
|
+
tokenId: minted.value.tokenId ?? "",
|
|
109
|
+
targetEmail: minted.value.targetEmail ?? body.targetEmail ?? "",
|
|
110
|
+
targetName: minted.value.targetName,
|
|
111
|
+
targetImage: body.targetImage ?? null,
|
|
112
|
+
finance: minted.value.finance ?? false,
|
|
113
|
+
};
|
|
111
114
|
const summary = {
|
|
112
115
|
targetEmail: result.targetEmail,
|
|
113
116
|
targetName: result.targetName,
|
|
@@ -200,19 +203,14 @@ export function createImpersonationAdoptHandler(authConfig: Auth.Config) {
|
|
|
200
203
|
export function createImpersonationEndHandler(authConfig: Auth.Config) {
|
|
201
204
|
return async function POST(request: Request) {
|
|
202
205
|
const session = readImpersonationSession(request);
|
|
203
|
-
const base = platformBase();
|
|
204
206
|
|
|
205
|
-
if (session?.tokenId
|
|
207
|
+
if (session?.tokenId) {
|
|
206
208
|
const caller = await resolveAccessToken(authConfig, request);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
headers: { Authorization: `Bearer ${caller.accessToken}` },
|
|
213
|
-
}
|
|
214
|
-
).catch(() => undefined);
|
|
215
|
-
}
|
|
209
|
+
const client =
|
|
210
|
+
"error" in caller ? null : platformClient(caller.accessToken);
|
|
211
|
+
await client
|
|
212
|
+
?.endImpersonation({ tokenId: session.tokenId })
|
|
213
|
+
.catch(() => undefined);
|
|
216
214
|
}
|
|
217
215
|
|
|
218
216
|
const out = Response.json({ ok: true });
|
|
@@ -1,18 +1,25 @@
|
|
|
1
|
+
import type { ProtectedEmployee } from "@snokam/platform-client";
|
|
1
2
|
import { Auth } from "../types";
|
|
2
3
|
import { resolveAccessToken } from "../route-handler/token";
|
|
3
|
-
import {
|
|
4
|
+
import { tokenClaims } from "./claims";
|
|
5
|
+
import { platformCall, platformClient, platformNotConfigured } from "./request";
|
|
4
6
|
import { impersonationRequired } from "./required";
|
|
5
7
|
|
|
6
|
-
function
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
function persona(employee: ProtectedEmployee) {
|
|
9
|
+
return {
|
|
10
|
+
name: employee.name,
|
|
11
|
+
email: employee.email,
|
|
12
|
+
position: employee.position,
|
|
13
|
+
about: employee.about,
|
|
14
|
+
active: employee.active,
|
|
15
|
+
isBot: employee.isBot,
|
|
16
|
+
image: { asset: { url: employee.image?.asset?.url } },
|
|
17
|
+
scopeGrants: employee.scopeGrants,
|
|
18
|
+
groups: (employee.groups ?? []).map((group) => ({
|
|
19
|
+
name: group.name,
|
|
20
|
+
scopeGrants: group.scopeGrants,
|
|
21
|
+
})),
|
|
22
|
+
};
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
export function createImpersonationPersonasHandler(authConfig: Auth.Config) {
|
|
@@ -21,32 +28,29 @@ export function createImpersonationPersonasHandler(authConfig: Auth.Config) {
|
|
|
21
28
|
return new Response("Not found", { status: 404 });
|
|
22
29
|
}
|
|
23
30
|
|
|
24
|
-
const base = platformBase();
|
|
25
|
-
if (!base) {
|
|
26
|
-
return new Response(
|
|
27
|
-
"NEXT_PUBLIC_PLATFORM_API_BASE_URL is not configured",
|
|
28
|
-
{ status: 500 }
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
31
|
const caller = await resolveAccessToken(authConfig, request);
|
|
33
32
|
if ("error" in caller) return caller.error;
|
|
34
33
|
|
|
35
|
-
const
|
|
34
|
+
const client = platformClient(caller.accessToken);
|
|
35
|
+
if (!client) return platformNotConfigured();
|
|
36
|
+
|
|
37
|
+
const businessId = tokenClaims(caller.accessToken).biz;
|
|
36
38
|
if (!businessId) {
|
|
37
39
|
return new Response("The signed-in account has no business", {
|
|
38
40
|
status: 409,
|
|
39
41
|
});
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
const employees = await
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
);
|
|
46
|
-
if (
|
|
44
|
+
const [employees, apps] = await Promise.all([
|
|
45
|
+
platformCall(client.listBusinessEmployees({ id: businessId })),
|
|
46
|
+
platformCall(client.getMeApps()),
|
|
47
|
+
]);
|
|
48
|
+
if ("error" in employees) return employees.error;
|
|
49
|
+
if ("error" in apps) return apps.error;
|
|
47
50
|
|
|
48
|
-
return
|
|
49
|
-
|
|
51
|
+
return Response.json({
|
|
52
|
+
personas: employees.value.map(persona),
|
|
53
|
+
apps: apps.value,
|
|
50
54
|
});
|
|
51
55
|
};
|
|
52
56
|
}
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Configuration,
|
|
3
|
-
PlatformClient,
|
|
4
|
-
ResponseError,
|
|
5
|
-
} from "@snokam/platform-client";
|
|
6
1
|
import { Auth } from "../types";
|
|
7
2
|
import { resolveAccessToken } from "../route-handler/token";
|
|
8
3
|
import {
|
|
@@ -10,7 +5,12 @@ import {
|
|
|
10
5
|
readImpersonationActive,
|
|
11
6
|
readImpersonationSession,
|
|
12
7
|
} from "./cookies";
|
|
13
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
platformCall,
|
|
10
|
+
platformClient,
|
|
11
|
+
platformNotConfigured,
|
|
12
|
+
requestHost,
|
|
13
|
+
} from "./request";
|
|
14
14
|
|
|
15
15
|
export function createImpersonationRenewHandler(authConfig: Auth.Config) {
|
|
16
16
|
return async function POST(request: Request) {
|
|
@@ -20,56 +20,36 @@ export function createImpersonationRenewHandler(authConfig: Auth.Config) {
|
|
|
20
20
|
return new Response("No impersonation session", { status: 404 });
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
const basePath = platformBase();
|
|
24
|
-
if (!basePath) {
|
|
25
|
-
return new Response(
|
|
26
|
-
"NEXT_PUBLIC_PLATFORM_API_BASE_URL is not configured",
|
|
27
|
-
{
|
|
28
|
-
status: 500,
|
|
29
|
-
}
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
23
|
const caller = await resolveAccessToken(authConfig, request);
|
|
34
24
|
if ("error" in caller) return caller.error;
|
|
35
25
|
|
|
36
|
-
const client =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
})
|
|
26
|
+
const client = platformClient(caller.accessToken);
|
|
27
|
+
if (!client) return platformNotConfigured();
|
|
28
|
+
|
|
29
|
+
const renewal = await platformCall(
|
|
30
|
+
client.renewImpersonation({ tokenId: session.tokenId })
|
|
41
31
|
);
|
|
32
|
+
if ("error" in renewal) return renewal.error;
|
|
42
33
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (!renewed.accessToken || !renewed.expiresOn) {
|
|
48
|
-
return new Response("Incomplete impersonation renewal", {
|
|
49
|
-
status: 502,
|
|
50
|
-
});
|
|
51
|
-
}
|
|
34
|
+
const renewed = renewal.value;
|
|
35
|
+
if (!renewed.accessToken || !renewed.expiresOn) {
|
|
36
|
+
return new Response("Incomplete impersonation renewal", { status: 502 });
|
|
37
|
+
}
|
|
52
38
|
|
|
53
|
-
|
|
54
|
-
|
|
39
|
+
const out = Response.json({
|
|
40
|
+
tokenId: session.tokenId,
|
|
41
|
+
expiresOn: renewed.expiresOn,
|
|
42
|
+
});
|
|
43
|
+
const cookies = buildImpersonationCookies(
|
|
44
|
+
{
|
|
45
|
+
...active,
|
|
46
|
+
accessToken: renewed.accessToken,
|
|
55
47
|
expiresOn: renewed.expiresOn,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
finance: renewed.finance ?? false,
|
|
63
|
-
},
|
|
64
|
-
requestHost(request)
|
|
65
|
-
);
|
|
66
|
-
for (const cookie of cookies) out.headers.append("Set-Cookie", cookie);
|
|
67
|
-
return out;
|
|
68
|
-
} catch (error) {
|
|
69
|
-
if (error instanceof ResponseError) {
|
|
70
|
-
return platformErrorResponse(error.response);
|
|
71
|
-
}
|
|
72
|
-
throw error;
|
|
73
|
-
}
|
|
48
|
+
finance: renewed.finance ?? false,
|
|
49
|
+
},
|
|
50
|
+
requestHost(request)
|
|
51
|
+
);
|
|
52
|
+
for (const cookie of cookies) out.headers.append("Set-Cookie", cookie);
|
|
53
|
+
return out;
|
|
74
54
|
};
|
|
75
55
|
}
|
|
@@ -1,9 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Configuration,
|
|
3
|
+
PlatformClient,
|
|
4
|
+
ResponseError,
|
|
5
|
+
} from "@snokam/platform-client";
|
|
6
|
+
|
|
1
7
|
export function platformBase(): string | null {
|
|
2
8
|
return (
|
|
3
9
|
process.env.NEXT_PUBLIC_PLATFORM_API_BASE_URL?.replace(/\/$/, "") ?? null
|
|
4
10
|
);
|
|
5
11
|
}
|
|
6
12
|
|
|
13
|
+
export function platformClient(accessToken: string): PlatformClient | null {
|
|
14
|
+
const basePath = platformBase();
|
|
15
|
+
if (!basePath) return null;
|
|
16
|
+
return new PlatformClient(
|
|
17
|
+
new Configuration({
|
|
18
|
+
basePath,
|
|
19
|
+
accessToken: async () => `Bearer ${accessToken}`,
|
|
20
|
+
})
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function platformNotConfigured(): Response {
|
|
25
|
+
return new Response("NEXT_PUBLIC_PLATFORM_API_BASE_URL is not configured", {
|
|
26
|
+
status: 500,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
7
30
|
export function requestHost(request: Request): string | null {
|
|
8
31
|
return request.headers.get("x-forwarded-host") ?? request.headers.get("host");
|
|
9
32
|
}
|
|
@@ -24,3 +47,18 @@ export async function platformErrorResponse(
|
|
|
24
47
|
status: response.status,
|
|
25
48
|
});
|
|
26
49
|
}
|
|
50
|
+
|
|
51
|
+
export type PlatformResult<T> = { value: T } | { error: Response };
|
|
52
|
+
|
|
53
|
+
export async function platformCall<T>(
|
|
54
|
+
call: Promise<T>
|
|
55
|
+
): Promise<PlatformResult<T>> {
|
|
56
|
+
try {
|
|
57
|
+
return { value: await call };
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error instanceof ResponseError) {
|
|
60
|
+
return { error: await platformErrorResponse(error.response) };
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|