@stage5/lumine 0.2.61 → 0.2.63
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 +9 -6
- package/lib/admin-workflows.js +8 -1
- package/lib/admin.js +110 -13
- package/lib/api.js +1 -0
- package/lib/auth.js +5 -1
- package/lib/commands.js +1 -0
- package/lib/forum.js +1 -0
- package/lib/http.js +28 -8
- package/lib/sdk.js +2 -3
- package/lib/sponsor-duty.js +343 -136
- package/lib/util.js +15 -2
- package/package.json +1 -1
- package/sdk/LUMINE_ADMIN.md +207 -30
package/README.md
CHANGED
|
@@ -284,7 +284,7 @@ starts unless they accept it there. This notice is invitation metadata only;
|
|
|
284
284
|
it does not expose the unapproved Workshop plan or private chat.
|
|
285
285
|
|
|
286
286
|
When a user approves a plan, `duty watch` returns the scoped assignment file and
|
|
287
|
-
|
|
287
|
+
exact approved Build workspace to that same session. It does not launch a fresh Codex
|
|
288
288
|
or Claude process. The on-duty agent uses this explicit lifecycle while it
|
|
289
289
|
works:
|
|
290
290
|
|
|
@@ -298,7 +298,7 @@ lumine sponsor job complete 42 --summary "Implemented and tested the approved ch
|
|
|
298
298
|
|
|
299
299
|
`pulse` renews the duty/job leases and returns any newly approved follow-up.
|
|
300
300
|
`update` sends the exact file text back to Zero or Ciel as a user-visible
|
|
301
|
-
message from Lumine,
|
|
301
|
+
message from Lumine, the on-duty project collaborator they talk with. Use it for concise, deliberate
|
|
302
302
|
milestones only; never send hidden reasoning, raw terminal output, credentials,
|
|
303
303
|
tokens, private paths, or unrelated data. Run `pulse` between substantial work
|
|
304
304
|
steps. `relay-applied` is an explicit receipt;
|
|
@@ -318,11 +318,14 @@ shows none of the Workshop UI.
|
|
|
318
318
|
|
|
319
319
|
Zero or Ciel remains the user-facing teammate. The on-duty agent session
|
|
320
320
|
receives only the initial relay and active-job Build follow-ups covered by the
|
|
321
|
-
user’s explicit Workshop consent
|
|
322
|
-
|
|
323
|
-
|
|
321
|
+
user’s explicit Workshop consent and the exact approved Main or requester-owned
|
|
322
|
+
branch—not the raw assistant chat. Temporary job access never includes Forum
|
|
323
|
+
comments. A Forum snapshot is available only when the sponsor's normal account
|
|
324
|
+
already owns the project or is an accepted teammate. Editing jobs save directly
|
|
325
|
+
to the approved workspace after Twinkle creates a restore point; they cannot
|
|
326
|
+
publish or use website-management APIs. Lumine records
|
|
324
327
|
the requested and provider-reported model, effort, service tier, runtime/usage
|
|
325
|
-
evidence, coordinator/helper tree,
|
|
328
|
+
evidence, coordinator/helper tree, restore point, and saved artifact. Every
|
|
326
329
|
completed handoff enters the daily integrity flow; probationary, hard-flagged,
|
|
327
330
|
and sampled work requires review. Each unique cleared contribution to another
|
|
328
331
|
user earns a flat 50 Karma Points, while self-sponsored testing, retries, and
|
package/lib/admin-workflows.js
CHANGED
|
@@ -542,6 +542,12 @@ function aggregatePageResult({ operation, lastPage, checkpointPath, state }) {
|
|
|
542
542
|
const last = lastPage || { ok: true, status: "success", data: {} };
|
|
543
543
|
const lastData = { ...(last.data || {}) };
|
|
544
544
|
delete lastData[operation.pagination.collectionKey];
|
|
545
|
+
const lastPagination = { ...(last.data?.pagination || {}) };
|
|
546
|
+
const pageScannedCount = normalizeScannedCount(
|
|
547
|
+
lastPagination.scannedCount,
|
|
548
|
+
0,
|
|
549
|
+
);
|
|
550
|
+
delete lastPagination.scannedCount;
|
|
545
551
|
if (state.filterSummariesComplete) {
|
|
546
552
|
if (state.clientFilter) lastData.clientFilter = state.clientFilter;
|
|
547
553
|
else delete lastData.clientFilter;
|
|
@@ -568,7 +574,8 @@ function aggregatePageResult({ operation, lastPage, checkpointPath, state }) {
|
|
|
568
574
|
data: {
|
|
569
575
|
...lastData,
|
|
570
576
|
pagination: {
|
|
571
|
-
...
|
|
577
|
+
...lastPagination,
|
|
578
|
+
pageScannedCount,
|
|
572
579
|
nextCursor: state.nextCursor,
|
|
573
580
|
hasMore: !state.exhausted,
|
|
574
581
|
exhausted: state.exhausted,
|
package/lib/admin.js
CHANGED
|
@@ -171,12 +171,32 @@ export async function adminCommand(options) {
|
|
|
171
171
|
scope: operation.mutates ? "build:write" : "build:read",
|
|
172
172
|
});
|
|
173
173
|
let runId = 0;
|
|
174
|
+
let correctionSessionId = 0;
|
|
175
|
+
let correctionSession = null;
|
|
174
176
|
if (adminOperationRequiresRun(operation)) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
if (operation.correctionEligible) {
|
|
178
|
+
const correctionStatus = await requestJson({
|
|
179
|
+
url: `${options.apiUrl}/cli/admin/corrections/status`,
|
|
180
|
+
authToken: auth.token,
|
|
181
|
+
timeoutMs: options.timeoutMs,
|
|
182
|
+
});
|
|
183
|
+
const correction = correctionStatus?.data?.correction || null;
|
|
184
|
+
if (
|
|
185
|
+
correction?.status === "active" &&
|
|
186
|
+
Number(correction.correctionCommentId || 0) ===
|
|
187
|
+
Number(operation.correctionCommentId || 0)
|
|
188
|
+
) {
|
|
189
|
+
correctionSessionId = Number(correction.id || 0);
|
|
190
|
+
correctionSession = correction;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const runStatus = correctionSessionId
|
|
194
|
+
? null
|
|
195
|
+
: await requestJson({
|
|
196
|
+
url: `${options.apiUrl}/cli/admin/daily-runs/status`,
|
|
197
|
+
authToken: auth.token,
|
|
198
|
+
timeoutMs: options.timeoutMs,
|
|
199
|
+
});
|
|
180
200
|
const activeRun = runStatus?.data?.run || null;
|
|
181
201
|
const retryableFinishedRun = [
|
|
182
202
|
"daily-run.complete",
|
|
@@ -186,15 +206,29 @@ export async function adminCommand(options) {
|
|
|
186
206
|
: null;
|
|
187
207
|
const selectedRun = activeRun || retryableFinishedRun;
|
|
188
208
|
runId = Number(selectedRun?.id || 0);
|
|
189
|
-
if (!runId
|
|
190
|
-
|
|
209
|
+
if (!runId && !correctionSessionId) {
|
|
210
|
+
if (operation.correctionEligible) {
|
|
211
|
+
const error = new Error(
|
|
212
|
+
`Start a correction session first: lumine admin correction start ${operation.correctionCommentId}.`,
|
|
213
|
+
);
|
|
214
|
+
error.code = "CLI_ADMIN_CORRECTION_NOT_ACTIVE";
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
throw noActiveRunError();
|
|
218
|
+
}
|
|
219
|
+
if (options.adminIdentity && (selectedRun || correctionSession)) {
|
|
191
220
|
const requestedIdentity = parseIdentity(options.adminIdentity);
|
|
221
|
+
const canonicalIdentity = correctionSession
|
|
222
|
+
? correctionSession.identity?.key
|
|
223
|
+
: selectedRun?.identity?.key;
|
|
192
224
|
if (
|
|
193
225
|
requestedIdentity !== "auto" &&
|
|
194
|
-
requestedIdentity !==
|
|
226
|
+
requestedIdentity !== canonicalIdentity
|
|
195
227
|
) {
|
|
196
228
|
throw cliValidationError(
|
|
197
|
-
|
|
229
|
+
correctionSession
|
|
230
|
+
? `--identity ${requestedIdentity} does not match correction session #${correctionSessionId} (${canonicalIdentity || "unknown"}).`
|
|
231
|
+
: `--identity ${requestedIdentity} does not match active run #${runId} (${canonicalIdentity || "unknown"}).`,
|
|
198
232
|
);
|
|
199
233
|
}
|
|
200
234
|
}
|
|
@@ -211,6 +245,13 @@ export async function adminCommand(options) {
|
|
|
211
245
|
body: operation.body,
|
|
212
246
|
headers: {
|
|
213
247
|
...(runId ? { "x-lumine-admin-run-id": String(runId) } : {}),
|
|
248
|
+
...(correctionSessionId
|
|
249
|
+
? {
|
|
250
|
+
"x-lumine-admin-correction-session-id": String(
|
|
251
|
+
correctionSessionId,
|
|
252
|
+
),
|
|
253
|
+
}
|
|
254
|
+
: {}),
|
|
214
255
|
...(requestId ? { "x-lumine-idempotency-key": requestId } : {}),
|
|
215
256
|
},
|
|
216
257
|
timeoutMs: options.timeoutMs,
|
|
@@ -663,6 +704,9 @@ function adminOperationRequiresRun(operation) {
|
|
|
663
704
|
"rescue.wordle.audit",
|
|
664
705
|
"daily-run.start",
|
|
665
706
|
"daily-run.status",
|
|
707
|
+
"correction.start",
|
|
708
|
+
"correction.status",
|
|
709
|
+
"correction.complete",
|
|
666
710
|
"escalation.list",
|
|
667
711
|
"escalation.set",
|
|
668
712
|
"notable.add",
|
|
@@ -694,6 +738,39 @@ export function parseAdminOperation(options) {
|
|
|
694
738
|
const [namespace = "", action = "", target = "", extra = ""] =
|
|
695
739
|
options.positional;
|
|
696
740
|
|
|
741
|
+
if (namespace === "correction" || namespace === "corrections") {
|
|
742
|
+
if (!action || action === "status") {
|
|
743
|
+
return readOperation("correction.status", "/cli/admin/corrections/status");
|
|
744
|
+
}
|
|
745
|
+
if (action === "start") {
|
|
746
|
+
const commentId = parseRequiredInteger(target, "comment ID", 1);
|
|
747
|
+
return writeOperation(
|
|
748
|
+
"correction.start",
|
|
749
|
+
"POST",
|
|
750
|
+
"/cli/admin/corrections",
|
|
751
|
+
{
|
|
752
|
+
commentId,
|
|
753
|
+
},
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
if (action === "complete" || action === "stop") {
|
|
757
|
+
const correctionId = parseRequiredInteger(
|
|
758
|
+
target,
|
|
759
|
+
"correction session ID",
|
|
760
|
+
1,
|
|
761
|
+
);
|
|
762
|
+
return writeOperation(
|
|
763
|
+
"correction.complete",
|
|
764
|
+
"POST",
|
|
765
|
+
`/cli/admin/corrections/${correctionId}/complete`,
|
|
766
|
+
{},
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
throw cliValidationError(
|
|
770
|
+
"Usage: lumine admin correction start <commentId> | correction status | correction complete <sessionId>.",
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
|
|
697
774
|
if (namespace === "identity") {
|
|
698
775
|
if (action === "list") {
|
|
699
776
|
return readOperation("identity.list", "/cli/admin/identities");
|
|
@@ -1260,7 +1337,10 @@ export function parseAdminOperation(options) {
|
|
|
1260
1337
|
|
|
1261
1338
|
if (namespace === "comments" && action === "get") {
|
|
1262
1339
|
const commentId = parseRequiredInteger(target, "comment ID", 1);
|
|
1263
|
-
return readOperation("comments.get", `/cli/admin/comments/${commentId}
|
|
1340
|
+
return readOperation("comments.get", `/cli/admin/comments/${commentId}`, {
|
|
1341
|
+
correctionEligible: true,
|
|
1342
|
+
correctionCommentId: commentId,
|
|
1343
|
+
});
|
|
1264
1344
|
}
|
|
1265
1345
|
|
|
1266
1346
|
if (namespace === "post") {
|
|
@@ -1674,6 +1754,10 @@ export function parseAdminOperation(options) {
|
|
|
1674
1754
|
"PUT",
|
|
1675
1755
|
`/cli/admin/comments/${commentId}`,
|
|
1676
1756
|
{ content: readComposedTextFile(options.adminFile) },
|
|
1757
|
+
{
|
|
1758
|
+
correctionEligible: true,
|
|
1759
|
+
correctionCommentId: commentId,
|
|
1760
|
+
},
|
|
1677
1761
|
);
|
|
1678
1762
|
}
|
|
1679
1763
|
if (action === "post") {
|
|
@@ -1738,7 +1822,10 @@ function postGetOperation(target, options) {
|
|
|
1738
1822
|
return subjectGetOperation(parsedTarget.id, options);
|
|
1739
1823
|
}
|
|
1740
1824
|
if (parsedTarget.type === "comment") {
|
|
1741
|
-
return readOperation("post.get", `/cli/admin/comments/${parsedTarget.id}
|
|
1825
|
+
return readOperation("post.get", `/cli/admin/comments/${parsedTarget.id}`, {
|
|
1826
|
+
correctionEligible: true,
|
|
1827
|
+
correctionCommentId: parsedTarget.id,
|
|
1828
|
+
});
|
|
1742
1829
|
}
|
|
1743
1830
|
return readOperation(
|
|
1744
1831
|
"post.get",
|
|
@@ -2066,8 +2153,8 @@ function bodyReadOperation(name, method, path, body) {
|
|
|
2066
2153
|
return { name, method, path, body, mutates: false };
|
|
2067
2154
|
}
|
|
2068
2155
|
|
|
2069
|
-
function writeOperation(name, method, path, body) {
|
|
2070
|
-
return { name, method, path, body, mutates: true };
|
|
2156
|
+
function writeOperation(name, method, path, body, extra = {}) {
|
|
2157
|
+
return { name, method, path, body, mutates: true, ...extra };
|
|
2071
2158
|
}
|
|
2072
2159
|
|
|
2073
2160
|
function withQuery(path, values) {
|
|
@@ -2810,6 +2897,11 @@ function printAdminResult({ operation, result }) {
|
|
|
2810
2897
|
console.log(
|
|
2811
2898
|
`Run #${data.run.id}: ${data.run.status}; identity ${data.run.identity.key}; comments ${data.run.commentMode}.`,
|
|
2812
2899
|
);
|
|
2900
|
+
if (data.scheduledDay && data.scheduledIdentity) {
|
|
2901
|
+
console.log(
|
|
2902
|
+
`Bangkok schedule for ${data.scheduledDay}: ${data.scheduledIdentity.key}.`,
|
|
2903
|
+
);
|
|
2904
|
+
}
|
|
2813
2905
|
if (data.carryoverTodos) {
|
|
2814
2906
|
printTodoItems(data.carryoverTodos.items || [], "Carry-over work");
|
|
2815
2907
|
}
|
|
@@ -2824,6 +2916,11 @@ function printAdminResult({ operation, result }) {
|
|
|
2824
2916
|
}
|
|
2825
2917
|
if (Object.hasOwn(data, "activeRun")) {
|
|
2826
2918
|
console.log(`Preferred identity: ${data.preferredIdentity}.`);
|
|
2919
|
+
if (data.scheduledDay && data.scheduledIdentity) {
|
|
2920
|
+
console.log(
|
|
2921
|
+
`Bangkok schedule for ${data.scheduledDay}: ${data.scheduledIdentity.key}.`,
|
|
2922
|
+
);
|
|
2923
|
+
}
|
|
2827
2924
|
console.log(
|
|
2828
2925
|
`Last completed identity: ${data.lastCompletedIdentity || "none"}.`,
|
|
2829
2926
|
);
|
package/lib/api.js
CHANGED
package/lib/auth.js
CHANGED
|
@@ -23,6 +23,7 @@ export async function login(options) {
|
|
|
23
23
|
],
|
|
24
24
|
},
|
|
25
25
|
timeoutMs: options.timeoutMs,
|
|
26
|
+
signal: options.signal,
|
|
26
27
|
});
|
|
27
28
|
|
|
28
29
|
const approvalUrl = start.verificationUriComplete || start.verificationUri;
|
|
@@ -41,7 +42,7 @@ export async function login(options) {
|
|
|
41
42
|
const expiresInMs = Math.max(Number(start.expiresIn || 600), 1) * 1000;
|
|
42
43
|
|
|
43
44
|
while (Date.now() - startedAt < expiresInMs) {
|
|
44
|
-
await sleep(intervalMs);
|
|
45
|
+
await sleep(intervalMs, options.signal);
|
|
45
46
|
const tokenResponse = await pollToken({
|
|
46
47
|
options,
|
|
47
48
|
deviceCode: start.deviceCode,
|
|
@@ -72,6 +73,7 @@ export async function pollToken({ options, deviceCode }) {
|
|
|
72
73
|
url: `${options.apiUrl}/cli/device/token`,
|
|
73
74
|
body: { deviceCode },
|
|
74
75
|
timeoutMs: options.timeoutMs,
|
|
76
|
+
signal: options.signal,
|
|
75
77
|
});
|
|
76
78
|
} catch (error) {
|
|
77
79
|
if (error.status === 428 || error.data?.error === "authorization_pending") {
|
|
@@ -102,6 +104,7 @@ export async function whoami(options) {
|
|
|
102
104
|
url: `${options.apiUrl}/cli/session`,
|
|
103
105
|
authToken: auth.token,
|
|
104
106
|
timeoutMs: options.timeoutMs,
|
|
107
|
+
signal: options.signal,
|
|
105
108
|
});
|
|
106
109
|
console.log(
|
|
107
110
|
`Logged in as ${session.username || auth.username || "unknown"} ` +
|
|
@@ -187,6 +190,7 @@ export async function assertAuthScope({ options, auth, scope }) {
|
|
|
187
190
|
url: `${options.apiUrl}/cli/session`,
|
|
188
191
|
authToken: auth.token,
|
|
189
192
|
timeoutMs: options.timeoutMs,
|
|
193
|
+
signal: options.signal,
|
|
190
194
|
});
|
|
191
195
|
const scopes = Array.isArray(session.scopes) ? session.scopes : [];
|
|
192
196
|
if (!scopes.includes(scope)) {
|
package/lib/commands.js
CHANGED
|
@@ -1547,6 +1547,7 @@ export async function pullBuildFiles({ options, auth, buildId }) {
|
|
|
1547
1547
|
build,
|
|
1548
1548
|
dir,
|
|
1549
1549
|
fileCount: files.length,
|
|
1550
|
+
filesHash: typeof result.filesHash === "string" ? result.filesHash : null,
|
|
1550
1551
|
manifest: result.projectManifest || null,
|
|
1551
1552
|
assetCount,
|
|
1552
1553
|
};
|
package/lib/forum.js
CHANGED
package/lib/http.js
CHANGED
|
@@ -7,16 +7,17 @@ export async function requestJson({
|
|
|
7
7
|
body,
|
|
8
8
|
timeoutMs,
|
|
9
9
|
headers = {},
|
|
10
|
+
signal,
|
|
10
11
|
}) {
|
|
11
|
-
const response = await
|
|
12
|
+
const { response, text } = await requestText({
|
|
12
13
|
method,
|
|
13
14
|
url,
|
|
14
15
|
authToken,
|
|
15
16
|
body,
|
|
16
17
|
timeoutMs,
|
|
17
18
|
headers,
|
|
19
|
+
signal,
|
|
18
20
|
});
|
|
19
|
-
const text = await response.text();
|
|
20
21
|
const data = parseJson(text);
|
|
21
22
|
if (!response.ok) {
|
|
22
23
|
const error = new Error(
|
|
@@ -32,9 +33,13 @@ export async function requestJson({
|
|
|
32
33
|
return data || {};
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
export async function probeUrl({ url, authToken, timeoutMs }) {
|
|
36
|
-
const response = await
|
|
37
|
-
|
|
36
|
+
export async function probeUrl({ url, authToken, timeoutMs, signal }) {
|
|
37
|
+
const { response, text } = await requestText({
|
|
38
|
+
url,
|
|
39
|
+
authToken,
|
|
40
|
+
timeoutMs,
|
|
41
|
+
signal,
|
|
42
|
+
});
|
|
38
43
|
return {
|
|
39
44
|
ok: response.ok && text.trim().length > 0,
|
|
40
45
|
status: response.status,
|
|
@@ -42,18 +47,30 @@ export async function probeUrl({ url, authToken, timeoutMs }) {
|
|
|
42
47
|
};
|
|
43
48
|
}
|
|
44
49
|
|
|
45
|
-
export async function
|
|
50
|
+
export async function requestText({
|
|
46
51
|
method = "GET",
|
|
47
52
|
url,
|
|
48
53
|
authToken,
|
|
49
54
|
body,
|
|
50
55
|
timeoutMs,
|
|
51
56
|
headers = {},
|
|
57
|
+
signal,
|
|
52
58
|
}) {
|
|
53
59
|
const controller = new AbortController();
|
|
54
|
-
const
|
|
60
|
+
const parentAbort = () => controller.abort(signal?.reason);
|
|
61
|
+
if (signal?.aborted) parentAbort();
|
|
62
|
+
else signal?.addEventListener("abort", parentAbort, { once: true });
|
|
63
|
+
const effectiveTimeoutMs = Math.max(Number(timeoutMs) || 0, 1);
|
|
64
|
+
const timeoutError = new Error(
|
|
65
|
+
`HTTP request timed out after ${effectiveTimeoutMs}ms.`,
|
|
66
|
+
);
|
|
67
|
+
timeoutError.code = "lumine_http_timeout";
|
|
68
|
+
const timeout = setTimeout(
|
|
69
|
+
() => controller.abort(timeoutError),
|
|
70
|
+
effectiveTimeoutMs,
|
|
71
|
+
);
|
|
55
72
|
try {
|
|
56
|
-
|
|
73
|
+
const response = await fetch(url, {
|
|
57
74
|
method,
|
|
58
75
|
headers: {
|
|
59
76
|
...(authToken ? { authorization: authorizationHeader(authToken) } : {}),
|
|
@@ -63,8 +80,11 @@ export async function request({
|
|
|
63
80
|
body: body ? JSON.stringify(body) : undefined,
|
|
64
81
|
signal: controller.signal,
|
|
65
82
|
});
|
|
83
|
+
const text = await response.text();
|
|
84
|
+
return { response, text };
|
|
66
85
|
} finally {
|
|
67
86
|
clearTimeout(timeout);
|
|
87
|
+
signal?.removeEventListener("abort", parentAbort);
|
|
68
88
|
}
|
|
69
89
|
}
|
|
70
90
|
|
package/lib/sdk.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from "path";
|
|
|
2
2
|
|
|
3
3
|
import { ensureAuth, assertAuthScope } from "./auth.js";
|
|
4
4
|
import { mintBuildApiToken } from "./api.js";
|
|
5
|
-
import {
|
|
5
|
+
import { requestText } from "./http.js";
|
|
6
6
|
import { parseJson, resolveBuildId } from "./util.js";
|
|
7
7
|
import { findLocalProjectMetadata } from "./workspace.js";
|
|
8
8
|
|
|
@@ -495,7 +495,7 @@ export async function executeSdkHttpCall({
|
|
|
495
495
|
buildApiToken,
|
|
496
496
|
}) {
|
|
497
497
|
const startedAt = Date.now();
|
|
498
|
-
const response = await
|
|
498
|
+
const { response, text } = await requestText({
|
|
499
499
|
method,
|
|
500
500
|
url,
|
|
501
501
|
authToken,
|
|
@@ -503,7 +503,6 @@ export async function executeSdkHttpCall({
|
|
|
503
503
|
timeoutMs: options.timeoutMs,
|
|
504
504
|
headers: buildApiToken ? { "x-build-api-token": buildApiToken } : {},
|
|
505
505
|
});
|
|
506
|
-
const text = await response.text();
|
|
507
506
|
return {
|
|
508
507
|
ok: response.ok,
|
|
509
508
|
status: response.status,
|