@gamerdexapp/gamerdex-sync 0.1.0 → 0.2.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/CHANGELOG.md +24 -0
- package/dist/index.d.mts +55 -2
- package/dist/index.d.ts +55 -2
- package/dist/index.js +346 -27
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +343 -27
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -285,6 +285,16 @@ var decodeBase64 = (base64) => {
|
|
|
285
285
|
return new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));
|
|
286
286
|
};
|
|
287
287
|
|
|
288
|
+
// src/provider/detail.ts
|
|
289
|
+
var MAX_DETAIL_LENGTH = 300;
|
|
290
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
291
|
+
var detailOf = async (response) => {
|
|
292
|
+
const body = await response.json().catch(() => null);
|
|
293
|
+
if (!isRecord2(body)) return void 0;
|
|
294
|
+
const message = isRecord2(body.error) ? body.error.message : body.message;
|
|
295
|
+
return typeof message === "string" ? message.slice(0, MAX_DETAIL_LENGTH) : void 0;
|
|
296
|
+
};
|
|
297
|
+
|
|
288
298
|
// src/provider/types.ts
|
|
289
299
|
var VersionConflictError = class extends Error {
|
|
290
300
|
constructor(path) {
|
|
@@ -295,23 +305,25 @@ var VersionConflictError = class extends Error {
|
|
|
295
305
|
path;
|
|
296
306
|
};
|
|
297
307
|
var ProviderError = class extends Error {
|
|
298
|
-
constructor(kind, message, status, retryAfterSeconds) {
|
|
308
|
+
constructor(kind, message, status, retryAfterSeconds, detail) {
|
|
299
309
|
super(message);
|
|
300
310
|
this.kind = kind;
|
|
301
311
|
this.status = status;
|
|
302
312
|
this.retryAfterSeconds = retryAfterSeconds;
|
|
313
|
+
this.detail = detail;
|
|
303
314
|
this.name = "ProviderError";
|
|
304
315
|
}
|
|
305
316
|
kind;
|
|
306
317
|
status;
|
|
307
318
|
retryAfterSeconds;
|
|
319
|
+
detail;
|
|
308
320
|
};
|
|
309
321
|
|
|
310
322
|
// src/provider/github.ts
|
|
311
323
|
var API = "https://api.github.com";
|
|
312
324
|
var API_VERSION = "2022-11-28";
|
|
313
325
|
var encodePath = (path) => path.split("/").map(encodeURIComponent).join("/");
|
|
314
|
-
var
|
|
326
|
+
var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
315
327
|
var retryAfterOf = (response) => {
|
|
316
328
|
const header = response.headers.get("retry-after");
|
|
317
329
|
if (header !== null && Number.isFinite(Number(header))) return Number(header);
|
|
@@ -377,10 +389,10 @@ var GitHubProvider = class {
|
|
|
377
389
|
const response = await this.request("GET", `${API}/repos/${this.options.repository}`);
|
|
378
390
|
await this.assertOk(response);
|
|
379
391
|
const body = await response.json();
|
|
380
|
-
if (!
|
|
392
|
+
if (!isRecord3(body) || typeof body.private !== "boolean") {
|
|
381
393
|
throw this.invalid("The repository answer is not understood.");
|
|
382
394
|
}
|
|
383
|
-
const permissions =
|
|
395
|
+
const permissions = isRecord3(body.permissions) ? body.permissions : {};
|
|
384
396
|
return {
|
|
385
397
|
private: body.private,
|
|
386
398
|
canWrite: permissions.push === true,
|
|
@@ -415,7 +427,10 @@ var GitHubProvider = class {
|
|
|
415
427
|
async assertOk(response) {
|
|
416
428
|
if (response.ok) return;
|
|
417
429
|
const { status } = response;
|
|
418
|
-
|
|
430
|
+
const detail = await detailOf(response);
|
|
431
|
+
if (status === 401) {
|
|
432
|
+
throw new ProviderError("auth", "The token is not valid.", status, void 0, detail);
|
|
433
|
+
}
|
|
419
434
|
if (status === 403 || status === 429) {
|
|
420
435
|
const limited = status === 429 || response.headers.get("x-ratelimit-remaining") === "0";
|
|
421
436
|
if (limited) {
|
|
@@ -423,18 +438,34 @@ var GitHubProvider = class {
|
|
|
423
438
|
"rate-limit",
|
|
424
439
|
"GitHub limits the calls.",
|
|
425
440
|
status,
|
|
426
|
-
retryAfterOf(response)
|
|
441
|
+
retryAfterOf(response),
|
|
442
|
+
detail
|
|
427
443
|
);
|
|
428
444
|
}
|
|
429
|
-
throw new ProviderError(
|
|
445
|
+
throw new ProviderError(
|
|
446
|
+
"auth",
|
|
447
|
+
"The token has no permission for the repository.",
|
|
448
|
+
status,
|
|
449
|
+
void 0,
|
|
450
|
+
detail
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
if (status === 404) {
|
|
454
|
+
throw new ProviderError(
|
|
455
|
+
"not-found",
|
|
456
|
+
"The repository was not found.",
|
|
457
|
+
status,
|
|
458
|
+
void 0,
|
|
459
|
+
detail
|
|
460
|
+
);
|
|
430
461
|
}
|
|
431
|
-
if (status
|
|
432
|
-
throw new ProviderError("
|
|
433
|
-
|
|
434
|
-
throw this.invalid(`GitHub answered ${status}.`, status);
|
|
462
|
+
if (status >= 500) {
|
|
463
|
+
throw new ProviderError("server", "GitHub failed.", status, void 0, detail);
|
|
464
|
+
}
|
|
465
|
+
throw this.invalid(`GitHub answered ${status}.`, status, detail);
|
|
435
466
|
}
|
|
436
|
-
invalid(message, status) {
|
|
437
|
-
return new ProviderError("invalid", message, status);
|
|
467
|
+
invalid(message, status, detail) {
|
|
468
|
+
return new ProviderError("invalid", message, status, void 0, detail);
|
|
438
469
|
}
|
|
439
470
|
};
|
|
440
471
|
|
|
@@ -497,11 +528,14 @@ var readMeta = (text, appId) => {
|
|
|
497
528
|
if (!parsed.ok) throw new RemoteDataError(parsed.error);
|
|
498
529
|
return parsed.meta;
|
|
499
530
|
};
|
|
500
|
-
var writeMeta = async (options) => {
|
|
531
|
+
var writeMeta = async (options, uploaded) => {
|
|
501
532
|
const { provider, appId, deviceId, deviceName, updateMeta, now } = options;
|
|
502
533
|
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
|
|
503
534
|
const current = await provider.read(metaPath(appId));
|
|
504
535
|
const meta = readMeta(current?.content, appId);
|
|
536
|
+
if (current && !uploaded && !updateMeta && Object.hasOwn(meta.devices, deviceId)) {
|
|
537
|
+
return current.version;
|
|
538
|
+
}
|
|
505
539
|
const time = now();
|
|
506
540
|
const updated = withDevice(updateMeta ? updateMeta(meta) : meta, deviceId, {
|
|
507
541
|
name: deviceName,
|
|
@@ -531,7 +565,7 @@ var sync = async (options) => {
|
|
|
531
565
|
const cloud = restoreCloudValues(local, void 0, schema);
|
|
532
566
|
const created = await tryWrite(provider, path, cloud, null);
|
|
533
567
|
if (created === null) continue;
|
|
534
|
-
const metaVersion2 = await writeMeta(options);
|
|
568
|
+
const metaVersion2 = await writeMeta(options, true);
|
|
535
569
|
return done(local, cloud, created, metaVersion2, [], true);
|
|
536
570
|
}
|
|
537
571
|
const remote = parseData(file.content, validate);
|
|
@@ -544,12 +578,12 @@ var sync = async (options) => {
|
|
|
544
578
|
throw new RemoteDataError("The merged data is not valid.");
|
|
545
579
|
}
|
|
546
580
|
if (equal(merged.cloud, remote)) {
|
|
547
|
-
const metaVersion2 = await writeMeta(options);
|
|
581
|
+
const metaVersion2 = await writeMeta(options, false);
|
|
548
582
|
return done(merged.merged, merged.cloud, file.version, metaVersion2, merged.conflicts, false);
|
|
549
583
|
}
|
|
550
584
|
const version = await tryWrite(provider, path, merged.cloud, file.version);
|
|
551
585
|
if (version === null) continue;
|
|
552
|
-
const metaVersion = await writeMeta(options);
|
|
586
|
+
const metaVersion = await writeMeta(options, true);
|
|
553
587
|
return done(merged.merged, merged.cloud, version, metaVersion, merged.conflicts, true);
|
|
554
588
|
}
|
|
555
589
|
throw new SyncBusyError();
|
|
@@ -588,10 +622,10 @@ var GITHUB_APP = {
|
|
|
588
622
|
};
|
|
589
623
|
|
|
590
624
|
// src/auth/tokens.ts
|
|
591
|
-
var
|
|
625
|
+
var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
592
626
|
var isSeconds = (value) => typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
593
627
|
var parseTokenResponse = (body) => {
|
|
594
|
-
if (!
|
|
628
|
+
if (!isRecord4(body)) return null;
|
|
595
629
|
const { accessToken, expiresIn, refreshToken, refreshExpiresIn } = body;
|
|
596
630
|
if (typeof accessToken !== "string" || accessToken === "") return null;
|
|
597
631
|
if (typeof refreshToken !== "string" || refreshToken === "") return null;
|
|
@@ -681,7 +715,7 @@ var readCallback = (search, expectedState) => {
|
|
|
681
715
|
// src/provider/repositories.ts
|
|
682
716
|
var API2 = "https://api.github.com";
|
|
683
717
|
var PAGE_SIZE = 100;
|
|
684
|
-
var
|
|
718
|
+
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
685
719
|
var getJson = async (send, token, url) => {
|
|
686
720
|
let response;
|
|
687
721
|
try {
|
|
@@ -695,13 +729,26 @@ var getJson = async (send, token, url) => {
|
|
|
695
729
|
} catch {
|
|
696
730
|
throw new ProviderError("network", "GitHub could not be reached.");
|
|
697
731
|
}
|
|
698
|
-
if (response.
|
|
699
|
-
|
|
732
|
+
if (!response.ok) {
|
|
733
|
+
const { status } = response;
|
|
734
|
+
const detail = await detailOf(response);
|
|
735
|
+
if (status === 401 || status === 403) {
|
|
736
|
+
throw new ProviderError("auth", "The token is not valid.", status, void 0, detail);
|
|
737
|
+
}
|
|
738
|
+
if (status >= 500)
|
|
739
|
+
throw new ProviderError("server", "GitHub failed.", status, void 0, detail);
|
|
740
|
+
throw new ProviderError(
|
|
741
|
+
"invalid",
|
|
742
|
+
"GitHub answer is not understood.",
|
|
743
|
+
status,
|
|
744
|
+
void 0,
|
|
745
|
+
detail
|
|
746
|
+
);
|
|
700
747
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
if (!isRecord4(body))
|
|
748
|
+
const body = await response.json().catch(() => null);
|
|
749
|
+
if (!isRecord5(body)) {
|
|
704
750
|
throw new ProviderError("invalid", "GitHub answer is not understood.", response.status);
|
|
751
|
+
}
|
|
705
752
|
return body;
|
|
706
753
|
};
|
|
707
754
|
var listInstalledRepositories = async (token, send = (...args) => globalThis.fetch(...args)) => {
|
|
@@ -710,7 +757,7 @@ var listInstalledRepositories = async (token, send = (...args) => globalThis.fet
|
|
|
710
757
|
token,
|
|
711
758
|
`${API2}/user/installations?per_page=${PAGE_SIZE}`
|
|
712
759
|
);
|
|
713
|
-
const ids = (Array.isArray(installations.installations) ? installations.installations : []).filter(
|
|
760
|
+
const ids = (Array.isArray(installations.installations) ? installations.installations : []).filter(isRecord5).map((installation) => installation.id).filter((id) => typeof id === "number");
|
|
714
761
|
const repositories = [];
|
|
715
762
|
for (const id of ids) {
|
|
716
763
|
const page = await getJson(
|
|
@@ -719,17 +766,285 @@ var listInstalledRepositories = async (token, send = (...args) => globalThis.fet
|
|
|
719
766
|
`${API2}/user/installations/${id}/repositories?per_page=${PAGE_SIZE}`
|
|
720
767
|
);
|
|
721
768
|
for (const item of Array.isArray(page.repositories) ? page.repositories : []) {
|
|
722
|
-
if (
|
|
769
|
+
if (isRecord5(item) && typeof item.full_name === "string" && typeof item.private === "boolean") {
|
|
723
770
|
repositories.push({ fullName: item.full_name, private: item.private });
|
|
724
771
|
}
|
|
725
772
|
}
|
|
726
773
|
}
|
|
727
774
|
return repositories;
|
|
728
775
|
};
|
|
776
|
+
|
|
777
|
+
// src/provider/drive.ts
|
|
778
|
+
var API3 = "https://www.googleapis.com/drive/v3";
|
|
779
|
+
var UPLOAD = "https://www.googleapis.com/upload/drive/v3";
|
|
780
|
+
var FOLDER_TYPE = "application/vnd.google-apps.folder";
|
|
781
|
+
var RATE_LIMIT_REASONS = ["rateLimitExceeded", "userRateLimitExceeded", "dailyLimitExceeded"];
|
|
782
|
+
var isRecord6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
783
|
+
var quoted = (value) => `'${value.replace(/[\\']/g, "\\$&")}'`;
|
|
784
|
+
var oldestFirst = (a, b) => a.createdTime.localeCompare(b.createdTime) || a.id.localeCompare(b.id);
|
|
785
|
+
var reasonOf = (body) => {
|
|
786
|
+
if (!isRecord6(body) || !isRecord6(body.error) || !Array.isArray(body.error.errors))
|
|
787
|
+
return void 0;
|
|
788
|
+
const first = body.error.errors[0];
|
|
789
|
+
return isRecord6(first) && typeof first.reason === "string" ? first.reason : void 0;
|
|
790
|
+
};
|
|
791
|
+
var GoogleDriveProvider = class {
|
|
792
|
+
constructor(options) {
|
|
793
|
+
this.options = options;
|
|
794
|
+
this.send = options.fetch ?? ((...args) => globalThis.fetch(...args));
|
|
795
|
+
}
|
|
796
|
+
options;
|
|
797
|
+
send;
|
|
798
|
+
folders = /* @__PURE__ */ new Map();
|
|
799
|
+
async read(path) {
|
|
800
|
+
const file = await this.findFile(path);
|
|
801
|
+
if (!file) return null;
|
|
802
|
+
const head = await this.headOf(file.id);
|
|
803
|
+
if (head === null) return null;
|
|
804
|
+
return { content: await this.revisionContent(file.id, head), version: head };
|
|
805
|
+
}
|
|
806
|
+
async write(path, content, expectedVersion) {
|
|
807
|
+
if (expectedVersion === null) return this.create(path, content);
|
|
808
|
+
const file = await this.findFile(path);
|
|
809
|
+
if (!file || await this.headOf(file.id) !== expectedVersion) {
|
|
810
|
+
throw new VersionConflictError(path);
|
|
811
|
+
}
|
|
812
|
+
const version = await this.upload(file.id, content);
|
|
813
|
+
if (version === expectedVersion) return { version };
|
|
814
|
+
const previous = await this.revisionBefore(file.id, version);
|
|
815
|
+
if (previous === expectedVersion) return { version };
|
|
816
|
+
await this.upload(file.id, await this.revisionContent(file.id, previous));
|
|
817
|
+
throw new VersionConflictError(path);
|
|
818
|
+
}
|
|
819
|
+
async remove(path, expectedVersion) {
|
|
820
|
+
const file = await this.findFile(path);
|
|
821
|
+
if (!file) return;
|
|
822
|
+
if (await this.headOf(file.id) !== expectedVersion) throw new VersionConflictError(path);
|
|
823
|
+
await this.deleteItem(file.id);
|
|
824
|
+
}
|
|
825
|
+
async create(path, content) {
|
|
826
|
+
const { parentId, name } = await this.locate(path, true);
|
|
827
|
+
if ((await this.children(parentId, name)).length > 0) throw new VersionConflictError(path);
|
|
828
|
+
const created = await this.createFile(parentId, name, content);
|
|
829
|
+
const [oldest] = await this.children(parentId, name);
|
|
830
|
+
if (oldest.id !== created.id) {
|
|
831
|
+
await this.deleteItem(created.id);
|
|
832
|
+
throw new VersionConflictError(path);
|
|
833
|
+
}
|
|
834
|
+
return { version: created.version };
|
|
835
|
+
}
|
|
836
|
+
async findFile(path) {
|
|
837
|
+
const located = await this.locate(path, false);
|
|
838
|
+
if (!located) return null;
|
|
839
|
+
return (await this.children(located.parentId, located.name))[0] ?? null;
|
|
840
|
+
}
|
|
841
|
+
async locate(path, create) {
|
|
842
|
+
const parts = path.split("/");
|
|
843
|
+
const name = parts.pop();
|
|
844
|
+
let parentId = "root";
|
|
845
|
+
let walked = "";
|
|
846
|
+
for (const folder of parts) {
|
|
847
|
+
walked += `/${folder}`;
|
|
848
|
+
const known = this.folders.get(walked);
|
|
849
|
+
if (known) {
|
|
850
|
+
parentId = known;
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
const id = await this.folderId(parentId, folder, create);
|
|
854
|
+
if (id === null) return null;
|
|
855
|
+
this.folders.set(walked, id);
|
|
856
|
+
parentId = id;
|
|
857
|
+
}
|
|
858
|
+
return { parentId, name };
|
|
859
|
+
}
|
|
860
|
+
async folderId(parentId, name, create) {
|
|
861
|
+
const found = await this.children(parentId, name, true);
|
|
862
|
+
if (found.length > 0) return found[0].id;
|
|
863
|
+
if (!create) return null;
|
|
864
|
+
const response = await this.request("POST", `${API3}/files?fields=id`, {
|
|
865
|
+
body: JSON.stringify({ name, mimeType: FOLDER_TYPE, parents: [parentId] }),
|
|
866
|
+
headers: { "Content-Type": "application/json" }
|
|
867
|
+
});
|
|
868
|
+
const id = this.stringField(await response.json(), "id");
|
|
869
|
+
const [oldest] = await this.children(parentId, name, true);
|
|
870
|
+
if (oldest.id !== id) {
|
|
871
|
+
await this.deleteItem(id);
|
|
872
|
+
return oldest.id;
|
|
873
|
+
}
|
|
874
|
+
return id;
|
|
875
|
+
}
|
|
876
|
+
async children(parentId, name, folder = false) {
|
|
877
|
+
const query = [
|
|
878
|
+
`name = ${quoted(name)}`,
|
|
879
|
+
`${quoted(parentId)} in parents`,
|
|
880
|
+
"trashed = false",
|
|
881
|
+
`mimeType ${folder ? "=" : "!="} '${FOLDER_TYPE}'`
|
|
882
|
+
].join(" and ");
|
|
883
|
+
const url = `${API3}/files?${new URLSearchParams({
|
|
884
|
+
q: query,
|
|
885
|
+
fields: "files(id,createdTime)",
|
|
886
|
+
pageSize: "100"
|
|
887
|
+
})}`;
|
|
888
|
+
const body = await (await this.request("GET", url)).json();
|
|
889
|
+
const files = isRecord6(body) && Array.isArray(body.files) ? body.files : [];
|
|
890
|
+
return files.filter(
|
|
891
|
+
(item) => isRecord6(item) && typeof item.id === "string" && typeof item.createdTime === "string"
|
|
892
|
+
).sort(oldestFirst);
|
|
893
|
+
}
|
|
894
|
+
async headOf(id) {
|
|
895
|
+
const response = await this.request(
|
|
896
|
+
"GET",
|
|
897
|
+
`${API3}/files/${encodeURIComponent(id)}?fields=headRevisionId,trashed`,
|
|
898
|
+
{ allowMissing: true }
|
|
899
|
+
);
|
|
900
|
+
if (response.status === 404) return null;
|
|
901
|
+
const body = await response.json();
|
|
902
|
+
if (isRecord6(body) && body.trashed === true) return null;
|
|
903
|
+
return this.stringField(body, "headRevisionId");
|
|
904
|
+
}
|
|
905
|
+
async revisionContent(id, revision) {
|
|
906
|
+
const url = `${API3}/files/${encodeURIComponent(id)}/revisions/${encodeURIComponent(revision)}?alt=media`;
|
|
907
|
+
return (await this.request("GET", url)).text();
|
|
908
|
+
}
|
|
909
|
+
async revisionBefore(id, revision) {
|
|
910
|
+
const url = `${API3}/files/${encodeURIComponent(id)}/revisions?${new URLSearchParams({
|
|
911
|
+
fields: "revisions(id)",
|
|
912
|
+
pageSize: "1000"
|
|
913
|
+
})}`;
|
|
914
|
+
const body = await (await this.request("GET", url)).json();
|
|
915
|
+
const revisions = (isRecord6(body) && Array.isArray(body.revisions) ? body.revisions : []).filter(isRecord6).map((item) => item.id);
|
|
916
|
+
const index = revisions.indexOf(revision);
|
|
917
|
+
const previous = revisions[index - 1];
|
|
918
|
+
if (index < 1 || typeof previous !== "string") {
|
|
919
|
+
throw new ProviderError("server", "The new revision is not in the revision list.");
|
|
920
|
+
}
|
|
921
|
+
return previous;
|
|
922
|
+
}
|
|
923
|
+
async upload(id, content) {
|
|
924
|
+
const url = `${UPLOAD}/files/${encodeURIComponent(id)}?uploadType=media&fields=headRevisionId`;
|
|
925
|
+
const response = await this.request("PATCH", url, {
|
|
926
|
+
body: content,
|
|
927
|
+
headers: { "Content-Type": "application/json" }
|
|
928
|
+
});
|
|
929
|
+
return this.stringField(await response.json(), "headRevisionId");
|
|
930
|
+
}
|
|
931
|
+
async createFile(parentId, name, content) {
|
|
932
|
+
const boundary = `gamerdex-${crypto.randomUUID()}`;
|
|
933
|
+
const metadata = JSON.stringify({ name, parents: [parentId], mimeType: "application/json" });
|
|
934
|
+
const body = `--${boundary}\r
|
|
935
|
+
Content-Type: application/json; charset=UTF-8\r
|
|
936
|
+
\r
|
|
937
|
+
${metadata}\r
|
|
938
|
+
--${boundary}\r
|
|
939
|
+
Content-Type: application/json\r
|
|
940
|
+
\r
|
|
941
|
+
${content}\r
|
|
942
|
+
--${boundary}--`;
|
|
943
|
+
const response = await this.request(
|
|
944
|
+
"POST",
|
|
945
|
+
`${UPLOAD}/files?uploadType=multipart&fields=id,headRevisionId`,
|
|
946
|
+
{ body, headers: { "Content-Type": `multipart/related; boundary=${boundary}` } }
|
|
947
|
+
);
|
|
948
|
+
const answer = await response.json();
|
|
949
|
+
return {
|
|
950
|
+
id: this.stringField(answer, "id"),
|
|
951
|
+
version: this.stringField(answer, "headRevisionId")
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
async deleteItem(id) {
|
|
955
|
+
await this.request("DELETE", `${API3}/files/${encodeURIComponent(id)}`, { allowMissing: true });
|
|
956
|
+
}
|
|
957
|
+
stringField(body, field) {
|
|
958
|
+
const value = isRecord6(body) ? body[field] : void 0;
|
|
959
|
+
if (typeof value !== "string") {
|
|
960
|
+
throw new ProviderError("invalid", `Google Drive answered without ${field}.`);
|
|
961
|
+
}
|
|
962
|
+
return value;
|
|
963
|
+
}
|
|
964
|
+
async request(method, url, options = {}) {
|
|
965
|
+
const token = await this.options.getToken();
|
|
966
|
+
let response;
|
|
967
|
+
try {
|
|
968
|
+
response = await this.send(url, {
|
|
969
|
+
method,
|
|
970
|
+
headers: { Authorization: `Bearer ${token}`, ...options.headers ?? {} },
|
|
971
|
+
body: options.body
|
|
972
|
+
});
|
|
973
|
+
} catch {
|
|
974
|
+
throw new ProviderError("network", "Google Drive could not be reached.");
|
|
975
|
+
}
|
|
976
|
+
if (response.ok || options.allowMissing && response.status === 404) return response;
|
|
977
|
+
throw await this.failure(response);
|
|
978
|
+
}
|
|
979
|
+
async failure(response) {
|
|
980
|
+
const { status } = response;
|
|
981
|
+
const body = await response.clone().json().catch(() => null);
|
|
982
|
+
const detail = await detailOf(response);
|
|
983
|
+
if (status === 401) {
|
|
984
|
+
return new ProviderError("auth", "The token is not valid.", status, void 0, detail);
|
|
985
|
+
}
|
|
986
|
+
const reason = reasonOf(body);
|
|
987
|
+
if (status === 429 || status === 403 && reason && RATE_LIMIT_REASONS.includes(reason)) {
|
|
988
|
+
const retryAfter = Number(response.headers.get("retry-after"));
|
|
989
|
+
return new ProviderError(
|
|
990
|
+
"rate-limit",
|
|
991
|
+
"Google Drive limits the calls.",
|
|
992
|
+
status,
|
|
993
|
+
Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : void 0,
|
|
994
|
+
detail
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
if (status === 403) {
|
|
998
|
+
return new ProviderError(
|
|
999
|
+
"auth",
|
|
1000
|
+
"The token has no permission for Drive.",
|
|
1001
|
+
status,
|
|
1002
|
+
void 0,
|
|
1003
|
+
detail
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
if (status === 404) {
|
|
1007
|
+
return new ProviderError(
|
|
1008
|
+
"not-found",
|
|
1009
|
+
"The file or folder was not found.",
|
|
1010
|
+
status,
|
|
1011
|
+
void 0,
|
|
1012
|
+
detail
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
if (status >= 500) {
|
|
1016
|
+
return new ProviderError("server", "Google Drive failed.", status, void 0, detail);
|
|
1017
|
+
}
|
|
1018
|
+
return new ProviderError(
|
|
1019
|
+
"invalid",
|
|
1020
|
+
`Google Drive answered ${status}.`,
|
|
1021
|
+
status,
|
|
1022
|
+
void 0,
|
|
1023
|
+
detail
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
// src/auth/google.ts
|
|
1029
|
+
var GOOGLE_DRIVE_SCOPE = "https://www.googleapis.com/auth/drive.file";
|
|
1030
|
+
var googleAuthorizeUrl = (clientId, redirectUri, state) => {
|
|
1031
|
+
const query = new URLSearchParams({
|
|
1032
|
+
client_id: clientId,
|
|
1033
|
+
redirect_uri: redirectUri,
|
|
1034
|
+
response_type: "code",
|
|
1035
|
+
scope: GOOGLE_DRIVE_SCOPE,
|
|
1036
|
+
access_type: "offline",
|
|
1037
|
+
prompt: "consent",
|
|
1038
|
+
state
|
|
1039
|
+
});
|
|
1040
|
+
return `https://accounts.google.com/o/oauth2/v2/auth?${query}`;
|
|
1041
|
+
};
|
|
729
1042
|
export {
|
|
730
1043
|
DATA_FILE,
|
|
731
1044
|
GITHUB_APP,
|
|
1045
|
+
GOOGLE_DRIVE_SCOPE,
|
|
732
1046
|
GitHubProvider,
|
|
1047
|
+
GoogleDriveProvider,
|
|
733
1048
|
META_FILE,
|
|
734
1049
|
META_FORMAT,
|
|
735
1050
|
MemoryProvider,
|
|
@@ -745,6 +1060,7 @@ export {
|
|
|
745
1060
|
emptyMeta,
|
|
746
1061
|
encodeBase64,
|
|
747
1062
|
equal,
|
|
1063
|
+
googleAuthorizeUrl,
|
|
748
1064
|
installUrl,
|
|
749
1065
|
isAppId,
|
|
750
1066
|
isObject,
|