@stackframe/stack-shared 1.0.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/LICENSE +7 -0
- package/dist/helpers/fetch-token.d.ts +1 -0
- package/dist/helpers/fetch-token.d.ts.map +1 -0
- package/dist/helpers/fetch-token.js +1 -0
- package/dist/helpers/password.d.ts +1 -0
- package/dist/helpers/password.d.ts.map +1 -0
- package/dist/helpers/password.js +33 -0
- package/dist/hooks/use-async-external-store.d.ts +4 -0
- package/dist/hooks/use-async-external-store.d.ts.map +1 -0
- package/dist/hooks/use-async-external-store.js +20 -0
- package/dist/hooks/use-strict-memo.d.ts +6 -0
- package/dist/hooks/use-strict-memo.d.ts.map +1 -0
- package/dist/hooks/use-strict-memo.js +64 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/interface/adminInterface.d.ts +88 -0
- package/dist/interface/adminInterface.d.ts.map +1 -0
- package/dist/interface/adminInterface.js +109 -0
- package/dist/interface/clientInterface.d.ts +134 -0
- package/dist/interface/clientInterface.d.ts.map +1 -0
- package/dist/interface/clientInterface.js +444 -0
- package/dist/interface/serverInterface.d.ts +33 -0
- package/dist/interface/serverInterface.d.ts.map +1 -0
- package/dist/interface/serverInterface.js +70 -0
- package/dist/utils/arrays.d.ts +10 -0
- package/dist/utils/arrays.d.ts.map +1 -0
- package/dist/utils/arrays.js +54 -0
- package/dist/utils/caches.d.ts +76 -0
- package/dist/utils/caches.d.ts.map +1 -0
- package/dist/utils/caches.js +100 -0
- package/dist/utils/crypto.d.ts +1 -0
- package/dist/utils/crypto.d.ts.map +1 -0
- package/dist/utils/crypto.js +5 -0
- package/dist/utils/dates.d.ts +12 -0
- package/dist/utils/dates.d.ts.map +1 -0
- package/dist/utils/dates.js +57 -0
- package/dist/utils/dom.d.ts +4 -0
- package/dist/utils/dom.d.ts.map +1 -0
- package/dist/utils/dom.js +11 -0
- package/dist/utils/env.d.ts +4 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +7 -0
- package/dist/utils/errors.d.ts +184 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +72 -0
- package/dist/utils/html.d.ts +2 -0
- package/dist/utils/html.d.ts.map +1 -0
- package/dist/utils/html.js +12 -0
- package/dist/utils/json.d.ts +10 -0
- package/dist/utils/json.d.ts.map +1 -0
- package/dist/utils/json.js +26 -0
- package/dist/utils/jwt.d.ts +3 -0
- package/dist/utils/jwt.d.ts.map +1 -0
- package/dist/utils/jwt.js +16 -0
- package/dist/utils/math.d.ts +4 -0
- package/dist/utils/math.d.ts.map +1 -0
- package/dist/utils/math.js +6 -0
- package/dist/utils/numbers.d.ts +2 -0
- package/dist/utils/numbers.d.ts.map +1 -0
- package/dist/utils/numbers.js +26 -0
- package/dist/utils/objects.d.ts +18 -0
- package/dist/utils/objects.d.ts.map +1 -0
- package/dist/utils/objects.js +63 -0
- package/dist/utils/password.d.ts +2 -0
- package/dist/utils/password.d.ts.map +1 -0
- package/dist/utils/password.js +8 -0
- package/dist/utils/promises.d.ts +49 -0
- package/dist/utils/promises.d.ts.map +1 -0
- package/dist/utils/promises.js +145 -0
- package/dist/utils/react.d.ts +12 -0
- package/dist/utils/react.d.ts.map +1 -0
- package/dist/utils/react.js +44 -0
- package/dist/utils/results.d.ts +73 -0
- package/dist/utils/results.d.ts.map +1 -0
- package/dist/utils/results.js +112 -0
- package/dist/utils/stores.d.ts +57 -0
- package/dist/utils/stores.d.ts.map +1 -0
- package/dist/utils/stores.js +122 -0
- package/dist/utils/strings.d.ts +40 -0
- package/dist/utils/strings.d.ts.map +1 -0
- package/dist/utils/strings.js +91 -0
- package/dist/utils/types.d.ts +33 -0
- package/dist/utils/types.d.ts.map +1 -0
- package/dist/utils/types.js +61 -0
- package/dist/utils/uuids.d.ts +1 -0
- package/dist/utils/uuids.d.ts.map +1 -0
- package/dist/utils/uuids.js +4 -0
- package/package.json +40 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import * as oauth from 'oauth4webapi';
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
import { AccessTokenExpiredErrorCode, GrantInvalidErrorCode, KnownErrorCodes, KnownError, SignUpErrorCodes, SignInErrorCodes, EmailVerificationLinkErrorCodes, PasswordResetLinkErrorCodes } from "../utils/types";
|
|
4
|
+
import { Result } from "../utils/results";
|
|
5
|
+
import { parseJson } from '../utils/json';
|
|
6
|
+
import { AsyncCache, AsyncValueCache } from '../utils/caches';
|
|
7
|
+
import { typedAssign } from '../utils/objects';
|
|
8
|
+
import { AsyncStore } from '../utils/stores';
|
|
9
|
+
import { runAsynchronously } from '../utils/promises';
|
|
10
|
+
function getSessionCookieName(projectId) {
|
|
11
|
+
return "__stack-token-" + crypto.createHash("sha256").update(projectId).digest("hex");
|
|
12
|
+
}
|
|
13
|
+
export class StackClientInterface {
|
|
14
|
+
options;
|
|
15
|
+
// note that we intentionally use TokenStore (a reference type) as a key, as different token stores with the same tokens should be treated differently
|
|
16
|
+
// (if we wouldn't do that, we would cache users across requests, which may cause caching issues)
|
|
17
|
+
currentUserCache;
|
|
18
|
+
clientProjectCache;
|
|
19
|
+
constructor(options) {
|
|
20
|
+
this.options = options;
|
|
21
|
+
this.currentUserCache = new AsyncCache(async (key, isFirst) => {
|
|
22
|
+
if (isFirst) {
|
|
23
|
+
key.onChange((newValue, oldValue) => {
|
|
24
|
+
if (JSON.stringify(newValue) === JSON.stringify(oldValue))
|
|
25
|
+
return;
|
|
26
|
+
runAsynchronously(this.currentUserCache.refresh(key));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
const user = await this.getClientUserByToken(key);
|
|
30
|
+
return Result.or(user, null);
|
|
31
|
+
});
|
|
32
|
+
this.clientProjectCache = new AsyncValueCache(async () => {
|
|
33
|
+
return Result.orThrow(await this.getClientProject());
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
get projectId() {
|
|
37
|
+
return this.options.projectId;
|
|
38
|
+
}
|
|
39
|
+
getSessionCookieName() {
|
|
40
|
+
return getSessionCookieName(this.projectId);
|
|
41
|
+
}
|
|
42
|
+
getApiUrl() {
|
|
43
|
+
return this.options.baseUrl + "/api/v1";
|
|
44
|
+
}
|
|
45
|
+
async refreshUser(tokenStore) {
|
|
46
|
+
await this.currentUserCache.refresh(tokenStore);
|
|
47
|
+
}
|
|
48
|
+
async refreshProject() {
|
|
49
|
+
await this.clientProjectCache.refresh();
|
|
50
|
+
}
|
|
51
|
+
async refreshAccessToken(tokenStore) {
|
|
52
|
+
if (!('publishableClientKey' in this.options)) {
|
|
53
|
+
// TODO fix
|
|
54
|
+
throw new Error("Admin session token is currently not supported for fetching new access token");
|
|
55
|
+
}
|
|
56
|
+
const refreshToken = (await tokenStore.getOrWait()).refreshToken;
|
|
57
|
+
if (!refreshToken) {
|
|
58
|
+
tokenStore.set({
|
|
59
|
+
accessToken: null,
|
|
60
|
+
refreshToken: null,
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const as = {
|
|
65
|
+
issuer: this.options.baseUrl,
|
|
66
|
+
algorithm: 'oauth2',
|
|
67
|
+
token_endpoint: this.getApiUrl() + '/auth/token',
|
|
68
|
+
};
|
|
69
|
+
const client = {
|
|
70
|
+
client_id: this.projectId,
|
|
71
|
+
client_secret: this.options.publishableClientKey,
|
|
72
|
+
token_endpoint_auth_method: 'client_secret_basic',
|
|
73
|
+
};
|
|
74
|
+
const response = await oauth.refreshTokenGrantRequest(as, client, refreshToken);
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
const error = await response.text();
|
|
77
|
+
let errorJsonResult = parseJson(error);
|
|
78
|
+
if (response.status === 401
|
|
79
|
+
&& errorJsonResult.status === "ok"
|
|
80
|
+
&& errorJsonResult.data
|
|
81
|
+
&& errorJsonResult.data.error_code === GrantInvalidErrorCode) {
|
|
82
|
+
return tokenStore.set({
|
|
83
|
+
accessToken: null,
|
|
84
|
+
refreshToken: null,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
throw new Error(`Failed to send refresh token request: ${response.status} ${error}`);
|
|
88
|
+
}
|
|
89
|
+
let challenges;
|
|
90
|
+
if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) {
|
|
91
|
+
for (const challenge of challenges) {
|
|
92
|
+
console.error('WWW-Authenticate Challenge', challenge);
|
|
93
|
+
}
|
|
94
|
+
throw new Error(); // Handle WWW-Authenticate Challenges as needed
|
|
95
|
+
}
|
|
96
|
+
const result = await oauth.processRefreshTokenResponse(as, client, response);
|
|
97
|
+
if (oauth.isOAuth2Error(result)) {
|
|
98
|
+
console.error('Error Response', result);
|
|
99
|
+
throw new Error(); // Handle OAuth 2.0 response body error
|
|
100
|
+
}
|
|
101
|
+
tokenStore.update(old => ({
|
|
102
|
+
accessToken: result.access_token ?? null,
|
|
103
|
+
refreshToken: result.refresh_token ?? old?.refreshToken ?? null,
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
async sendClientRequest(path, requestOptions, tokenStoreOrNull) {
|
|
107
|
+
const tokenStore = tokenStoreOrNull ?? new AsyncStore({
|
|
108
|
+
accessToken: null,
|
|
109
|
+
refreshToken: null,
|
|
110
|
+
});
|
|
111
|
+
return await Result.orThrowAsync(Result.retry(() => this.sendClientRequestInner(path, requestOptions, tokenStore), 5, { exponentialDelayBase: 1000 }));
|
|
112
|
+
}
|
|
113
|
+
async sendClientRequestAndCatchKnownError(path, requestOptions, tokenStoreOrNull, errorCodes) {
|
|
114
|
+
try {
|
|
115
|
+
return Result.ok(await this.sendClientRequest(path, requestOptions, tokenStoreOrNull));
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
if (e instanceof KnownError && errorCodes.includes(e.errorCode)) {
|
|
119
|
+
return Result.error(e.errorCode);
|
|
120
|
+
}
|
|
121
|
+
throw e;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async sendClientRequestInner(path, options,
|
|
125
|
+
/**
|
|
126
|
+
* This object will be modified for future retries, so it should be passed by reference.
|
|
127
|
+
*/
|
|
128
|
+
tokenStore) {
|
|
129
|
+
let tokenObj = await tokenStore.getOrWait();
|
|
130
|
+
if (!tokenObj.accessToken && tokenObj.refreshToken) {
|
|
131
|
+
await this.refreshAccessToken(tokenStore);
|
|
132
|
+
tokenObj = await tokenStore.getOrWait();
|
|
133
|
+
}
|
|
134
|
+
const url = this.getApiUrl() + path;
|
|
135
|
+
const params = {
|
|
136
|
+
...options,
|
|
137
|
+
headers: {
|
|
138
|
+
...tokenObj.accessToken ? {
|
|
139
|
+
"authorization": "StackSession " + tokenObj.accessToken,
|
|
140
|
+
} : {},
|
|
141
|
+
"x-stack-project-id": this.projectId,
|
|
142
|
+
...'publishableClientKey' in this.options ? {
|
|
143
|
+
"x-stack-publishable-client-key": this.options.publishableClientKey,
|
|
144
|
+
} : {},
|
|
145
|
+
...'internalAdminAccessToken' in this.options ? {
|
|
146
|
+
"x-stack-admin-access-token": this.options.internalAdminAccessToken,
|
|
147
|
+
} : {},
|
|
148
|
+
...options.headers,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
const res = await fetch(url, params);
|
|
152
|
+
typedAssign(res, {
|
|
153
|
+
usedTokens: tokenObj,
|
|
154
|
+
});
|
|
155
|
+
if (res.ok) {
|
|
156
|
+
return Result.ok(res);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
const error = await res.text();
|
|
160
|
+
let errorJsonResult = parseJson(error);
|
|
161
|
+
if (res.status === 401
|
|
162
|
+
&& errorJsonResult.status === "ok"
|
|
163
|
+
&& errorJsonResult.data
|
|
164
|
+
&& errorJsonResult.data.error_code === AccessTokenExpiredErrorCode) {
|
|
165
|
+
tokenStore.set({
|
|
166
|
+
accessToken: null,
|
|
167
|
+
refreshToken: tokenObj.refreshToken,
|
|
168
|
+
});
|
|
169
|
+
return Result.error(new Error("Access token expired"));
|
|
170
|
+
}
|
|
171
|
+
if (res.status >= 400 && res.status <= 599
|
|
172
|
+
&& errorJsonResult.status === "ok"
|
|
173
|
+
&& errorJsonResult.data
|
|
174
|
+
&& KnownErrorCodes.includes(errorJsonResult.data.error_code)) {
|
|
175
|
+
throw new KnownError(errorJsonResult.data.error_code);
|
|
176
|
+
}
|
|
177
|
+
// Do not retry, throw error instead of returning one
|
|
178
|
+
throw new Error(`Failed to send request to ${url}: ${res.status} ${error}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async sendForgotPasswordEmail(email, redirectUrl) {
|
|
182
|
+
const res = await this.sendClientRequestAndCatchKnownError("/auth/forgot-password", {
|
|
183
|
+
method: "POST",
|
|
184
|
+
headers: {
|
|
185
|
+
"Content-Type": "application/json"
|
|
186
|
+
},
|
|
187
|
+
body: JSON.stringify({
|
|
188
|
+
email,
|
|
189
|
+
redirectUrl,
|
|
190
|
+
}),
|
|
191
|
+
}, null, PasswordResetLinkErrorCodes);
|
|
192
|
+
if (res.status === "error") {
|
|
193
|
+
return res.error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async resetPassword(options) {
|
|
197
|
+
const res = await this.sendClientRequestAndCatchKnownError("/auth/password-reset", {
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers: {
|
|
200
|
+
"Content-Type": "application/json"
|
|
201
|
+
},
|
|
202
|
+
body: JSON.stringify(options),
|
|
203
|
+
}, null, PasswordResetLinkErrorCodes);
|
|
204
|
+
if (res.status === "error") {
|
|
205
|
+
return res.error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async verifyPasswordResetCode(code) {
|
|
209
|
+
const res = await this.sendClientRequestAndCatchKnownError("/auth/password-reset", {
|
|
210
|
+
method: "POST",
|
|
211
|
+
headers: {
|
|
212
|
+
"Content-Type": "application/json"
|
|
213
|
+
},
|
|
214
|
+
body: JSON.stringify({
|
|
215
|
+
code,
|
|
216
|
+
onlyVerifyCode: true,
|
|
217
|
+
}),
|
|
218
|
+
}, null, PasswordResetLinkErrorCodes);
|
|
219
|
+
if (res.status === "error") {
|
|
220
|
+
return res.error;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async verifyEmail(code) {
|
|
224
|
+
const res = await this.sendClientRequestAndCatchKnownError("/auth/email-verification", {
|
|
225
|
+
method: "POST",
|
|
226
|
+
headers: {
|
|
227
|
+
"Content-Type": "application/json"
|
|
228
|
+
},
|
|
229
|
+
body: JSON.stringify({
|
|
230
|
+
code,
|
|
231
|
+
}),
|
|
232
|
+
}, null, EmailVerificationLinkErrorCodes);
|
|
233
|
+
if (res.status === "error") {
|
|
234
|
+
return res.error;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async signInWithCredential(email, password, tokenStore) {
|
|
238
|
+
const res = await this.sendClientRequestAndCatchKnownError("/auth/signin", {
|
|
239
|
+
method: "POST",
|
|
240
|
+
headers: {
|
|
241
|
+
"Content-Type": "application/json"
|
|
242
|
+
},
|
|
243
|
+
body: JSON.stringify({
|
|
244
|
+
email,
|
|
245
|
+
password,
|
|
246
|
+
}),
|
|
247
|
+
}, tokenStore, SignInErrorCodes);
|
|
248
|
+
if (res.status === "error") {
|
|
249
|
+
return res.error;
|
|
250
|
+
}
|
|
251
|
+
const result = await res.data.json();
|
|
252
|
+
tokenStore.set({
|
|
253
|
+
accessToken: result.access_token,
|
|
254
|
+
refreshToken: result.refresh_token,
|
|
255
|
+
});
|
|
256
|
+
await this.refreshUser(tokenStore);
|
|
257
|
+
}
|
|
258
|
+
async signUpWithCredential(email, password, emailVerificationRedirectUrl, tokenStore) {
|
|
259
|
+
const res = await this.sendClientRequestAndCatchKnownError("/auth/signup", {
|
|
260
|
+
headers: {
|
|
261
|
+
"Content-Type": "application/json"
|
|
262
|
+
},
|
|
263
|
+
method: "POST",
|
|
264
|
+
body: JSON.stringify({
|
|
265
|
+
email,
|
|
266
|
+
password,
|
|
267
|
+
emailVerificationRedirectUrl,
|
|
268
|
+
}),
|
|
269
|
+
}, tokenStore, SignUpErrorCodes);
|
|
270
|
+
if (res.status === "error") {
|
|
271
|
+
return res.error;
|
|
272
|
+
}
|
|
273
|
+
const result = await res.data.json();
|
|
274
|
+
tokenStore.set({
|
|
275
|
+
accessToken: result.access_token,
|
|
276
|
+
refreshToken: result.refresh_token,
|
|
277
|
+
});
|
|
278
|
+
await this.refreshUser(tokenStore);
|
|
279
|
+
}
|
|
280
|
+
async getOauthUrl(provider, redirectUrl, codeChallenge, state) {
|
|
281
|
+
const updatedRedirectUrl = new URL(redirectUrl);
|
|
282
|
+
for (const key of ["code", "state"]) {
|
|
283
|
+
if (updatedRedirectUrl.searchParams.has(key)) {
|
|
284
|
+
console.warn("Redirect URL already contains " + key + " parameter, removing it as it will be overwritten by the OAuth callback");
|
|
285
|
+
}
|
|
286
|
+
updatedRedirectUrl.searchParams.delete(key);
|
|
287
|
+
}
|
|
288
|
+
if (!('publishableClientKey' in this.options)) {
|
|
289
|
+
// TODO fix
|
|
290
|
+
throw new Error("Admin session token is currently not supported for Oauth");
|
|
291
|
+
}
|
|
292
|
+
const url = new URL(this.getApiUrl() + "/auth/authorize/" + provider.toLowerCase());
|
|
293
|
+
url.searchParams.set("client_id", this.projectId);
|
|
294
|
+
url.searchParams.set("client_secret", this.options.publishableClientKey);
|
|
295
|
+
url.searchParams.set("redirect_uri", updatedRedirectUrl.toString());
|
|
296
|
+
url.searchParams.set("scope", "openid");
|
|
297
|
+
url.searchParams.set("state", state);
|
|
298
|
+
url.searchParams.set("grant_type", "authorization_code");
|
|
299
|
+
url.searchParams.set("code_challenge", codeChallenge);
|
|
300
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
301
|
+
url.searchParams.set("response_type", "code");
|
|
302
|
+
return url.toString();
|
|
303
|
+
}
|
|
304
|
+
async callOauthCallback(oauthParams, redirectUri, codeVerifier, state, tokenStore) {
|
|
305
|
+
if (!('publishableClientKey' in this.options)) {
|
|
306
|
+
// TODO fix
|
|
307
|
+
throw new Error("Admin session token is currently not supported for Oauth");
|
|
308
|
+
}
|
|
309
|
+
const as = {
|
|
310
|
+
issuer: this.options.baseUrl,
|
|
311
|
+
algorithm: 'oauth2',
|
|
312
|
+
token_endpoint: this.getApiUrl() + '/auth/token',
|
|
313
|
+
};
|
|
314
|
+
const client = {
|
|
315
|
+
client_id: this.projectId,
|
|
316
|
+
client_secret: this.options.publishableClientKey,
|
|
317
|
+
token_endpoint_auth_method: 'client_secret_basic',
|
|
318
|
+
};
|
|
319
|
+
const params = oauth.validateAuthResponse(as, client, oauthParams, state);
|
|
320
|
+
if (oauth.isOAuth2Error(params)) {
|
|
321
|
+
console.error('Error validating OAuth response', params);
|
|
322
|
+
throw new Error("Error validating OAuth response"); // Handle OAuth 2.0 redirect error
|
|
323
|
+
}
|
|
324
|
+
const response = await oauth.authorizationCodeGrantRequest(as, client, params, redirectUri, codeVerifier);
|
|
325
|
+
let challenges;
|
|
326
|
+
if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) {
|
|
327
|
+
for (const challenge of challenges) {
|
|
328
|
+
console.error('WWW-Authenticate Challenge', challenge);
|
|
329
|
+
}
|
|
330
|
+
throw new Error(); // Handle WWW-Authenticate Challenges as needed
|
|
331
|
+
}
|
|
332
|
+
const result = await oauth.processAuthorizationCodeOAuth2Response(as, client, response);
|
|
333
|
+
if (oauth.isOAuth2Error(result)) {
|
|
334
|
+
console.error('Error Response', result);
|
|
335
|
+
throw new Error(); // Handle OAuth 2.0 response body error
|
|
336
|
+
}
|
|
337
|
+
tokenStore.update(old => ({
|
|
338
|
+
accessToken: result.access_token ?? null,
|
|
339
|
+
refreshToken: result.refresh_token ?? old?.refreshToken ?? null,
|
|
340
|
+
}));
|
|
341
|
+
await this.refreshUser(tokenStore);
|
|
342
|
+
}
|
|
343
|
+
async signOut(tokenStore) {
|
|
344
|
+
const tokenObj = await tokenStore.getOrWait();
|
|
345
|
+
const res = await this.sendClientRequest("/auth/signout", {
|
|
346
|
+
method: "POST",
|
|
347
|
+
headers: {
|
|
348
|
+
"Content-Type": "application/json"
|
|
349
|
+
},
|
|
350
|
+
body: JSON.stringify({
|
|
351
|
+
refreshToken: tokenObj.refreshToken ?? "",
|
|
352
|
+
}),
|
|
353
|
+
}, tokenStore);
|
|
354
|
+
await res.json();
|
|
355
|
+
tokenStore.set({
|
|
356
|
+
accessToken: null,
|
|
357
|
+
refreshToken: null,
|
|
358
|
+
});
|
|
359
|
+
await this.refreshUser(tokenStore);
|
|
360
|
+
}
|
|
361
|
+
async getClientUserByToken(tokenStore) {
|
|
362
|
+
const response = await this.sendClientRequest("/current-user", {}, tokenStore);
|
|
363
|
+
const user = await response.json();
|
|
364
|
+
if (!user)
|
|
365
|
+
return Result.error(new Error("Failed to get user"));
|
|
366
|
+
return Result.ok(user);
|
|
367
|
+
}
|
|
368
|
+
async getClientProject() {
|
|
369
|
+
const response = await this.sendClientRequest("/projects/" + this.options.projectId, {}, null);
|
|
370
|
+
const project = await response.json();
|
|
371
|
+
if (!project)
|
|
372
|
+
return Result.error(new Error("Failed to get project"));
|
|
373
|
+
return Result.ok(project);
|
|
374
|
+
}
|
|
375
|
+
async setClientUserCustomizableData(update, tokenStore) {
|
|
376
|
+
await this.sendClientRequest("/current-user", {
|
|
377
|
+
method: "PUT",
|
|
378
|
+
headers: {
|
|
379
|
+
"content-type": "application/json",
|
|
380
|
+
},
|
|
381
|
+
body: JSON.stringify(update),
|
|
382
|
+
}, tokenStore);
|
|
383
|
+
await this.refreshUser(tokenStore);
|
|
384
|
+
}
|
|
385
|
+
async listProjects(tokenStore) {
|
|
386
|
+
const response = await this.sendClientRequest("/projects", {}, tokenStore);
|
|
387
|
+
if (!response.ok) {
|
|
388
|
+
throw new Error("Failed to list projects: " + response.status + " " + (await response.text()));
|
|
389
|
+
}
|
|
390
|
+
const json = await response.json();
|
|
391
|
+
return json;
|
|
392
|
+
}
|
|
393
|
+
async createProject(project, tokenStore) {
|
|
394
|
+
const fetchResponse = await this.sendClientRequest("/projects", {
|
|
395
|
+
method: "POST",
|
|
396
|
+
headers: {
|
|
397
|
+
"content-type": "application/json",
|
|
398
|
+
},
|
|
399
|
+
body: JSON.stringify(project),
|
|
400
|
+
}, tokenStore);
|
|
401
|
+
if (!fetchResponse.ok) {
|
|
402
|
+
throw new Error("Failed to create project: " + fetchResponse.status + " " + (await fetchResponse.text()));
|
|
403
|
+
}
|
|
404
|
+
const json = await fetchResponse.json();
|
|
405
|
+
return json;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
export function getProductionModeErrors(project) {
|
|
409
|
+
const errors = [];
|
|
410
|
+
for (const { domain, handlerPath } of project.evaluatedConfig.domains) {
|
|
411
|
+
// TODO: check if handlerPath is valid
|
|
412
|
+
const fixUrlRelative = `/projects/${encodeURIComponent(project.id)}/auth/urls-and-callbacks`;
|
|
413
|
+
let url;
|
|
414
|
+
try {
|
|
415
|
+
url = new URL(domain);
|
|
416
|
+
}
|
|
417
|
+
catch (e) {
|
|
418
|
+
errors.push({
|
|
419
|
+
errorMessage: "Domain should be a valid URL: " + domain,
|
|
420
|
+
fixUrlRelative,
|
|
421
|
+
});
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (url.hostname === "localhost") {
|
|
425
|
+
errors.push({
|
|
426
|
+
errorMessage: "Domain should not be localhost: " + domain,
|
|
427
|
+
fixUrlRelative,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
else if (!url.hostname.includes(".") || url.hostname.match(/\d+(\.\d+)*/)) {
|
|
431
|
+
errors.push({
|
|
432
|
+
errorMessage: "Not a valid domain" + domain,
|
|
433
|
+
fixUrlRelative,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
else if (url.protocol !== "https:") {
|
|
437
|
+
errors.push({
|
|
438
|
+
errorMessage: "Auth callback prefix should be HTTPS: " + domain,
|
|
439
|
+
fixUrlRelative,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return errors;
|
|
444
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ClientInterfaceOptions, UserCustomizableJson, UserJson, TokenStore, StackClientInterface } from "./clientInterface";
|
|
2
|
+
import { Result } from "../utils/results";
|
|
3
|
+
import { ReadonlyJson } from "../utils/types";
|
|
4
|
+
import { AsyncCache } from "../utils/caches";
|
|
5
|
+
export type ServerUserJson = UserJson & {
|
|
6
|
+
readonly serverMetadata: ReadonlyJson;
|
|
7
|
+
};
|
|
8
|
+
export type ServerUserCustomizableJson = UserCustomizableJson & {
|
|
9
|
+
readonly serverMetadata: ReadonlyJson;
|
|
10
|
+
readonly primaryEmail: string | null;
|
|
11
|
+
readonly primaryEmailVerified: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type ServerAuthApplicationOptions = (ClientInterfaceOptions & ({
|
|
14
|
+
readonly secretServerKey: string;
|
|
15
|
+
} | {
|
|
16
|
+
readonly internalAdminAccessToken: string;
|
|
17
|
+
}));
|
|
18
|
+
export declare class StackServerInterface extends StackClientInterface {
|
|
19
|
+
options: ServerAuthApplicationOptions;
|
|
20
|
+
readonly currentServerUserCache: AsyncCache<TokenStore, ServerUserJson | null>;
|
|
21
|
+
constructor(options: ServerAuthApplicationOptions);
|
|
22
|
+
refreshUser(tokenStore: TokenStore): Promise<void>;
|
|
23
|
+
protected sendServerRequest(path: string, options: RequestInit, tokenStore: TokenStore | null): Promise<Response & {
|
|
24
|
+
usedTokens: Readonly<{
|
|
25
|
+
refreshToken: string | null;
|
|
26
|
+
accessToken: string | null;
|
|
27
|
+
}>;
|
|
28
|
+
}>;
|
|
29
|
+
getServerUserByToken(tokenStore: TokenStore): Promise<Result<ServerUserJson>>;
|
|
30
|
+
listUsers(): Promise<ServerUserJson[]>;
|
|
31
|
+
setServerUserCustomizableData(userId: string, update: Partial<ServerUserCustomizableJson>): Promise<void>;
|
|
32
|
+
deleteServerUser(userId: string): Promise<void>;
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serverInterface.d.ts","sourceRoot":"","sources":["../../src/interface/serverInterface.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,QAAQ,EACR,UAAU,EACV,oBAAoB,EACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAG7C,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG;IACtC,QAAQ,CAAC,cAAc,EAAE,YAAY,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG,oBAAoB,GAAG;IAC9D,QAAQ,CAAC,cAAc,EAAE,YAAY,CAAC;IACtC,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAC;CACxC,CAAA;AAGD,MAAM,MAAM,4BAA4B,GAAG,CACvC,sBAAsB,GACtB,CACE;IACA,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC,GACC;IACA,QAAQ,CAAC,wBAAwB,EAAE,MAAM,CAAC;CAC3C,CACF,CACF,CAAC;AAGF,qBAAa,oBAAqB,SAAQ,oBAAoB;IAKhC,OAAO,EAAE,4BAA4B;IAFjE,SAAgB,sBAAsB,EAAE,UAAU,CAAC,UAAU,EAAE,cAAc,GAAG,IAAI,CAAC,CAAC;gBAE1D,OAAO,EAAE,4BAA4B;IAelD,WAAW,CAAC,UAAU,EAAE,UAAU;cAOjC,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,GAAG,IAAI;;;;;;IAc7F,oBAAoB,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;IAW7E,SAAS,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAKtC,6BAA6B,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,0BAA0B,CAAC;IAczF,gBAAgB,CAAC,MAAM,EAAE,MAAM;CAatC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { StackClientInterface, } from "./clientInterface";
|
|
2
|
+
import { Result } from "../utils/results";
|
|
3
|
+
import { AsyncCache } from "../utils/caches";
|
|
4
|
+
import { runAsynchronously } from "../utils/promises";
|
|
5
|
+
export class StackServerInterface extends StackClientInterface {
|
|
6
|
+
options;
|
|
7
|
+
// note that we intentionally use TokenStore (a reference type) as a key, as different token stores with the same tokens should be treated differently
|
|
8
|
+
// (if we wouldn't do that, we would cache users across requests, which may cause caching issues)
|
|
9
|
+
currentServerUserCache;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
super(options);
|
|
12
|
+
this.options = options;
|
|
13
|
+
this.currentServerUserCache = new AsyncCache(async (key, isFirst) => {
|
|
14
|
+
if (isFirst) {
|
|
15
|
+
key.onChange((newValue, oldValue) => {
|
|
16
|
+
if (JSON.stringify(newValue) === JSON.stringify(oldValue))
|
|
17
|
+
return;
|
|
18
|
+
runAsynchronously(this.currentServerUserCache.refresh(key));
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
const user = await this.getServerUserByToken(key);
|
|
22
|
+
return Result.or(user, null);
|
|
23
|
+
});
|
|
24
|
+
// TODO override the client user cache to use the server user cache, so we save some requests
|
|
25
|
+
}
|
|
26
|
+
async refreshUser(tokenStore) {
|
|
27
|
+
await Promise.all([
|
|
28
|
+
super.refreshUser(tokenStore),
|
|
29
|
+
this.currentServerUserCache.refresh(tokenStore),
|
|
30
|
+
]);
|
|
31
|
+
}
|
|
32
|
+
async sendServerRequest(path, options, tokenStore) {
|
|
33
|
+
return await this.sendClientRequest(path, {
|
|
34
|
+
...options,
|
|
35
|
+
headers: {
|
|
36
|
+
"x-stack-secret-server-key": "secretServerKey" in this.options ? this.options.secretServerKey : "",
|
|
37
|
+
...options.headers,
|
|
38
|
+
},
|
|
39
|
+
}, tokenStore);
|
|
40
|
+
}
|
|
41
|
+
async getServerUserByToken(tokenStore) {
|
|
42
|
+
const response = await this.sendServerRequest("/current-user?server=true", {}, tokenStore);
|
|
43
|
+
const user = await response.json();
|
|
44
|
+
if (!user)
|
|
45
|
+
return Result.error(new Error("Failed to get user"));
|
|
46
|
+
return Result.ok(user);
|
|
47
|
+
}
|
|
48
|
+
async listUsers() {
|
|
49
|
+
const response = await this.sendServerRequest("/users?server=true", {}, null);
|
|
50
|
+
return await response.json();
|
|
51
|
+
}
|
|
52
|
+
async setServerUserCustomizableData(userId, update) {
|
|
53
|
+
await this.sendServerRequest(`/users/${userId}?server=true`, {
|
|
54
|
+
method: "PUT",
|
|
55
|
+
headers: {
|
|
56
|
+
"content-type": "application/json",
|
|
57
|
+
},
|
|
58
|
+
body: JSON.stringify(update),
|
|
59
|
+
}, null);
|
|
60
|
+
}
|
|
61
|
+
async deleteServerUser(userId) {
|
|
62
|
+
await this.sendServerRequest(`/users/${userId}?server=true`, {
|
|
63
|
+
method: "DELETE",
|
|
64
|
+
headers: {
|
|
65
|
+
"content-type": "application/json",
|
|
66
|
+
},
|
|
67
|
+
body: JSON.stringify({}),
|
|
68
|
+
}, null);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function typedIncludes<T extends readonly any[]>(arr: T, item: unknown): item is T[number];
|
|
2
|
+
export declare function enumerate<T extends readonly any[]>(arr: T): [number, T[number]][];
|
|
3
|
+
export declare function isShallowEqual(a: readonly any[], b: readonly any[]): boolean;
|
|
4
|
+
export declare function groupBy<T extends readonly any[], K>(arr: T, key: (item: T[number]) => K): Map<K, T[number][]>;
|
|
5
|
+
export declare function range(endExclusive: number): number[];
|
|
6
|
+
export declare function range(startInclusive: number, endExclusive: number): number[];
|
|
7
|
+
export declare function range(startInclusive: number, endExclusive: number, step: number): number[];
|
|
8
|
+
export declare function rotateLeft(arr: readonly any[], n: number): any[];
|
|
9
|
+
export declare function rotateRight(arr: readonly any[], n: number): any[];
|
|
10
|
+
export declare function shuffle<T>(arr: readonly T[]): T[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"arrays.d.ts","sourceRoot":"","sources":["../../src/utils/arrays.tsx"],"names":[],"mappings":"AAEA,wBAAgB,aAAa,CAAC,CAAC,SAAS,SAAS,GAAG,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,CAAC,CAAC,MAAM,CAAC,CAEhG;AAED,wBAAgB,SAAS,CAAC,CAAC,SAAS,SAAS,GAAG,EAAE,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,CAEjF;AAED,wBAAgB,cAAc,CAAC,CAAC,EAAE,SAAS,GAAG,EAAE,EAAE,CAAC,EAAE,SAAS,GAAG,EAAE,GAAG,OAAO,CAM5E;AAED,wBAAgB,OAAO,CAAC,CAAC,SAAS,SAAS,GAAG,EAAE,EAAE,CAAC,EACjD,GAAG,EAAE,CAAC,EACN,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,GAC1B,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAQrB;AAGD,wBAAgB,KAAK,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;AACtD,wBAAgB,KAAK,CAAC,cAAc,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;AAC9E,wBAAgB,KAAK,CAAC,cAAc,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;AAgB5F,wBAAgB,UAAU,CAAC,GAAG,EAAE,SAAS,GAAG,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,GAAG,EAAE,CAGhE;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,SAAS,GAAG,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,GAAG,EAAE,CAEjE;AAGD,wBAAgB,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,SAAS,CAAC,EAAE,GAAG,CAAC,EAAE,CAOjD"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { remainder } from "./math";
|
|
2
|
+
export function typedIncludes(arr, item) {
|
|
3
|
+
return arr.includes(item);
|
|
4
|
+
}
|
|
5
|
+
export function enumerate(arr) {
|
|
6
|
+
return arr.map((item, index) => [index, item]);
|
|
7
|
+
}
|
|
8
|
+
export function isShallowEqual(a, b) {
|
|
9
|
+
if (a.length !== b.length)
|
|
10
|
+
return false;
|
|
11
|
+
for (let i = 0; i < a.length; i++) {
|
|
12
|
+
if (a[i] !== b[i])
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
export function groupBy(arr, key) {
|
|
18
|
+
const result = new Map;
|
|
19
|
+
for (const item of arr) {
|
|
20
|
+
const k = key(item);
|
|
21
|
+
if (result.get(k) === undefined)
|
|
22
|
+
result.set(k, []);
|
|
23
|
+
result.get(k).push(item);
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
export function range(startInclusive, endExclusive, step) {
|
|
28
|
+
if (endExclusive === undefined) {
|
|
29
|
+
endExclusive = startInclusive;
|
|
30
|
+
startInclusive = 0;
|
|
31
|
+
}
|
|
32
|
+
if (step === undefined)
|
|
33
|
+
step = 1;
|
|
34
|
+
const result = [];
|
|
35
|
+
for (let i = startInclusive; step > 0 ? (i < endExclusive) : (i > endExclusive); i += step) {
|
|
36
|
+
result.push(i);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
export function rotateLeft(arr, n) {
|
|
41
|
+
const index = remainder(n, arr.length);
|
|
42
|
+
return [...arr.slice(n), arr.slice(0, n)];
|
|
43
|
+
}
|
|
44
|
+
export function rotateRight(arr, n) {
|
|
45
|
+
return rotateLeft(arr, -n);
|
|
46
|
+
}
|
|
47
|
+
export function shuffle(arr) {
|
|
48
|
+
const result = [...arr];
|
|
49
|
+
for (let i = result.length - 1; i > 0; i--) {
|
|
50
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
51
|
+
[result[i], result[j]] = [result[j], result[i]];
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|