@slates/oauth-microsoft 1.0.0-rc.1 → 1.0.0-rc.2

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/README.md CHANGED
@@ -1,3 +1,3 @@
1
1
  # @slates/oauth-microsoft
2
2
 
3
- Shared helpers for Microsoft OAuth flows in Slates.
3
+ Shared helpers for Microsoft OAuth and Graph flows in Slates.
package/dist/index.cjs CHANGED
@@ -20,12 +20,66 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ MICROSOFT_GRAPH_BASE: () => MICROSOFT_GRAPH_BASE,
24
+ MICROSOFT_LOGIN_BASE: () => MICROSOFT_LOGIN_BASE,
23
25
  MICROSOFT_OAUTH_INTEGRATION_KEYS: () => MICROSOFT_OAUTH_INTEGRATION_KEYS,
26
+ buildMicrosoftGraphUploadBody: () => buildMicrosoftGraphUploadBody,
27
+ createMicrosoftGraphOauth: () => createMicrosoftGraphOauth,
28
+ mapAzureDevOpsScopes: () => mapAzureDevOpsScopes,
24
29
  normalizeMicrosoftRedirectUri: () => normalizeMicrosoftRedirectUri,
25
30
  normalizeMicrosoftRedirectUriForIntegration: () => normalizeMicrosoftRedirectUriForIntegration,
31
+ resolveMicrosoftTenant: () => resolveMicrosoftTenant,
26
32
  usesMicrosoftOAuth: () => usesMicrosoftOAuth
27
33
  });
28
34
  module.exports = __toCommonJS(index_exports);
35
+ var import_slates = require("slates");
36
+ var MICROSOFT_LOGIN_BASE = "https://login.microsoftonline.com";
37
+ var MICROSOFT_GRAPH_BASE = "https://graph.microsoft.com/v1.0";
38
+ var AZURE_DEVOPS_RESOURCE = "499b84ac-1321-427f-aa17-267ca6975798";
39
+ var SECONDS_TO_MS = 1e3;
40
+ var MICROSOFT_GRAPH_TEXT_LIKE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
41
+ ".txt",
42
+ ".md",
43
+ ".csv",
44
+ ".json",
45
+ ".xml",
46
+ ".html",
47
+ ".htm",
48
+ ".js",
49
+ ".jsx",
50
+ ".ts",
51
+ ".tsx",
52
+ ".css",
53
+ ".svg",
54
+ ".yaml",
55
+ ".yml"
56
+ ]);
57
+ var MICROSOFT_GRAPH_BASE64_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;
58
+ var getMicrosoftTokenOutput = (data, currentRefreshToken) => ({
59
+ token: data.access_token,
60
+ refreshToken: data.refresh_token ?? currentRefreshToken,
61
+ expiresAt: data.expires_in ? new Date(Date.now() + data.expires_in * SECONDS_TO_MS).toISOString() : void 0
62
+ });
63
+ var hasTextLikeFileExtension = (fileName) => {
64
+ let dotIndex = fileName.lastIndexOf(".");
65
+ return dotIndex >= 0 && MICROSOFT_GRAPH_TEXT_LIKE_FILE_EXTENSIONS.has(fileName.slice(dotIndex).toLowerCase());
66
+ };
67
+ var isTextLikeContentType = (contentType) => {
68
+ if (!contentType) {
69
+ return false;
70
+ }
71
+ let normalized = contentType.toLowerCase();
72
+ if (normalized.includes("openxmlformats-officedocument")) {
73
+ return false;
74
+ }
75
+ return normalized.startsWith("text/") || ["json", "xml", "javascript", "typescript", "svg", "x-www-form-urlencoded"].some(
76
+ (fragment) => normalized.includes(fragment)
77
+ );
78
+ };
79
+ var looksLikeBase64 = (content) => {
80
+ let normalized = content.replace(/\s+/g, "");
81
+ return !!normalized && normalized.length % 4 === 0 && MICROSOFT_GRAPH_BASE64_PATTERN.test(normalized);
82
+ };
29
83
  var MICROSOFT_OAUTH_INTEGRATION_KEYS = /* @__PURE__ */ new Set([
30
84
  "azure-blob-storage",
31
85
  "azure-devops",
@@ -52,10 +106,147 @@ var normalizeMicrosoftRedirectUri = (redirectUri) => {
52
106
  return url.toString();
53
107
  };
54
108
  var normalizeMicrosoftRedirectUriForIntegration = (integration, redirectUri) => usesMicrosoftOAuth(integration) ? normalizeMicrosoftRedirectUri(redirectUri) : redirectUri;
109
+ var resolveMicrosoftTenant = (tenantId, defaultTenant) => {
110
+ if (typeof tenantId !== "string") {
111
+ return defaultTenant;
112
+ }
113
+ let normalizedTenant = tenantId.trim();
114
+ return normalizedTenant || defaultTenant;
115
+ };
116
+ var mapAzureDevOpsScopes = (scopes) => scopes.map((scope) => `${AZURE_DEVOPS_RESOURCE}/${scope}`);
117
+ var buildMicrosoftGraphUploadBody = (fileName, content, contentType) => {
118
+ if (isTextLikeContentType(contentType) || hasTextLikeFileExtension(fileName) || !looksLikeBase64(content)) {
119
+ return content;
120
+ }
121
+ return Buffer.from(content.replace(/\s+/g, ""), "base64");
122
+ };
123
+ var defaultGraphProfile = {
124
+ baseURL: MICROSOFT_GRAPH_BASE,
125
+ path: "/me",
126
+ mapProfile: (data) => {
127
+ let user = data ?? {};
128
+ return {
129
+ id: user.id,
130
+ email: user.mail || user.userPrincipalName,
131
+ name: user.displayName
132
+ };
133
+ }
134
+ };
135
+ var tokenClientCache = /* @__PURE__ */ new Map();
136
+ var getCachedTokenClient = (resolvedTenant) => {
137
+ let cached = tokenClientCache.get(resolvedTenant);
138
+ if (cached) return cached;
139
+ let client = (0, import_slates.createAxios)({
140
+ baseURL: `${MICROSOFT_LOGIN_BASE}/${resolvedTenant}/oauth2/v2.0`
141
+ });
142
+ tokenClientCache.set(resolvedTenant, client);
143
+ return client;
144
+ };
145
+ var createMicrosoftGraphOauth = ({
146
+ name,
147
+ key,
148
+ tenant,
149
+ scopes,
150
+ allowTenantInput = false,
151
+ missingRefreshTokenMessage = "No refresh token available. Ensure offline_access scope is requested.",
152
+ normalizeRedirectUri: shouldNormalizeRedirectUri = false,
153
+ scopeMapper,
154
+ extraScopes,
155
+ onMissingRefreshToken = "throw",
156
+ profile = defaultGraphProfile
157
+ }) => {
158
+ let profileAxios = (0, import_slates.createAxios)({ baseURL: profile.baseURL });
159
+ let getTenant = (ctx) => allowTenantInput ? resolveMicrosoftTenant(ctx.input?.tenantId, tenant) : tenant;
160
+ let getRedirectUri = (redirectUri) => shouldNormalizeRedirectUri ? normalizeMicrosoftRedirectUri(redirectUri) : redirectUri;
161
+ let buildScopeParam = (rawScopes) => {
162
+ let mapped = scopeMapper ? scopeMapper(rawScopes) : rawScopes;
163
+ return extraScopes ? [...mapped, ...extraScopes].join(" ") : mapped.join(" ");
164
+ };
165
+ return {
166
+ type: "auth.oauth",
167
+ name,
168
+ key,
169
+ scopes,
170
+ getAuthorizationUrl: async (ctx) => {
171
+ let resolvedTenant = getTenant(ctx);
172
+ let params = new URLSearchParams({
173
+ client_id: ctx.clientId,
174
+ response_type: "code",
175
+ redirect_uri: getRedirectUri(ctx.redirectUri),
176
+ scope: buildScopeParam(ctx.scopes),
177
+ state: ctx.state,
178
+ response_mode: "query"
179
+ });
180
+ return {
181
+ url: `${MICROSOFT_LOGIN_BASE}/${resolvedTenant}/oauth2/v2.0/authorize?${params.toString()}`
182
+ };
183
+ },
184
+ handleCallback: async (ctx) => {
185
+ let tokenClient = getCachedTokenClient(getTenant(ctx));
186
+ let response = await tokenClient.post(
187
+ "/token",
188
+ new URLSearchParams({
189
+ client_id: ctx.clientId,
190
+ client_secret: ctx.clientSecret,
191
+ code: ctx.code,
192
+ redirect_uri: getRedirectUri(ctx.redirectUri),
193
+ grant_type: "authorization_code",
194
+ scope: buildScopeParam(ctx.scopes)
195
+ }).toString(),
196
+ {
197
+ headers: { "Content-Type": "application/x-www-form-urlencoded" }
198
+ }
199
+ );
200
+ return {
201
+ output: getMicrosoftTokenOutput(response.data)
202
+ };
203
+ },
204
+ handleTokenRefresh: async (ctx) => {
205
+ if (!ctx.output.refreshToken) {
206
+ if (onMissingRefreshToken === "preserve") {
207
+ return { output: ctx.output };
208
+ }
209
+ throw new Error(missingRefreshTokenMessage);
210
+ }
211
+ let tokenClient = getCachedTokenClient(getTenant(ctx));
212
+ let response = await tokenClient.post(
213
+ "/token",
214
+ new URLSearchParams({
215
+ client_id: ctx.clientId,
216
+ client_secret: ctx.clientSecret,
217
+ refresh_token: ctx.output.refreshToken,
218
+ grant_type: "refresh_token",
219
+ scope: buildScopeParam(ctx.scopes)
220
+ }).toString(),
221
+ {
222
+ headers: { "Content-Type": "application/x-www-form-urlencoded" }
223
+ }
224
+ );
225
+ return {
226
+ output: getMicrosoftTokenOutput(
227
+ response.data,
228
+ ctx.output.refreshToken
229
+ )
230
+ };
231
+ },
232
+ getProfile: async (ctx) => {
233
+ let response = await profileAxios.get(profile.path, {
234
+ headers: { Authorization: `Bearer ${ctx.output.token}` }
235
+ });
236
+ return { profile: profile.mapProfile(response.data) };
237
+ }
238
+ };
239
+ };
55
240
  // Annotate the CommonJS export names for ESM import in node:
56
241
  0 && (module.exports = {
242
+ MICROSOFT_GRAPH_BASE,
243
+ MICROSOFT_LOGIN_BASE,
57
244
  MICROSOFT_OAUTH_INTEGRATION_KEYS,
245
+ buildMicrosoftGraphUploadBody,
246
+ createMicrosoftGraphOauth,
247
+ mapAzureDevOpsScopes,
58
248
  normalizeMicrosoftRedirectUri,
59
249
  normalizeMicrosoftRedirectUriForIntegration,
250
+ resolveMicrosoftTenant,
60
251
  usesMicrosoftOAuth
61
252
  });
package/dist/index.d.cts CHANGED
@@ -1,6 +1,100 @@
1
+ declare let MICROSOFT_LOGIN_BASE: string;
2
+ declare let MICROSOFT_GRAPH_BASE: string;
3
+ type MicrosoftOauthScope = {
4
+ title: string;
5
+ description: string;
6
+ scope: string;
7
+ defaultChecked?: boolean;
8
+ };
9
+ type MicrosoftGraphProfile = {
10
+ id?: string;
11
+ email?: string;
12
+ name?: string;
13
+ imageUrl?: string;
14
+ };
15
+ type MicrosoftGraphProfileOptions = {
16
+ baseURL: string;
17
+ path: string;
18
+ mapProfile: (data: unknown) => MicrosoftGraphProfile;
19
+ };
20
+ type MicrosoftGraphOauthOptions = {
21
+ name: string;
22
+ key: string;
23
+ tenant: string;
24
+ scopes: MicrosoftOauthScope[];
25
+ allowTenantInput?: boolean;
26
+ missingRefreshTokenMessage?: string;
27
+ /** Normalize loopback redirect URIs for Microsoft app registration compatibility. */
28
+ normalizeRedirectUri?: boolean;
29
+ /** Transforms the raw scope list before it is sent to the token endpoint. */
30
+ scopeMapper?: (scopes: string[]) => string[];
31
+ /** Scopes appended after mapping (e.g. `offline_access` for Azure DevOps). */
32
+ extraScopes?: string[];
33
+ /** When no refresh token is stored, throw (default) or preserve the existing output. */
34
+ onMissingRefreshToken?: 'throw' | 'preserve';
35
+ /** Custom profile endpoint. Defaults to Microsoft Graph `/me`. */
36
+ profile?: MicrosoftGraphProfileOptions;
37
+ };
38
+ type MicrosoftGraphOauthInput = {
39
+ tenantId?: unknown;
40
+ };
41
+ type MicrosoftGraphAuthorizationUrlContext = {
42
+ clientId: string;
43
+ clientSecret: string;
44
+ redirectUri: string;
45
+ scopes: string[];
46
+ state: string;
47
+ input?: MicrosoftGraphOauthInput;
48
+ };
49
+ type MicrosoftGraphCallbackContext = {
50
+ clientId: string;
51
+ clientSecret: string;
52
+ redirectUri: string;
53
+ scopes: string[];
54
+ code: string;
55
+ input?: MicrosoftGraphOauthInput;
56
+ };
57
+ type MicrosoftGraphOauthOutput = {
58
+ token: string;
59
+ refreshToken?: string;
60
+ expiresAt?: string;
61
+ };
62
+ type MicrosoftGraphTokenRefreshContext = {
63
+ clientId: string;
64
+ clientSecret: string;
65
+ scopes: string[];
66
+ input?: MicrosoftGraphOauthInput;
67
+ output: MicrosoftGraphOauthOutput;
68
+ };
69
+ type MicrosoftGraphProfileContext = {
70
+ output: {
71
+ token: string;
72
+ };
73
+ };
1
74
  declare let MICROSOFT_OAUTH_INTEGRATION_KEYS: Set<string>;
2
75
  declare let usesMicrosoftOAuth: (integration: string) => boolean;
3
76
  declare let normalizeMicrosoftRedirectUri: (redirectUri: string) => string;
4
77
  declare let normalizeMicrosoftRedirectUriForIntegration: (integration: string, redirectUri: string) => string;
78
+ declare let resolveMicrosoftTenant: (tenantId: unknown, defaultTenant: string) => string;
79
+ declare let mapAzureDevOpsScopes: (scopes: string[]) => string[];
80
+ declare let buildMicrosoftGraphUploadBody: (fileName: string, content: string, contentType?: string) => string | Buffer<ArrayBuffer>;
81
+ declare let createMicrosoftGraphOauth: ({ name, key, tenant, scopes, allowTenantInput, missingRefreshTokenMessage, normalizeRedirectUri: shouldNormalizeRedirectUri, scopeMapper, extraScopes, onMissingRefreshToken, profile }: MicrosoftGraphOauthOptions) => {
82
+ type: "auth.oauth";
83
+ name: string;
84
+ key: string;
85
+ scopes: MicrosoftOauthScope[];
86
+ getAuthorizationUrl: (ctx: MicrosoftGraphAuthorizationUrlContext) => Promise<{
87
+ url: string;
88
+ }>;
89
+ handleCallback: (ctx: MicrosoftGraphCallbackContext) => Promise<{
90
+ output: MicrosoftGraphOauthOutput;
91
+ }>;
92
+ handleTokenRefresh: (ctx: MicrosoftGraphTokenRefreshContext) => Promise<{
93
+ output: MicrosoftGraphOauthOutput;
94
+ }>;
95
+ getProfile: (ctx: MicrosoftGraphProfileContext) => Promise<{
96
+ profile: MicrosoftGraphProfile;
97
+ }>;
98
+ };
5
99
 
6
- export { MICROSOFT_OAUTH_INTEGRATION_KEYS, normalizeMicrosoftRedirectUri, normalizeMicrosoftRedirectUriForIntegration, usesMicrosoftOAuth };
100
+ export { MICROSOFT_GRAPH_BASE, MICROSOFT_LOGIN_BASE, MICROSOFT_OAUTH_INTEGRATION_KEYS, type MicrosoftGraphAuthorizationUrlContext, type MicrosoftGraphCallbackContext, type MicrosoftGraphOauthInput, type MicrosoftGraphOauthOptions, type MicrosoftGraphOauthOutput, type MicrosoftGraphProfile, type MicrosoftGraphProfileContext, type MicrosoftGraphProfileOptions, type MicrosoftGraphTokenRefreshContext, type MicrosoftOauthScope, buildMicrosoftGraphUploadBody, createMicrosoftGraphOauth, mapAzureDevOpsScopes, normalizeMicrosoftRedirectUri, normalizeMicrosoftRedirectUriForIntegration, resolveMicrosoftTenant, usesMicrosoftOAuth };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,100 @@
1
+ declare let MICROSOFT_LOGIN_BASE: string;
2
+ declare let MICROSOFT_GRAPH_BASE: string;
3
+ type MicrosoftOauthScope = {
4
+ title: string;
5
+ description: string;
6
+ scope: string;
7
+ defaultChecked?: boolean;
8
+ };
9
+ type MicrosoftGraphProfile = {
10
+ id?: string;
11
+ email?: string;
12
+ name?: string;
13
+ imageUrl?: string;
14
+ };
15
+ type MicrosoftGraphProfileOptions = {
16
+ baseURL: string;
17
+ path: string;
18
+ mapProfile: (data: unknown) => MicrosoftGraphProfile;
19
+ };
20
+ type MicrosoftGraphOauthOptions = {
21
+ name: string;
22
+ key: string;
23
+ tenant: string;
24
+ scopes: MicrosoftOauthScope[];
25
+ allowTenantInput?: boolean;
26
+ missingRefreshTokenMessage?: string;
27
+ /** Normalize loopback redirect URIs for Microsoft app registration compatibility. */
28
+ normalizeRedirectUri?: boolean;
29
+ /** Transforms the raw scope list before it is sent to the token endpoint. */
30
+ scopeMapper?: (scopes: string[]) => string[];
31
+ /** Scopes appended after mapping (e.g. `offline_access` for Azure DevOps). */
32
+ extraScopes?: string[];
33
+ /** When no refresh token is stored, throw (default) or preserve the existing output. */
34
+ onMissingRefreshToken?: 'throw' | 'preserve';
35
+ /** Custom profile endpoint. Defaults to Microsoft Graph `/me`. */
36
+ profile?: MicrosoftGraphProfileOptions;
37
+ };
38
+ type MicrosoftGraphOauthInput = {
39
+ tenantId?: unknown;
40
+ };
41
+ type MicrosoftGraphAuthorizationUrlContext = {
42
+ clientId: string;
43
+ clientSecret: string;
44
+ redirectUri: string;
45
+ scopes: string[];
46
+ state: string;
47
+ input?: MicrosoftGraphOauthInput;
48
+ };
49
+ type MicrosoftGraphCallbackContext = {
50
+ clientId: string;
51
+ clientSecret: string;
52
+ redirectUri: string;
53
+ scopes: string[];
54
+ code: string;
55
+ input?: MicrosoftGraphOauthInput;
56
+ };
57
+ type MicrosoftGraphOauthOutput = {
58
+ token: string;
59
+ refreshToken?: string;
60
+ expiresAt?: string;
61
+ };
62
+ type MicrosoftGraphTokenRefreshContext = {
63
+ clientId: string;
64
+ clientSecret: string;
65
+ scopes: string[];
66
+ input?: MicrosoftGraphOauthInput;
67
+ output: MicrosoftGraphOauthOutput;
68
+ };
69
+ type MicrosoftGraphProfileContext = {
70
+ output: {
71
+ token: string;
72
+ };
73
+ };
1
74
  declare let MICROSOFT_OAUTH_INTEGRATION_KEYS: Set<string>;
2
75
  declare let usesMicrosoftOAuth: (integration: string) => boolean;
3
76
  declare let normalizeMicrosoftRedirectUri: (redirectUri: string) => string;
4
77
  declare let normalizeMicrosoftRedirectUriForIntegration: (integration: string, redirectUri: string) => string;
78
+ declare let resolveMicrosoftTenant: (tenantId: unknown, defaultTenant: string) => string;
79
+ declare let mapAzureDevOpsScopes: (scopes: string[]) => string[];
80
+ declare let buildMicrosoftGraphUploadBody: (fileName: string, content: string, contentType?: string) => string | Buffer<ArrayBuffer>;
81
+ declare let createMicrosoftGraphOauth: ({ name, key, tenant, scopes, allowTenantInput, missingRefreshTokenMessage, normalizeRedirectUri: shouldNormalizeRedirectUri, scopeMapper, extraScopes, onMissingRefreshToken, profile }: MicrosoftGraphOauthOptions) => {
82
+ type: "auth.oauth";
83
+ name: string;
84
+ key: string;
85
+ scopes: MicrosoftOauthScope[];
86
+ getAuthorizationUrl: (ctx: MicrosoftGraphAuthorizationUrlContext) => Promise<{
87
+ url: string;
88
+ }>;
89
+ handleCallback: (ctx: MicrosoftGraphCallbackContext) => Promise<{
90
+ output: MicrosoftGraphOauthOutput;
91
+ }>;
92
+ handleTokenRefresh: (ctx: MicrosoftGraphTokenRefreshContext) => Promise<{
93
+ output: MicrosoftGraphOauthOutput;
94
+ }>;
95
+ getProfile: (ctx: MicrosoftGraphProfileContext) => Promise<{
96
+ profile: MicrosoftGraphProfile;
97
+ }>;
98
+ };
5
99
 
6
- export { MICROSOFT_OAUTH_INTEGRATION_KEYS, normalizeMicrosoftRedirectUri, normalizeMicrosoftRedirectUriForIntegration, usesMicrosoftOAuth };
100
+ export { MICROSOFT_GRAPH_BASE, MICROSOFT_LOGIN_BASE, MICROSOFT_OAUTH_INTEGRATION_KEYS, type MicrosoftGraphAuthorizationUrlContext, type MicrosoftGraphCallbackContext, type MicrosoftGraphOauthInput, type MicrosoftGraphOauthOptions, type MicrosoftGraphOauthOutput, type MicrosoftGraphProfile, type MicrosoftGraphProfileContext, type MicrosoftGraphProfileOptions, type MicrosoftGraphTokenRefreshContext, type MicrosoftOauthScope, buildMicrosoftGraphUploadBody, createMicrosoftGraphOauth, mapAzureDevOpsScopes, normalizeMicrosoftRedirectUri, normalizeMicrosoftRedirectUriForIntegration, resolveMicrosoftTenant, usesMicrosoftOAuth };
@@ -1,4 +1,52 @@
1
1
  // src/index.ts
2
+ import { createAxios } from "slates";
3
+ var MICROSOFT_LOGIN_BASE = "https://login.microsoftonline.com";
4
+ var MICROSOFT_GRAPH_BASE = "https://graph.microsoft.com/v1.0";
5
+ var AZURE_DEVOPS_RESOURCE = "499b84ac-1321-427f-aa17-267ca6975798";
6
+ var SECONDS_TO_MS = 1e3;
7
+ var MICROSOFT_GRAPH_TEXT_LIKE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
8
+ ".txt",
9
+ ".md",
10
+ ".csv",
11
+ ".json",
12
+ ".xml",
13
+ ".html",
14
+ ".htm",
15
+ ".js",
16
+ ".jsx",
17
+ ".ts",
18
+ ".tsx",
19
+ ".css",
20
+ ".svg",
21
+ ".yaml",
22
+ ".yml"
23
+ ]);
24
+ var MICROSOFT_GRAPH_BASE64_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;
25
+ var getMicrosoftTokenOutput = (data, currentRefreshToken) => ({
26
+ token: data.access_token,
27
+ refreshToken: data.refresh_token ?? currentRefreshToken,
28
+ expiresAt: data.expires_in ? new Date(Date.now() + data.expires_in * SECONDS_TO_MS).toISOString() : void 0
29
+ });
30
+ var hasTextLikeFileExtension = (fileName) => {
31
+ let dotIndex = fileName.lastIndexOf(".");
32
+ return dotIndex >= 0 && MICROSOFT_GRAPH_TEXT_LIKE_FILE_EXTENSIONS.has(fileName.slice(dotIndex).toLowerCase());
33
+ };
34
+ var isTextLikeContentType = (contentType) => {
35
+ if (!contentType) {
36
+ return false;
37
+ }
38
+ let normalized = contentType.toLowerCase();
39
+ if (normalized.includes("openxmlformats-officedocument")) {
40
+ return false;
41
+ }
42
+ return normalized.startsWith("text/") || ["json", "xml", "javascript", "typescript", "svg", "x-www-form-urlencoded"].some(
43
+ (fragment) => normalized.includes(fragment)
44
+ );
45
+ };
46
+ var looksLikeBase64 = (content) => {
47
+ let normalized = content.replace(/\s+/g, "");
48
+ return !!normalized && normalized.length % 4 === 0 && MICROSOFT_GRAPH_BASE64_PATTERN.test(normalized);
49
+ };
2
50
  var MICROSOFT_OAUTH_INTEGRATION_KEYS = /* @__PURE__ */ new Set([
3
51
  "azure-blob-storage",
4
52
  "azure-devops",
@@ -25,9 +73,146 @@ var normalizeMicrosoftRedirectUri = (redirectUri) => {
25
73
  return url.toString();
26
74
  };
27
75
  var normalizeMicrosoftRedirectUriForIntegration = (integration, redirectUri) => usesMicrosoftOAuth(integration) ? normalizeMicrosoftRedirectUri(redirectUri) : redirectUri;
76
+ var resolveMicrosoftTenant = (tenantId, defaultTenant) => {
77
+ if (typeof tenantId !== "string") {
78
+ return defaultTenant;
79
+ }
80
+ let normalizedTenant = tenantId.trim();
81
+ return normalizedTenant || defaultTenant;
82
+ };
83
+ var mapAzureDevOpsScopes = (scopes) => scopes.map((scope) => `${AZURE_DEVOPS_RESOURCE}/${scope}`);
84
+ var buildMicrosoftGraphUploadBody = (fileName, content, contentType) => {
85
+ if (isTextLikeContentType(contentType) || hasTextLikeFileExtension(fileName) || !looksLikeBase64(content)) {
86
+ return content;
87
+ }
88
+ return Buffer.from(content.replace(/\s+/g, ""), "base64");
89
+ };
90
+ var defaultGraphProfile = {
91
+ baseURL: MICROSOFT_GRAPH_BASE,
92
+ path: "/me",
93
+ mapProfile: (data) => {
94
+ let user = data ?? {};
95
+ return {
96
+ id: user.id,
97
+ email: user.mail || user.userPrincipalName,
98
+ name: user.displayName
99
+ };
100
+ }
101
+ };
102
+ var tokenClientCache = /* @__PURE__ */ new Map();
103
+ var getCachedTokenClient = (resolvedTenant) => {
104
+ let cached = tokenClientCache.get(resolvedTenant);
105
+ if (cached) return cached;
106
+ let client = createAxios({
107
+ baseURL: `${MICROSOFT_LOGIN_BASE}/${resolvedTenant}/oauth2/v2.0`
108
+ });
109
+ tokenClientCache.set(resolvedTenant, client);
110
+ return client;
111
+ };
112
+ var createMicrosoftGraphOauth = ({
113
+ name,
114
+ key,
115
+ tenant,
116
+ scopes,
117
+ allowTenantInput = false,
118
+ missingRefreshTokenMessage = "No refresh token available. Ensure offline_access scope is requested.",
119
+ normalizeRedirectUri: shouldNormalizeRedirectUri = false,
120
+ scopeMapper,
121
+ extraScopes,
122
+ onMissingRefreshToken = "throw",
123
+ profile = defaultGraphProfile
124
+ }) => {
125
+ let profileAxios = createAxios({ baseURL: profile.baseURL });
126
+ let getTenant = (ctx) => allowTenantInput ? resolveMicrosoftTenant(ctx.input?.tenantId, tenant) : tenant;
127
+ let getRedirectUri = (redirectUri) => shouldNormalizeRedirectUri ? normalizeMicrosoftRedirectUri(redirectUri) : redirectUri;
128
+ let buildScopeParam = (rawScopes) => {
129
+ let mapped = scopeMapper ? scopeMapper(rawScopes) : rawScopes;
130
+ return extraScopes ? [...mapped, ...extraScopes].join(" ") : mapped.join(" ");
131
+ };
132
+ return {
133
+ type: "auth.oauth",
134
+ name,
135
+ key,
136
+ scopes,
137
+ getAuthorizationUrl: async (ctx) => {
138
+ let resolvedTenant = getTenant(ctx);
139
+ let params = new URLSearchParams({
140
+ client_id: ctx.clientId,
141
+ response_type: "code",
142
+ redirect_uri: getRedirectUri(ctx.redirectUri),
143
+ scope: buildScopeParam(ctx.scopes),
144
+ state: ctx.state,
145
+ response_mode: "query"
146
+ });
147
+ return {
148
+ url: `${MICROSOFT_LOGIN_BASE}/${resolvedTenant}/oauth2/v2.0/authorize?${params.toString()}`
149
+ };
150
+ },
151
+ handleCallback: async (ctx) => {
152
+ let tokenClient = getCachedTokenClient(getTenant(ctx));
153
+ let response = await tokenClient.post(
154
+ "/token",
155
+ new URLSearchParams({
156
+ client_id: ctx.clientId,
157
+ client_secret: ctx.clientSecret,
158
+ code: ctx.code,
159
+ redirect_uri: getRedirectUri(ctx.redirectUri),
160
+ grant_type: "authorization_code",
161
+ scope: buildScopeParam(ctx.scopes)
162
+ }).toString(),
163
+ {
164
+ headers: { "Content-Type": "application/x-www-form-urlencoded" }
165
+ }
166
+ );
167
+ return {
168
+ output: getMicrosoftTokenOutput(response.data)
169
+ };
170
+ },
171
+ handleTokenRefresh: async (ctx) => {
172
+ if (!ctx.output.refreshToken) {
173
+ if (onMissingRefreshToken === "preserve") {
174
+ return { output: ctx.output };
175
+ }
176
+ throw new Error(missingRefreshTokenMessage);
177
+ }
178
+ let tokenClient = getCachedTokenClient(getTenant(ctx));
179
+ let response = await tokenClient.post(
180
+ "/token",
181
+ new URLSearchParams({
182
+ client_id: ctx.clientId,
183
+ client_secret: ctx.clientSecret,
184
+ refresh_token: ctx.output.refreshToken,
185
+ grant_type: "refresh_token",
186
+ scope: buildScopeParam(ctx.scopes)
187
+ }).toString(),
188
+ {
189
+ headers: { "Content-Type": "application/x-www-form-urlencoded" }
190
+ }
191
+ );
192
+ return {
193
+ output: getMicrosoftTokenOutput(
194
+ response.data,
195
+ ctx.output.refreshToken
196
+ )
197
+ };
198
+ },
199
+ getProfile: async (ctx) => {
200
+ let response = await profileAxios.get(profile.path, {
201
+ headers: { Authorization: `Bearer ${ctx.output.token}` }
202
+ });
203
+ return { profile: profile.mapProfile(response.data) };
204
+ }
205
+ };
206
+ };
28
207
  export {
208
+ MICROSOFT_GRAPH_BASE,
209
+ MICROSOFT_LOGIN_BASE,
29
210
  MICROSOFT_OAUTH_INTEGRATION_KEYS,
211
+ buildMicrosoftGraphUploadBody,
212
+ createMicrosoftGraphOauth,
213
+ mapAzureDevOpsScopes,
30
214
  normalizeMicrosoftRedirectUri,
31
215
  normalizeMicrosoftRedirectUriForIntegration,
216
+ resolveMicrosoftTenant,
32
217
  usesMicrosoftOAuth
33
218
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slates/oauth-microsoft",
3
- "version": "1.0.0-rc.1",
3
+ "version": "1.0.0-rc.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -30,7 +30,11 @@
30
30
  "build": "tsup --config ../../tsup.packages.config.ts --external async_hooks",
31
31
  "typecheck": "tsc --noEmit"
32
32
  },
33
+ "dependencies": {
34
+ "slates": "1.0.0-rc.10"
35
+ },
33
36
  "devDependencies": {
37
+ "@types/node": "^20",
34
38
  "@slates/tsconfig": "1.0.0-rc.1",
35
39
  "typescript": "5.8.2",
36
40
  "vitest": "^3.1.2"
package/src/index.test.ts CHANGED
@@ -1,7 +1,11 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import {
3
+ buildMicrosoftGraphUploadBody,
4
+ createMicrosoftGraphOauth,
5
+ mapAzureDevOpsScopes,
3
6
  normalizeMicrosoftRedirectUri,
4
7
  normalizeMicrosoftRedirectUriForIntegration,
8
+ resolveMicrosoftTenant,
5
9
  usesMicrosoftOAuth
6
10
  } from './index';
7
11
 
@@ -25,16 +29,111 @@ describe('@slates/oauth-microsoft', () => {
25
29
 
26
30
  it('only normalizes redirect URIs for Microsoft OAuth integrations', () => {
27
31
  expect(
28
- normalizeMicrosoftRedirectUriForIntegration(
29
- 'outlook',
30
- 'http://127.0.0.1:45873/callback'
31
- )
32
+ normalizeMicrosoftRedirectUriForIntegration('outlook', 'http://127.0.0.1:45873/callback')
32
33
  ).toBe('http://localhost:45873/callback');
33
34
  expect(
34
- normalizeMicrosoftRedirectUriForIntegration(
35
- 'github',
36
- 'http://127.0.0.1:45873/callback'
37
- )
35
+ normalizeMicrosoftRedirectUriForIntegration('github', 'http://127.0.0.1:45873/callback')
38
36
  ).toBe('http://127.0.0.1:45873/callback');
39
37
  });
38
+
39
+ it('can normalize redirect URIs in the OAuth factory', async () => {
40
+ let oauth = createMicrosoftGraphOauth({
41
+ name: 'SharePoint',
42
+ key: 'oauth',
43
+ tenant: 'common',
44
+ scopes: [],
45
+ normalizeRedirectUri: true
46
+ });
47
+
48
+ let { url } = await oauth.getAuthorizationUrl({
49
+ clientId: 'client-id',
50
+ clientSecret: 'client-secret',
51
+ redirectUri: 'http://127.0.0.1:45873/callback',
52
+ scopes: [],
53
+ state: 'state'
54
+ });
55
+
56
+ expect(new URL(url).searchParams.get('redirect_uri')).toBe(
57
+ 'http://localhost:45873/callback'
58
+ );
59
+ });
60
+
61
+ it('uses the default tenant when the input tenant is missing or blank', () => {
62
+ expect(resolveMicrosoftTenant(undefined, 'common')).toBe('common');
63
+ expect(resolveMicrosoftTenant(' ', 'organizations')).toBe('organizations');
64
+ });
65
+
66
+ it('uses a trimmed tenant override when provided', () => {
67
+ expect(resolveMicrosoftTenant(' tenants/foo ', 'common')).toBe('tenants/foo');
68
+ });
69
+
70
+ it('builds Azure DevOps resource-qualified scopes with offline access', async () => {
71
+ let oauth = createMicrosoftGraphOauth({
72
+ name: 'Azure DevOps',
73
+ key: 'oauth',
74
+ tenant: 'organizations',
75
+ scopes: [
76
+ {
77
+ title: 'Profile',
78
+ description: 'Read Azure DevOps profile information',
79
+ scope: 'vso.profile'
80
+ }
81
+ ],
82
+ scopeMapper: mapAzureDevOpsScopes,
83
+ extraScopes: ['offline_access']
84
+ });
85
+
86
+ let { url } = await oauth.getAuthorizationUrl({
87
+ clientId: 'client-id',
88
+ clientSecret: 'client-secret',
89
+ redirectUri: 'http://localhost/callback',
90
+ scopes: ['vso.profile'],
91
+ state: 'state'
92
+ });
93
+
94
+ let parsed = new URL(url);
95
+ expect(`${parsed.origin}${parsed.pathname}`).toBe(
96
+ 'https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize'
97
+ );
98
+ expect(parsed.searchParams.get('scope')).toBe(
99
+ '499b84ac-1321-427f-aa17-267ca6975798/vso.profile offline_access'
100
+ );
101
+ });
102
+
103
+ it('can preserve OAuth output when a refresh token is missing', async () => {
104
+ let oauth = createMicrosoftGraphOauth({
105
+ name: 'Azure DevOps',
106
+ key: 'oauth',
107
+ tenant: 'organizations',
108
+ scopes: [],
109
+ onMissingRefreshToken: 'preserve'
110
+ });
111
+ let output = { token: 'existing-token' };
112
+
113
+ await expect(
114
+ oauth.handleTokenRefresh({
115
+ clientId: 'client-id',
116
+ clientSecret: 'client-secret',
117
+ scopes: [],
118
+ output
119
+ })
120
+ ).resolves.toEqual({ output });
121
+ });
122
+
123
+ it('decodes binary-looking base64 uploads for non-text files', () => {
124
+ let body = buildMicrosoftGraphUploadBody(
125
+ 'report.docx',
126
+ 'SGVsbG8=',
127
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
128
+ );
129
+
130
+ expect(Buffer.isBuffer(body)).toBe(true);
131
+ expect(body.toString('utf8')).toBe('Hello');
132
+ });
133
+
134
+ it('preserves text uploads even when the content looks like base64', () => {
135
+ let body = buildMicrosoftGraphUploadBody('notes.txt', 'SGVsbG8=', 'text/plain');
136
+
137
+ expect(body).toBe('SGVsbG8=');
138
+ });
40
139
  });
package/src/index.ts CHANGED
@@ -1,3 +1,167 @@
1
+ import { createAxios } from 'slates';
2
+
3
+ export let MICROSOFT_LOGIN_BASE = 'https://login.microsoftonline.com';
4
+ export let MICROSOFT_GRAPH_BASE = 'https://graph.microsoft.com/v1.0';
5
+
6
+ // Microsoft documents this as the Azure DevOps resource ID used when issuing
7
+ // Entra access tokens.
8
+ // https://learn.microsoft.com/en-us/azure/devops/cli/entra-tokens?view=azure-devops
9
+ let AZURE_DEVOPS_RESOURCE = '499b84ac-1321-427f-aa17-267ca6975798';
10
+ let SECONDS_TO_MS = 1000;
11
+
12
+ export type MicrosoftOauthScope = {
13
+ title: string;
14
+ description: string;
15
+ scope: string;
16
+ defaultChecked?: boolean;
17
+ };
18
+
19
+ export type MicrosoftGraphProfile = {
20
+ id?: string;
21
+ email?: string;
22
+ name?: string;
23
+ imageUrl?: string;
24
+ };
25
+
26
+ export type MicrosoftGraphProfileOptions = {
27
+ baseURL: string;
28
+ path: string;
29
+ mapProfile: (data: unknown) => MicrosoftGraphProfile;
30
+ };
31
+
32
+ export type MicrosoftGraphOauthOptions = {
33
+ name: string;
34
+ key: string;
35
+ tenant: string;
36
+ scopes: MicrosoftOauthScope[];
37
+ allowTenantInput?: boolean;
38
+ missingRefreshTokenMessage?: string;
39
+ /** Normalize loopback redirect URIs for Microsoft app registration compatibility. */
40
+ normalizeRedirectUri?: boolean;
41
+ /** Transforms the raw scope list before it is sent to the token endpoint. */
42
+ scopeMapper?: (scopes: string[]) => string[];
43
+ /** Scopes appended after mapping (e.g. `offline_access` for Azure DevOps). */
44
+ extraScopes?: string[];
45
+ /** When no refresh token is stored, throw (default) or preserve the existing output. */
46
+ onMissingRefreshToken?: 'throw' | 'preserve';
47
+ /** Custom profile endpoint. Defaults to Microsoft Graph `/me`. */
48
+ profile?: MicrosoftGraphProfileOptions;
49
+ };
50
+
51
+ export type MicrosoftGraphOauthInput = {
52
+ tenantId?: unknown;
53
+ };
54
+
55
+ export type MicrosoftGraphAuthorizationUrlContext = {
56
+ clientId: string;
57
+ clientSecret: string;
58
+ redirectUri: string;
59
+ scopes: string[];
60
+ state: string;
61
+ input?: MicrosoftGraphOauthInput;
62
+ };
63
+
64
+ export type MicrosoftGraphCallbackContext = {
65
+ clientId: string;
66
+ clientSecret: string;
67
+ redirectUri: string;
68
+ scopes: string[];
69
+ code: string;
70
+ input?: MicrosoftGraphOauthInput;
71
+ };
72
+
73
+ export type MicrosoftGraphOauthOutput = {
74
+ token: string;
75
+ refreshToken?: string;
76
+ expiresAt?: string;
77
+ };
78
+
79
+ export type MicrosoftGraphTokenRefreshContext = {
80
+ clientId: string;
81
+ clientSecret: string;
82
+ scopes: string[];
83
+ input?: MicrosoftGraphOauthInput;
84
+ output: MicrosoftGraphOauthOutput;
85
+ };
86
+
87
+ export type MicrosoftGraphProfileContext = {
88
+ output: {
89
+ token: string;
90
+ };
91
+ };
92
+
93
+ type MicrosoftTokenResponse = {
94
+ access_token: string;
95
+ refresh_token?: string;
96
+ expires_in?: number;
97
+ };
98
+
99
+ let MICROSOFT_GRAPH_TEXT_LIKE_FILE_EXTENSIONS = new Set([
100
+ '.txt',
101
+ '.md',
102
+ '.csv',
103
+ '.json',
104
+ '.xml',
105
+ '.html',
106
+ '.htm',
107
+ '.js',
108
+ '.jsx',
109
+ '.ts',
110
+ '.tsx',
111
+ '.css',
112
+ '.svg',
113
+ '.yaml',
114
+ '.yml'
115
+ ]);
116
+
117
+ let MICROSOFT_GRAPH_BASE64_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;
118
+
119
+ let getMicrosoftTokenOutput = (
120
+ data: MicrosoftTokenResponse,
121
+ currentRefreshToken?: string
122
+ ): MicrosoftGraphOauthOutput => ({
123
+ token: data.access_token,
124
+ refreshToken: data.refresh_token ?? currentRefreshToken,
125
+ expiresAt: data.expires_in
126
+ ? new Date(Date.now() + data.expires_in * SECONDS_TO_MS).toISOString()
127
+ : undefined
128
+ });
129
+
130
+ let hasTextLikeFileExtension = (fileName: string) => {
131
+ let dotIndex = fileName.lastIndexOf('.');
132
+ return (
133
+ dotIndex >= 0 &&
134
+ MICROSOFT_GRAPH_TEXT_LIKE_FILE_EXTENSIONS.has(fileName.slice(dotIndex).toLowerCase())
135
+ );
136
+ };
137
+
138
+ let isTextLikeContentType = (contentType?: string) => {
139
+ if (!contentType) {
140
+ return false;
141
+ }
142
+
143
+ let normalized = contentType.toLowerCase();
144
+ if (normalized.includes('openxmlformats-officedocument')) {
145
+ return false;
146
+ }
147
+
148
+ return (
149
+ normalized.startsWith('text/') ||
150
+ ['json', 'xml', 'javascript', 'typescript', 'svg', 'x-www-form-urlencoded'].some(
151
+ fragment => normalized.includes(fragment)
152
+ )
153
+ );
154
+ };
155
+
156
+ let looksLikeBase64 = (content: string) => {
157
+ let normalized = content.replace(/\s+/g, '');
158
+ return (
159
+ !!normalized &&
160
+ normalized.length % 4 === 0 &&
161
+ MICROSOFT_GRAPH_BASE64_PATTERN.test(normalized)
162
+ );
163
+ };
164
+
1
165
  export let MICROSOFT_OAUTH_INTEGRATION_KEYS = new Set([
2
166
  'azure-blob-storage',
3
167
  'azure-devops',
@@ -31,4 +195,175 @@ export let normalizeMicrosoftRedirectUri = (redirectUri: string) => {
31
195
  export let normalizeMicrosoftRedirectUriForIntegration = (
32
196
  integration: string,
33
197
  redirectUri: string
34
- ) => (usesMicrosoftOAuth(integration) ? normalizeMicrosoftRedirectUri(redirectUri) : redirectUri);
198
+ ) =>
199
+ usesMicrosoftOAuth(integration) ? normalizeMicrosoftRedirectUri(redirectUri) : redirectUri;
200
+
201
+ export let resolveMicrosoftTenant = (tenantId: unknown, defaultTenant: string) => {
202
+ if (typeof tenantId !== 'string') {
203
+ return defaultTenant;
204
+ }
205
+
206
+ let normalizedTenant = tenantId.trim();
207
+ return normalizedTenant || defaultTenant;
208
+ };
209
+
210
+ export let mapAzureDevOpsScopes = (scopes: string[]) =>
211
+ scopes.map(scope => `${AZURE_DEVOPS_RESOURCE}/${scope}`);
212
+
213
+ export let buildMicrosoftGraphUploadBody = (
214
+ fileName: string,
215
+ content: string,
216
+ contentType?: string
217
+ ) => {
218
+ if (
219
+ isTextLikeContentType(contentType) ||
220
+ hasTextLikeFileExtension(fileName) ||
221
+ !looksLikeBase64(content)
222
+ ) {
223
+ return content;
224
+ }
225
+
226
+ return Buffer.from(content.replace(/\s+/g, ''), 'base64');
227
+ };
228
+
229
+ let defaultGraphProfile: MicrosoftGraphProfileOptions = {
230
+ baseURL: MICROSOFT_GRAPH_BASE,
231
+ path: '/me',
232
+ mapProfile: data => {
233
+ let user = (data ?? {}) as {
234
+ id?: string;
235
+ mail?: string;
236
+ userPrincipalName?: string;
237
+ displayName?: string;
238
+ };
239
+
240
+ return {
241
+ id: user.id,
242
+ email: user.mail || user.userPrincipalName,
243
+ name: user.displayName
244
+ };
245
+ }
246
+ };
247
+
248
+ let tokenClientCache = new Map<string, ReturnType<typeof createAxios>>();
249
+ let getCachedTokenClient = (resolvedTenant: string) => {
250
+ let cached = tokenClientCache.get(resolvedTenant);
251
+ if (cached) return cached;
252
+
253
+ let client = createAxios({
254
+ baseURL: `${MICROSOFT_LOGIN_BASE}/${resolvedTenant}/oauth2/v2.0`
255
+ });
256
+ tokenClientCache.set(resolvedTenant, client);
257
+ return client;
258
+ };
259
+
260
+ export let createMicrosoftGraphOauth = ({
261
+ name,
262
+ key,
263
+ tenant,
264
+ scopes,
265
+ allowTenantInput = false,
266
+ missingRefreshTokenMessage = 'No refresh token available. Ensure offline_access scope is requested.',
267
+ normalizeRedirectUri: shouldNormalizeRedirectUri = false,
268
+ scopeMapper,
269
+ extraScopes,
270
+ onMissingRefreshToken = 'throw',
271
+ profile = defaultGraphProfile
272
+ }: MicrosoftGraphOauthOptions) => {
273
+ let profileAxios = createAxios({ baseURL: profile.baseURL });
274
+
275
+ let getTenant = (ctx: { input?: MicrosoftGraphOauthInput }) =>
276
+ allowTenantInput ? resolveMicrosoftTenant(ctx.input?.tenantId, tenant) : tenant;
277
+
278
+ let getRedirectUri = (redirectUri: string) =>
279
+ shouldNormalizeRedirectUri ? normalizeMicrosoftRedirectUri(redirectUri) : redirectUri;
280
+
281
+ let buildScopeParam = (rawScopes: string[]) => {
282
+ let mapped = scopeMapper ? scopeMapper(rawScopes) : rawScopes;
283
+ return extraScopes ? [...mapped, ...extraScopes].join(' ') : mapped.join(' ');
284
+ };
285
+
286
+ return {
287
+ type: 'auth.oauth' as const,
288
+ name,
289
+ key,
290
+ scopes,
291
+
292
+ getAuthorizationUrl: async (ctx: MicrosoftGraphAuthorizationUrlContext) => {
293
+ let resolvedTenant = getTenant(ctx);
294
+ let params = new URLSearchParams({
295
+ client_id: ctx.clientId,
296
+ response_type: 'code',
297
+ redirect_uri: getRedirectUri(ctx.redirectUri),
298
+ scope: buildScopeParam(ctx.scopes),
299
+ state: ctx.state,
300
+ response_mode: 'query'
301
+ });
302
+
303
+ return {
304
+ url: `${MICROSOFT_LOGIN_BASE}/${resolvedTenant}/oauth2/v2.0/authorize?${params.toString()}`
305
+ };
306
+ },
307
+
308
+ handleCallback: async (ctx: MicrosoftGraphCallbackContext) => {
309
+ let tokenClient = getCachedTokenClient(getTenant(ctx));
310
+ let response = await tokenClient.post(
311
+ '/token',
312
+ new URLSearchParams({
313
+ client_id: ctx.clientId,
314
+ client_secret: ctx.clientSecret,
315
+ code: ctx.code,
316
+ redirect_uri: getRedirectUri(ctx.redirectUri),
317
+ grant_type: 'authorization_code',
318
+ scope: buildScopeParam(ctx.scopes)
319
+ }).toString(),
320
+ {
321
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
322
+ }
323
+ );
324
+
325
+ return {
326
+ output: getMicrosoftTokenOutput(response.data as MicrosoftTokenResponse)
327
+ };
328
+ },
329
+
330
+ handleTokenRefresh: async (ctx: MicrosoftGraphTokenRefreshContext) => {
331
+ if (!ctx.output.refreshToken) {
332
+ if (onMissingRefreshToken === 'preserve') {
333
+ return { output: ctx.output };
334
+ }
335
+ throw new Error(missingRefreshTokenMessage);
336
+ }
337
+
338
+ let tokenClient = getCachedTokenClient(getTenant(ctx));
339
+ let response = await tokenClient.post(
340
+ '/token',
341
+ new URLSearchParams({
342
+ client_id: ctx.clientId,
343
+ client_secret: ctx.clientSecret,
344
+ refresh_token: ctx.output.refreshToken,
345
+ grant_type: 'refresh_token',
346
+ scope: buildScopeParam(ctx.scopes)
347
+ }).toString(),
348
+ {
349
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
350
+ }
351
+ );
352
+
353
+ return {
354
+ output: getMicrosoftTokenOutput(
355
+ response.data as MicrosoftTokenResponse,
356
+ ctx.output.refreshToken
357
+ )
358
+ };
359
+ },
360
+
361
+ getProfile: async (ctx: MicrosoftGraphProfileContext) => {
362
+ let response = await profileAxios.get(profile.path, {
363
+ headers: { Authorization: `Bearer ${ctx.output.token}` }
364
+ });
365
+
366
+ return { profile: profile.mapProfile(response.data) };
367
+ }
368
+ };
369
+ };