@slates/oauth-microsoft 1.0.0-rc.1 → 1.0.0-rc.3
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 +1 -1
- package/dist/index.cjs +191 -0
- package/dist/index.d.cts +95 -1
- package/dist/index.d.ts +95 -1
- package/dist/index.module.js +185 -0
- package/package.json +5 -1
- package/src/index.test.ts +107 -8
- package/src/index.ts +336 -1
package/README.md
CHANGED
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 };
|
package/dist/index.module.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "1.0.0-rc.3",
|
|
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.11"
|
|
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
|
-
) =>
|
|
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
|
+
};
|