|
| 1 | +import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime"; |
1 | 2 | import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http"; |
2 | 3 | import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; |
3 | 4 | import { createMSTeamsHttpError } from "./http-error.js"; |
@@ -15,7 +16,7 @@ const EXPIRY_BUFFER_MS = 5 * 60 * 1000;
|
15 | 16 | type MSTeamsTokenResponse = { |
16 | 17 | access_token: string; |
17 | 18 | refresh_token?: string; |
18 | | -expires_in: number; |
| 19 | +expiresAt: number; |
19 | 20 | scope?: string; |
20 | 21 | }; |
21 | 22 | |
@@ -40,6 +41,42 @@ function createMSTeamsTokenBody(params: {
|
40 | 41 | return body; |
41 | 42 | } |
42 | 43 | |
| 44 | +function resolveMSTeamsTokenExpiresAt(value: unknown): number | undefined { |
| 45 | +const expiresInSeconds = parseStrictPositiveInteger(value); |
| 46 | +if (expiresInSeconds === undefined) { |
| 47 | +return undefined; |
| 48 | +} |
| 49 | + |
| 50 | +const lifetimeMs = expiresInSeconds * 1000; |
| 51 | +const expiresAt = Date.now() + lifetimeMs - EXPIRY_BUFFER_MS; |
| 52 | +return Number.isSafeInteger(lifetimeMs) && Number.isSafeInteger(expiresAt) |
| 53 | + ? expiresAt |
| 54 | + : undefined; |
| 55 | +} |
| 56 | + |
| 57 | +function parseMSTeamsTokenResponse( |
| 58 | +data: Record<string, unknown>, |
| 59 | +failureLabel: string, |
| 60 | +): MSTeamsTokenResponse { |
| 61 | +const expiresAt = resolveMSTeamsTokenExpiresAt(data.expires_in); |
| 62 | +if ( |
| 63 | +typeof data.access_token !== "string" || |
| 64 | +!data.access_token || |
| 65 | +expiresAt === undefined || |
| 66 | +(data.refresh_token !== undefined && typeof data.refresh_token !== "string") || |
| 67 | +(data.scope !== undefined && typeof data.scope !== "string") |
| 68 | +) { |
| 69 | +throw new Error(`MSTeams ${failureLabel} failed: invalid token response fields`); |
| 70 | +} |
| 71 | + |
| 72 | +return { |
| 73 | +access_token: data.access_token, |
| 74 | +refresh_token: data.refresh_token, |
| 75 | + expiresAt, |
| 76 | +scope: data.scope, |
| 77 | +}; |
| 78 | +} |
| 79 | + |
43 | 80 | async function fetchMSTeamsTokens(params: { |
44 | 81 | tokenUrl: string; |
45 | 82 | body: URLSearchParams; |
@@ -66,10 +103,11 @@ async function fetchMSTeamsTokens(params: {
|
66 | 103 | if (!response.ok) { |
67 | 104 | throw await createMSTeamsHttpError(response, `MSTeams ${params.failureLabel} failed`); |
68 | 105 | } |
69 | | -return await readProviderJsonResponse<MSTeamsTokenResponse>( |
| 106 | +const data = await readProviderJsonResponse<Record<string, unknown>>( |
70 | 107 | response, |
71 | 108 | `MSTeams ${params.failureLabel} failed`, |
72 | 109 | ); |
| 110 | +return parseMSTeamsTokenResponse(data, params.failureLabel); |
73 | 111 | } finally { |
74 | 112 | await release(); |
75 | 113 | } |
@@ -104,7 +142,7 @@ async function requestMSTeamsDelegatedTokens(params: {
|
104 | 142 | return { |
105 | 143 | accessToken: data.access_token, |
106 | 144 | refreshToken: params.resolveRefreshToken(data), |
107 | | -expiresAt: Date.now() + data.expires_in * 1000 - EXPIRY_BUFFER_MS, |
| 145 | +expiresAt: data.expiresAt, |
108 | 146 | scopes: data.scope ? data.scope.split(" ") : [...scopes], |
109 | 147 | }; |
110 | 148 | } |
|