@lumibase/sdk 0.20.0 → 0.21.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/dist/index.cjs +50 -3
- package/dist/index.d.cts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +50 -3
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -111,12 +111,52 @@ function toErrorBody(status, body) {
|
|
|
111
111
|
function createLumiClient(opts) {
|
|
112
112
|
const fetcher = opts.fetcher ?? globalThis.fetch;
|
|
113
113
|
const base = opts.url.replace(/\/$/, "");
|
|
114
|
-
|
|
114
|
+
let currentToken = opts.token;
|
|
115
|
+
let currentRefreshToken = opts.refreshToken;
|
|
116
|
+
let refreshInFlight = null;
|
|
117
|
+
async function attemptRefresh() {
|
|
118
|
+
if (!currentRefreshToken) return false;
|
|
119
|
+
if (!refreshInFlight) {
|
|
120
|
+
refreshInFlight = (async () => {
|
|
121
|
+
try {
|
|
122
|
+
const res = await fetcher(`${base}/api/v1/auth/refresh`, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: {
|
|
125
|
+
"content-type": "application/json",
|
|
126
|
+
"x-lumi-site": opts.siteId
|
|
127
|
+
},
|
|
128
|
+
// Body transport is CSRF-exempt (the token isn't ambient).
|
|
129
|
+
body: JSON.stringify({ refreshToken: currentRefreshToken })
|
|
130
|
+
});
|
|
131
|
+
if (!res.ok) return false;
|
|
132
|
+
const data = parseResponseBody(await res.text())?.data;
|
|
133
|
+
if (!data?.token || !data.refreshToken) return false;
|
|
134
|
+
currentToken = data.token;
|
|
135
|
+
currentRefreshToken = data.refreshToken;
|
|
136
|
+
try {
|
|
137
|
+
opts.onTokensRefreshed?.({
|
|
138
|
+
token: data.token,
|
|
139
|
+
refreshToken: data.refreshToken,
|
|
140
|
+
refreshTokenExpiresAt: data.refreshTokenExpiresAt
|
|
141
|
+
});
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
return true;
|
|
145
|
+
} catch {
|
|
146
|
+
return false;
|
|
147
|
+
} finally {
|
|
148
|
+
refreshInFlight = null;
|
|
149
|
+
}
|
|
150
|
+
})();
|
|
151
|
+
}
|
|
152
|
+
return refreshInFlight;
|
|
153
|
+
}
|
|
154
|
+
async function rawRequest(path, init = {}, retried = false) {
|
|
115
155
|
const headers = new Headers(init.headers);
|
|
116
156
|
for (const [key, value] of Object.entries(opts.headers ?? {})) {
|
|
117
157
|
headers.set(key, value);
|
|
118
158
|
}
|
|
119
|
-
headers.set("authorization", `Bearer ${
|
|
159
|
+
headers.set("authorization", `Bearer ${currentToken}`);
|
|
120
160
|
headers.set("x-lumi-site", opts.siteId);
|
|
121
161
|
if (!headers.has("content-type") && init.body) {
|
|
122
162
|
headers.set("content-type", "application/json");
|
|
@@ -125,6 +165,11 @@ function createLumiClient(opts) {
|
|
|
125
165
|
const text = await res.text();
|
|
126
166
|
const body = parseResponseBody(text);
|
|
127
167
|
if (!res.ok) {
|
|
168
|
+
if (res.status === 401 && !retried && currentRefreshToken && path !== "/api/v1/auth/refresh") {
|
|
169
|
+
if (await attemptRefresh()) {
|
|
170
|
+
return rawRequest(path, init, true);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
128
173
|
if (res.status === 401 && opts.onUnauthorized) {
|
|
129
174
|
try {
|
|
130
175
|
opts.onUnauthorized();
|
|
@@ -137,7 +182,9 @@ function createLumiClient(opts) {
|
|
|
137
182
|
}
|
|
138
183
|
const baseClient = {
|
|
139
184
|
url: opts.url,
|
|
140
|
-
token
|
|
185
|
+
get token() {
|
|
186
|
+
return currentToken;
|
|
187
|
+
},
|
|
141
188
|
siteId: opts.siteId,
|
|
142
189
|
fetcher,
|
|
143
190
|
rawRequest,
|
package/dist/index.d.cts
CHANGED
|
@@ -1371,8 +1371,29 @@ interface LumiClientOptions {
|
|
|
1371
1371
|
* is thrown so a host (e.g. Studio) can clear the stale token and route
|
|
1372
1372
|
* the operator back to the login screen. Errors thrown by the callback
|
|
1373
1373
|
* are swallowed so they never mask the original `LumiError`.
|
|
1374
|
+
*
|
|
1375
|
+
* When auto-refresh is enabled (see `refreshToken`), `onUnauthorized`
|
|
1376
|
+
* fires only AFTER a refresh attempt has also failed.
|
|
1374
1377
|
*/
|
|
1375
1378
|
onUnauthorized?: () => void;
|
|
1379
|
+
/**
|
|
1380
|
+
* Rotating refresh token (body transport). When set, a `401` triggers a
|
|
1381
|
+
* single `POST /api/v1/auth/refresh` with this token; on success the new
|
|
1382
|
+
* access token is adopted and the original request is retried once. The
|
|
1383
|
+
* refresh token rotates each call, so the new pair is surfaced via
|
|
1384
|
+
* {@link onTokensRefreshed} for the host to persist. Omit to keep the
|
|
1385
|
+
* legacy behaviour (no retry; `onUnauthorized` fires immediately).
|
|
1386
|
+
*/
|
|
1387
|
+
refreshToken?: string;
|
|
1388
|
+
/**
|
|
1389
|
+
* Called after a successful silent refresh with the rotated token pair so
|
|
1390
|
+
* the host can persist them (the old refresh token is now revoked).
|
|
1391
|
+
*/
|
|
1392
|
+
onTokensRefreshed?: (tokens: {
|
|
1393
|
+
token: string;
|
|
1394
|
+
refreshToken: string;
|
|
1395
|
+
refreshTokenExpiresAt?: string;
|
|
1396
|
+
}) => void;
|
|
1376
1397
|
}
|
|
1377
1398
|
interface LumiResponse<T> {
|
|
1378
1399
|
data: T;
|
package/dist/index.d.ts
CHANGED
|
@@ -1371,8 +1371,29 @@ interface LumiClientOptions {
|
|
|
1371
1371
|
* is thrown so a host (e.g. Studio) can clear the stale token and route
|
|
1372
1372
|
* the operator back to the login screen. Errors thrown by the callback
|
|
1373
1373
|
* are swallowed so they never mask the original `LumiError`.
|
|
1374
|
+
*
|
|
1375
|
+
* When auto-refresh is enabled (see `refreshToken`), `onUnauthorized`
|
|
1376
|
+
* fires only AFTER a refresh attempt has also failed.
|
|
1374
1377
|
*/
|
|
1375
1378
|
onUnauthorized?: () => void;
|
|
1379
|
+
/**
|
|
1380
|
+
* Rotating refresh token (body transport). When set, a `401` triggers a
|
|
1381
|
+
* single `POST /api/v1/auth/refresh` with this token; on success the new
|
|
1382
|
+
* access token is adopted and the original request is retried once. The
|
|
1383
|
+
* refresh token rotates each call, so the new pair is surfaced via
|
|
1384
|
+
* {@link onTokensRefreshed} for the host to persist. Omit to keep the
|
|
1385
|
+
* legacy behaviour (no retry; `onUnauthorized` fires immediately).
|
|
1386
|
+
*/
|
|
1387
|
+
refreshToken?: string;
|
|
1388
|
+
/**
|
|
1389
|
+
* Called after a successful silent refresh with the rotated token pair so
|
|
1390
|
+
* the host can persist them (the old refresh token is now revoked).
|
|
1391
|
+
*/
|
|
1392
|
+
onTokensRefreshed?: (tokens: {
|
|
1393
|
+
token: string;
|
|
1394
|
+
refreshToken: string;
|
|
1395
|
+
refreshTokenExpiresAt?: string;
|
|
1396
|
+
}) => void;
|
|
1376
1397
|
}
|
|
1377
1398
|
interface LumiResponse<T> {
|
|
1378
1399
|
data: T;
|
package/dist/index.js
CHANGED
|
@@ -34,12 +34,52 @@ function toErrorBody(status, body) {
|
|
|
34
34
|
function createLumiClient(opts) {
|
|
35
35
|
const fetcher = opts.fetcher ?? globalThis.fetch;
|
|
36
36
|
const base = opts.url.replace(/\/$/, "");
|
|
37
|
-
|
|
37
|
+
let currentToken = opts.token;
|
|
38
|
+
let currentRefreshToken = opts.refreshToken;
|
|
39
|
+
let refreshInFlight = null;
|
|
40
|
+
async function attemptRefresh() {
|
|
41
|
+
if (!currentRefreshToken) return false;
|
|
42
|
+
if (!refreshInFlight) {
|
|
43
|
+
refreshInFlight = (async () => {
|
|
44
|
+
try {
|
|
45
|
+
const res = await fetcher(`${base}/api/v1/auth/refresh`, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: {
|
|
48
|
+
"content-type": "application/json",
|
|
49
|
+
"x-lumi-site": opts.siteId
|
|
50
|
+
},
|
|
51
|
+
// Body transport is CSRF-exempt (the token isn't ambient).
|
|
52
|
+
body: JSON.stringify({ refreshToken: currentRefreshToken })
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) return false;
|
|
55
|
+
const data = parseResponseBody(await res.text())?.data;
|
|
56
|
+
if (!data?.token || !data.refreshToken) return false;
|
|
57
|
+
currentToken = data.token;
|
|
58
|
+
currentRefreshToken = data.refreshToken;
|
|
59
|
+
try {
|
|
60
|
+
opts.onTokensRefreshed?.({
|
|
61
|
+
token: data.token,
|
|
62
|
+
refreshToken: data.refreshToken,
|
|
63
|
+
refreshTokenExpiresAt: data.refreshTokenExpiresAt
|
|
64
|
+
});
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
} finally {
|
|
71
|
+
refreshInFlight = null;
|
|
72
|
+
}
|
|
73
|
+
})();
|
|
74
|
+
}
|
|
75
|
+
return refreshInFlight;
|
|
76
|
+
}
|
|
77
|
+
async function rawRequest(path, init = {}, retried = false) {
|
|
38
78
|
const headers = new Headers(init.headers);
|
|
39
79
|
for (const [key, value] of Object.entries(opts.headers ?? {})) {
|
|
40
80
|
headers.set(key, value);
|
|
41
81
|
}
|
|
42
|
-
headers.set("authorization", `Bearer ${
|
|
82
|
+
headers.set("authorization", `Bearer ${currentToken}`);
|
|
43
83
|
headers.set("x-lumi-site", opts.siteId);
|
|
44
84
|
if (!headers.has("content-type") && init.body) {
|
|
45
85
|
headers.set("content-type", "application/json");
|
|
@@ -48,6 +88,11 @@ function createLumiClient(opts) {
|
|
|
48
88
|
const text = await res.text();
|
|
49
89
|
const body = parseResponseBody(text);
|
|
50
90
|
if (!res.ok) {
|
|
91
|
+
if (res.status === 401 && !retried && currentRefreshToken && path !== "/api/v1/auth/refresh") {
|
|
92
|
+
if (await attemptRefresh()) {
|
|
93
|
+
return rawRequest(path, init, true);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
51
96
|
if (res.status === 401 && opts.onUnauthorized) {
|
|
52
97
|
try {
|
|
53
98
|
opts.onUnauthorized();
|
|
@@ -60,7 +105,9 @@ function createLumiClient(opts) {
|
|
|
60
105
|
}
|
|
61
106
|
const baseClient = {
|
|
62
107
|
url: opts.url,
|
|
63
|
-
token
|
|
108
|
+
get token() {
|
|
109
|
+
return currentToken;
|
|
110
|
+
},
|
|
64
111
|
siteId: opts.siteId,
|
|
65
112
|
fetcher,
|
|
66
113
|
rawRequest,
|