Skip to content

Commit 5cd51b9

Browse files
committed
JWT management for xet access (WIP)
1 parent d83bfe8 commit 5cd51b9

File tree

1 file changed

+101
-1
lines changed

1 file changed

+101
-1
lines changed

packages/hub/src/utils/XetBlob.ts

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1-
import type { CredentialsParams } from "../types/public";
1+
import { HUB_URL } from "../consts";
2+
import type { CredentialsParams, RepoDesignation, RepoId } from "../types/public";
23
import { checkCredentials } from "./checkCredentials";
4+
import { toRepoId } from "./toRepoId";
5+
6+
const JWT_SAFETY_PERIOD = 60_000;
7+
const JWT_CACHE_SIZE = 1_000;
38

49
type XetBlobCreateOptions = {
510
/**
611
* Custom fetch function to use instead of the default one, for example to use a proxy or edit headers.
712
*/
813
fetch?: typeof fetch;
14+
repo: RepoDesignation;
15+
hash: string;
16+
hubUrl?: string;
917
} & Partial<CredentialsParams>;
1018

1119
/**
@@ -14,11 +22,103 @@ type XetBlobCreateOptions = {
1422
export class XetBlob extends Blob {
1523
fetch: typeof fetch;
1624
accessToken?: string;
25+
repoId: RepoId;
26+
hubUrl: string;
1727

1828
constructor(params: XetBlobCreateOptions) {
1929
super([]);
2030

2131
this.fetch = params.fetch ?? fetch;
2232
this.accessToken = checkCredentials(params);
33+
this.repoId = toRepoId(params.repo);
34+
this.hubUrl = params.hubUrl ?? HUB_URL;
35+
}
36+
}
37+
38+
const jwtPromises: Map<string, Promise<{ accessToken: string; casUrl: string }>> = new Map();
39+
/**
40+
* Cache to store JWTs, to avoid making many auth requests when downloading multiple files from the same repo
41+
*/
42+
const jwts: Map<
43+
string,
44+
{
45+
accessToken: string;
46+
expiresAt: Date;
47+
casUrl: string;
48+
}
49+
> = new Map();
50+
51+
function cacheKey(params: { repoId: RepoId; initialAccessToken: string | undefined }): string {
52+
return `${params.repoId.type}:${params.repoId.name}:${params.initialAccessToken}`;
53+
}
54+
55+
async function getAccessToken(
56+
repoId: RepoId,
57+
initialAccessToken: string | undefined,
58+
customFetch: typeof fetch,
59+
hubUrl: string
60+
): Promise<{ accessToken: string; casUrl: string }> {
61+
const key = cacheKey({ repoId, initialAccessToken });
62+
63+
const jwt = jwts.get(key);
64+
65+
if (jwt && jwt.expiresAt > new Date(Date.now() + JWT_SAFETY_PERIOD)) {
66+
return { accessToken: jwt.accessToken, casUrl: jwt.casUrl };
67+
}
68+
69+
// If we already have a promise for this repo, return it
70+
const existingPromise = jwtPromises.get(key);
71+
if (existingPromise) {
72+
return existingPromise;
2373
}
74+
75+
const promise = (async () => {
76+
const url = `${hubUrl}/api/${repoId.type}s/${repoId.name}/xet-read-token/main`;
77+
const resp = await customFetch(url, {
78+
headers: {
79+
...(initialAccessToken
80+
? {
81+
Authorization: `Bearer ${initialAccessToken}`,
82+
}
83+
: {}),
84+
},
85+
});
86+
87+
if (!resp.ok) {
88+
throw new Error(`Failed to get JWT token: ${resp.status} ${await resp.text()}`);
89+
}
90+
91+
const json = await resp.json();
92+
const jwt = {
93+
repoId,
94+
accessToken: json.token,
95+
expiresAt: new Date(json.exp * 1000),
96+
initialAccessToken,
97+
hubUrl,
98+
casUrl: json.casUrl,
99+
};
100+
101+
jwtPromises.delete(key);
102+
103+
for (const [key, value] of jwts.entries()) {
104+
if (value.expiresAt < new Date(Date.now() + JWT_SAFETY_PERIOD)) {
105+
jwts.delete(key);
106+
} else {
107+
break;
108+
}
109+
}
110+
if (jwts.size >= JWT_CACHE_SIZE) {
111+
const keyToDelete = jwts.keys().next().value;
112+
if (keyToDelete) {
113+
jwts.delete(keyToDelete);
114+
}
115+
}
116+
jwts.set(key, jwt);
117+
118+
return jwt.accessToken;
119+
})();
120+
121+
jwtPromises.set(repoId.name, promise);
122+
123+
return promise;
24124
}

0 commit comments

Comments
 (0)