@gamerdexapp/gamerdex-sync 0.2.1 → 0.3.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 +12 -0
- package/README.md +1 -1
- package/dist/index.d.mts +81 -1
- package/dist/index.d.ts +81 -1
- package/dist/index.js +345 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +338 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-10-18
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `DropboxProvider` keeps the files in the app folder of the Dropbox of the player. It writes with
|
|
15
|
+
the revision that was read and fails with a version conflict when the file changed since.
|
|
16
|
+
- `OneDriveProvider` keeps the files in the app folder of the OneDrive of a personal Microsoft
|
|
17
|
+
account. It writes with the `eTag` that was read and creates a new file only when none exists.
|
|
18
|
+
- `dropboxAuthorizeUrl()` with `DROPBOX_SCOPES`, and `microsoftAuthorizeUrl()` with
|
|
19
|
+
`ONEDRIVE_FILE_SCOPE` and `ONEDRIVE_SCOPES`, for the two sign in pages.
|
|
20
|
+
- `ProviderError.detail` also reads the summary of a failed Dropbox request.
|
|
21
|
+
|
|
10
22
|
## [0.2.1] - 2026-10-18
|
|
11
23
|
|
|
12
24
|
### Fixed
|
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ const result = await sync({
|
|
|
28
28
|
appId: "my-app",
|
|
29
29
|
deviceId: "device-1",
|
|
30
30
|
deviceName: "Laptop",
|
|
31
|
-
provider: new MemoryProvider(), // GitHubProvider in an app
|
|
31
|
+
provider: new MemoryProvider(), // GitHubProvider, GoogleDriveProvider, DropboxProvider or OneDriveProvider in an app
|
|
32
32
|
schema,
|
|
33
33
|
local: { chief: { level: 5 } },
|
|
34
34
|
state: null, // the state that the last sync returned, or null on the first sync
|
package/dist/index.d.mts
CHANGED
|
@@ -443,4 +443,84 @@ declare const GOOGLE_DRIVE_SCOPE = "https://www.googleapis.com/auth/drive.file";
|
|
|
443
443
|
*/
|
|
444
444
|
declare const googleAuthorizeUrl: (clientId: string, redirectUri: string, state: string) => string;
|
|
445
445
|
|
|
446
|
-
|
|
446
|
+
interface DropboxOptions {
|
|
447
|
+
/** Returns the token. It may refresh the token first, so it is called for every request. */
|
|
448
|
+
getToken: () => Promise<string>;
|
|
449
|
+
fetch?: typeof fetch;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Keeps the files of an app in the Dropbox of the player. The app should have the App folder
|
|
453
|
+
* access type with the `files.content.read` and `files.content.write` scopes, so it sees only its
|
|
454
|
+
* own folder under `Apps`.
|
|
455
|
+
*
|
|
456
|
+
* Dropbox writes with an expected revision. An upload in `update` mode with `strict_conflict`
|
|
457
|
+
* fails when the file changed since, and an upload in `add` mode fails when the file exists.
|
|
458
|
+
*/
|
|
459
|
+
declare class DropboxProvider implements SyncProvider {
|
|
460
|
+
private readonly options;
|
|
461
|
+
private readonly send;
|
|
462
|
+
constructor(options: DropboxOptions);
|
|
463
|
+
read(path: string): Promise<RemoteFile | null>;
|
|
464
|
+
write(path: string, content: string, expectedVersion: string | null): Promise<{
|
|
465
|
+
version: string;
|
|
466
|
+
}>;
|
|
467
|
+
remove(path: string, expectedVersion: string): Promise<void>;
|
|
468
|
+
private request;
|
|
469
|
+
/**
|
|
470
|
+
* Reads a failed answer. Dropbox reports a failure that belongs to the call, such as a missing
|
|
471
|
+
* file or a conflict, with status 409. The caller decides what that means. Every other status
|
|
472
|
+
* is a failure of the request and is thrown here.
|
|
473
|
+
*/
|
|
474
|
+
private failure;
|
|
475
|
+
private unexpected;
|
|
476
|
+
private invalid;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
interface OneDriveOptions {
|
|
480
|
+
/** Returns the token. It may refresh the token first, so it is called for every request. */
|
|
481
|
+
getToken: () => Promise<string>;
|
|
482
|
+
fetch?: typeof fetch;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Keeps the files of an app in the OneDrive of the player, in the app folder under `Apps`. It
|
|
486
|
+
* needs only the `Files.ReadWrite.AppFolder` scope, so it sees no other file. The app folder
|
|
487
|
+
* exists for personal Microsoft accounts.
|
|
488
|
+
*
|
|
489
|
+
* A write sends the `eTag` that was read in an `If-Match` header, and a new file is created with
|
|
490
|
+
* `conflictBehavior=fail`. OneDrive answers 412 and 409 when the file changed or exists.
|
|
491
|
+
*/
|
|
492
|
+
declare class OneDriveProvider implements SyncProvider {
|
|
493
|
+
private readonly options;
|
|
494
|
+
private readonly send;
|
|
495
|
+
constructor(options: OneDriveOptions);
|
|
496
|
+
read(path: string): Promise<RemoteFile | null>;
|
|
497
|
+
write(path: string, content: string, expectedVersion: string | null): Promise<{
|
|
498
|
+
version: string;
|
|
499
|
+
}>;
|
|
500
|
+
remove(path: string, expectedVersion: string): Promise<void>;
|
|
501
|
+
private itemUrl;
|
|
502
|
+
private ifMatch;
|
|
503
|
+
private info;
|
|
504
|
+
/** The download address is signed, so it must not carry the token. */
|
|
505
|
+
private fetchContent;
|
|
506
|
+
private request;
|
|
507
|
+
private assertOk;
|
|
508
|
+
private invalid;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** The scopes that sync asks for. Together with the App folder access type, they reach one folder. */
|
|
512
|
+
declare const DROPBOX_SCOPES: readonly ["files.content.read", "files.content.write"];
|
|
513
|
+
/**
|
|
514
|
+
* The address of the Dropbox sign in page. `token_access_type=offline` makes Dropbox send a
|
|
515
|
+
* refresh token, which does not expire until the player removes the access.
|
|
516
|
+
*/
|
|
517
|
+
declare const dropboxAuthorizeUrl: (clientId: string, redirectUri: string, state: string) => string;
|
|
518
|
+
|
|
519
|
+
/** The file scope that sync asks for. It reaches only the app folder of the player. */
|
|
520
|
+
declare const ONEDRIVE_FILE_SCOPE = "Files.ReadWrite.AppFolder";
|
|
521
|
+
/** The file scope, and `offline_access`, which makes Microsoft send a refresh token. */
|
|
522
|
+
declare const ONEDRIVE_SCOPES: readonly ["Files.ReadWrite.AppFolder", "offline_access"];
|
|
523
|
+
/** The address of the Microsoft sign in page for personal accounts. */
|
|
524
|
+
declare const microsoftAuthorizeUrl: (clientId: string, redirectUri: string, state: string) => string;
|
|
525
|
+
|
|
526
|
+
export { type CallbackResult, type Conflict, type ConflictKind, DATA_FILE, DROPBOX_SCOPES, type DeviceRecord, type DropboxOptions, DropboxProvider, type FirstSyncChoice, GITHUB_APP, GOOGLE_DRIVE_SCOPE, type GitHubOptions, GitHubProvider, type GoogleDriveOptions, GoogleDriveProvider, type InstalledRepository, META_FILE, META_FORMAT, MemoryProvider, type MergeInput, type MergeResult, ONEDRIVE_FILE_SCOPE, ONEDRIVE_SCOPES, type OneDriveOptions, OneDriveProvider, type ParsedMeta, type Path, type PathStep, ProviderError, type ProviderErrorKind, ROOT_FOLDER, RemoteDataError, type RemoteFile, type RepositoryCheck, type Schema, type Side, type StoredTokens, SyncBusyError, type SyncMeta, type SyncOptions, type SyncProvider, type SyncResult, type SyncState, type TokenResponse, type TokenSourceOptions, VersionConflictError, authorizeUrl, createTokenSource, dataPath, decodeBase64, dropboxAuthorizeUrl, emptyMeta, encodeBase64, equal, googleAuthorizeUrl, installUrl, isAppId, isObject, listInstalledRepositories, merge, metaPath, microsoftAuthorizeUrl, newState, parseMeta, parseTokenResponse, readCallback, resolveConflict, restoreCloudValues, sync, toStoredTokens, withChiefRemoved, withChiefRevision, withDevice, withSettingsRevision };
|
package/dist/index.d.ts
CHANGED
|
@@ -443,4 +443,84 @@ declare const GOOGLE_DRIVE_SCOPE = "https://www.googleapis.com/auth/drive.file";
|
|
|
443
443
|
*/
|
|
444
444
|
declare const googleAuthorizeUrl: (clientId: string, redirectUri: string, state: string) => string;
|
|
445
445
|
|
|
446
|
-
|
|
446
|
+
interface DropboxOptions {
|
|
447
|
+
/** Returns the token. It may refresh the token first, so it is called for every request. */
|
|
448
|
+
getToken: () => Promise<string>;
|
|
449
|
+
fetch?: typeof fetch;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Keeps the files of an app in the Dropbox of the player. The app should have the App folder
|
|
453
|
+
* access type with the `files.content.read` and `files.content.write` scopes, so it sees only its
|
|
454
|
+
* own folder under `Apps`.
|
|
455
|
+
*
|
|
456
|
+
* Dropbox writes with an expected revision. An upload in `update` mode with `strict_conflict`
|
|
457
|
+
* fails when the file changed since, and an upload in `add` mode fails when the file exists.
|
|
458
|
+
*/
|
|
459
|
+
declare class DropboxProvider implements SyncProvider {
|
|
460
|
+
private readonly options;
|
|
461
|
+
private readonly send;
|
|
462
|
+
constructor(options: DropboxOptions);
|
|
463
|
+
read(path: string): Promise<RemoteFile | null>;
|
|
464
|
+
write(path: string, content: string, expectedVersion: string | null): Promise<{
|
|
465
|
+
version: string;
|
|
466
|
+
}>;
|
|
467
|
+
remove(path: string, expectedVersion: string): Promise<void>;
|
|
468
|
+
private request;
|
|
469
|
+
/**
|
|
470
|
+
* Reads a failed answer. Dropbox reports a failure that belongs to the call, such as a missing
|
|
471
|
+
* file or a conflict, with status 409. The caller decides what that means. Every other status
|
|
472
|
+
* is a failure of the request and is thrown here.
|
|
473
|
+
*/
|
|
474
|
+
private failure;
|
|
475
|
+
private unexpected;
|
|
476
|
+
private invalid;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
interface OneDriveOptions {
|
|
480
|
+
/** Returns the token. It may refresh the token first, so it is called for every request. */
|
|
481
|
+
getToken: () => Promise<string>;
|
|
482
|
+
fetch?: typeof fetch;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Keeps the files of an app in the OneDrive of the player, in the app folder under `Apps`. It
|
|
486
|
+
* needs only the `Files.ReadWrite.AppFolder` scope, so it sees no other file. The app folder
|
|
487
|
+
* exists for personal Microsoft accounts.
|
|
488
|
+
*
|
|
489
|
+
* A write sends the `eTag` that was read in an `If-Match` header, and a new file is created with
|
|
490
|
+
* `conflictBehavior=fail`. OneDrive answers 412 and 409 when the file changed or exists.
|
|
491
|
+
*/
|
|
492
|
+
declare class OneDriveProvider implements SyncProvider {
|
|
493
|
+
private readonly options;
|
|
494
|
+
private readonly send;
|
|
495
|
+
constructor(options: OneDriveOptions);
|
|
496
|
+
read(path: string): Promise<RemoteFile | null>;
|
|
497
|
+
write(path: string, content: string, expectedVersion: string | null): Promise<{
|
|
498
|
+
version: string;
|
|
499
|
+
}>;
|
|
500
|
+
remove(path: string, expectedVersion: string): Promise<void>;
|
|
501
|
+
private itemUrl;
|
|
502
|
+
private ifMatch;
|
|
503
|
+
private info;
|
|
504
|
+
/** The download address is signed, so it must not carry the token. */
|
|
505
|
+
private fetchContent;
|
|
506
|
+
private request;
|
|
507
|
+
private assertOk;
|
|
508
|
+
private invalid;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** The scopes that sync asks for. Together with the App folder access type, they reach one folder. */
|
|
512
|
+
declare const DROPBOX_SCOPES: readonly ["files.content.read", "files.content.write"];
|
|
513
|
+
/**
|
|
514
|
+
* The address of the Dropbox sign in page. `token_access_type=offline` makes Dropbox send a
|
|
515
|
+
* refresh token, which does not expire until the player removes the access.
|
|
516
|
+
*/
|
|
517
|
+
declare const dropboxAuthorizeUrl: (clientId: string, redirectUri: string, state: string) => string;
|
|
518
|
+
|
|
519
|
+
/** The file scope that sync asks for. It reaches only the app folder of the player. */
|
|
520
|
+
declare const ONEDRIVE_FILE_SCOPE = "Files.ReadWrite.AppFolder";
|
|
521
|
+
/** The file scope, and `offline_access`, which makes Microsoft send a refresh token. */
|
|
522
|
+
declare const ONEDRIVE_SCOPES: readonly ["Files.ReadWrite.AppFolder", "offline_access"];
|
|
523
|
+
/** The address of the Microsoft sign in page for personal accounts. */
|
|
524
|
+
declare const microsoftAuthorizeUrl: (clientId: string, redirectUri: string, state: string) => string;
|
|
525
|
+
|
|
526
|
+
export { type CallbackResult, type Conflict, type ConflictKind, DATA_FILE, DROPBOX_SCOPES, type DeviceRecord, type DropboxOptions, DropboxProvider, type FirstSyncChoice, GITHUB_APP, GOOGLE_DRIVE_SCOPE, type GitHubOptions, GitHubProvider, type GoogleDriveOptions, GoogleDriveProvider, type InstalledRepository, META_FILE, META_FORMAT, MemoryProvider, type MergeInput, type MergeResult, ONEDRIVE_FILE_SCOPE, ONEDRIVE_SCOPES, type OneDriveOptions, OneDriveProvider, type ParsedMeta, type Path, type PathStep, ProviderError, type ProviderErrorKind, ROOT_FOLDER, RemoteDataError, type RemoteFile, type RepositoryCheck, type Schema, type Side, type StoredTokens, SyncBusyError, type SyncMeta, type SyncOptions, type SyncProvider, type SyncResult, type SyncState, type TokenResponse, type TokenSourceOptions, VersionConflictError, authorizeUrl, createTokenSource, dataPath, decodeBase64, dropboxAuthorizeUrl, emptyMeta, encodeBase64, equal, googleAuthorizeUrl, installUrl, isAppId, isObject, listInstalledRepositories, merge, metaPath, microsoftAuthorizeUrl, newState, parseMeta, parseTokenResponse, readCallback, resolveConflict, restoreCloudValues, sync, toStoredTokens, withChiefRemoved, withChiefRevision, withDevice, withSettingsRevision };
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
DATA_FILE: () => DATA_FILE,
|
|
24
|
+
DROPBOX_SCOPES: () => DROPBOX_SCOPES,
|
|
25
|
+
DropboxProvider: () => DropboxProvider,
|
|
24
26
|
GITHUB_APP: () => GITHUB_APP,
|
|
25
27
|
GOOGLE_DRIVE_SCOPE: () => GOOGLE_DRIVE_SCOPE,
|
|
26
28
|
GitHubProvider: () => GitHubProvider,
|
|
@@ -28,6 +30,9 @@ __export(index_exports, {
|
|
|
28
30
|
META_FILE: () => META_FILE,
|
|
29
31
|
META_FORMAT: () => META_FORMAT,
|
|
30
32
|
MemoryProvider: () => MemoryProvider,
|
|
33
|
+
ONEDRIVE_FILE_SCOPE: () => ONEDRIVE_FILE_SCOPE,
|
|
34
|
+
ONEDRIVE_SCOPES: () => ONEDRIVE_SCOPES,
|
|
35
|
+
OneDriveProvider: () => OneDriveProvider,
|
|
31
36
|
ProviderError: () => ProviderError,
|
|
32
37
|
ROOT_FOLDER: () => ROOT_FOLDER,
|
|
33
38
|
RemoteDataError: () => RemoteDataError,
|
|
@@ -37,6 +42,7 @@ __export(index_exports, {
|
|
|
37
42
|
createTokenSource: () => createTokenSource,
|
|
38
43
|
dataPath: () => dataPath,
|
|
39
44
|
decodeBase64: () => decodeBase64,
|
|
45
|
+
dropboxAuthorizeUrl: () => dropboxAuthorizeUrl,
|
|
40
46
|
emptyMeta: () => emptyMeta,
|
|
41
47
|
encodeBase64: () => encodeBase64,
|
|
42
48
|
equal: () => equal,
|
|
@@ -47,6 +53,7 @@ __export(index_exports, {
|
|
|
47
53
|
listInstalledRepositories: () => listInstalledRepositories,
|
|
48
54
|
merge: () => merge,
|
|
49
55
|
metaPath: () => metaPath,
|
|
56
|
+
microsoftAuthorizeUrl: () => microsoftAuthorizeUrl,
|
|
50
57
|
newState: () => newState,
|
|
51
58
|
parseMeta: () => parseMeta,
|
|
52
59
|
parseTokenResponse: () => parseTokenResponse,
|
|
@@ -355,7 +362,8 @@ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array
|
|
|
355
362
|
var detailOf = async (response) => {
|
|
356
363
|
const body = await response.json().catch(() => null);
|
|
357
364
|
if (!isRecord2(body)) return void 0;
|
|
358
|
-
const
|
|
365
|
+
const nested = isRecord2(body.error) ? body.error.message : void 0;
|
|
366
|
+
const message = nested ?? body.message ?? body.error_summary;
|
|
359
367
|
return typeof message === "string" ? message.slice(0, MAX_DETAIL_LENGTH) : void 0;
|
|
360
368
|
};
|
|
361
369
|
|
|
@@ -1104,9 +1112,340 @@ var googleAuthorizeUrl = (clientId, redirectUri, state) => {
|
|
|
1104
1112
|
});
|
|
1105
1113
|
return `https://accounts.google.com/o/oauth2/v2/auth?${query}`;
|
|
1106
1114
|
};
|
|
1115
|
+
|
|
1116
|
+
// src/provider/dropbox.ts
|
|
1117
|
+
var API4 = "https://api.dropboxapi.com/2";
|
|
1118
|
+
var CONTENT = "https://content.dropboxapi.com/2";
|
|
1119
|
+
var isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1120
|
+
var headerJson = (value) => JSON.stringify(value).replace(
|
|
1121
|
+
/[\u007f-]/g,
|
|
1122
|
+
(char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`
|
|
1123
|
+
);
|
|
1124
|
+
var tagsOf = (value, found = /* @__PURE__ */ new Set()) => {
|
|
1125
|
+
if (!isRecord7(value)) return found;
|
|
1126
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1127
|
+
if (key === ".tag" && typeof entry === "string") found.add(entry);
|
|
1128
|
+
else tagsOf(entry, found);
|
|
1129
|
+
}
|
|
1130
|
+
return found;
|
|
1131
|
+
};
|
|
1132
|
+
var retryAfterOf2 = (response, body) => {
|
|
1133
|
+
const header = response.headers.get("retry-after");
|
|
1134
|
+
if (header !== null && Number.isFinite(Number(header))) return Number(header);
|
|
1135
|
+
const seconds = isRecord7(body) && isRecord7(body.error) ? body.error.retry_after : void 0;
|
|
1136
|
+
return typeof seconds === "number" ? seconds : void 0;
|
|
1137
|
+
};
|
|
1138
|
+
var DropboxProvider = class {
|
|
1139
|
+
constructor(options) {
|
|
1140
|
+
this.options = options;
|
|
1141
|
+
this.send = options.fetch ?? ((...args) => globalThis.fetch(...args));
|
|
1142
|
+
}
|
|
1143
|
+
options;
|
|
1144
|
+
send;
|
|
1145
|
+
async read(path) {
|
|
1146
|
+
const response = await this.request(`${CONTENT}/files/download`, {
|
|
1147
|
+
"Dropbox-API-Arg": headerJson({ path: `/${path}` })
|
|
1148
|
+
});
|
|
1149
|
+
if (!response.ok) {
|
|
1150
|
+
const failure = await this.failure(response);
|
|
1151
|
+
if (failure.tags.has("not_found")) return null;
|
|
1152
|
+
throw this.unexpected(failure);
|
|
1153
|
+
}
|
|
1154
|
+
const result = JSON.parse(response.headers.get("dropbox-api-result") ?? "null");
|
|
1155
|
+
if (!isRecord7(result) || typeof result.rev !== "string") {
|
|
1156
|
+
throw this.invalid("The file has no version.");
|
|
1157
|
+
}
|
|
1158
|
+
return { content: await response.text(), version: result.rev };
|
|
1159
|
+
}
|
|
1160
|
+
async write(path, content, expectedVersion) {
|
|
1161
|
+
const mode = expectedVersion === null ? { ".tag": "add" } : { ".tag": "update", update: expectedVersion };
|
|
1162
|
+
const response = await this.request(
|
|
1163
|
+
`${CONTENT}/files/upload`,
|
|
1164
|
+
{
|
|
1165
|
+
"Content-Type": "application/octet-stream",
|
|
1166
|
+
"Dropbox-API-Arg": headerJson({
|
|
1167
|
+
path: `/${path}`,
|
|
1168
|
+
mode,
|
|
1169
|
+
autorename: false,
|
|
1170
|
+
mute: true,
|
|
1171
|
+
strict_conflict: true
|
|
1172
|
+
})
|
|
1173
|
+
},
|
|
1174
|
+
content
|
|
1175
|
+
);
|
|
1176
|
+
if (!response.ok) {
|
|
1177
|
+
const failure = await this.failure(response);
|
|
1178
|
+
if (failure.tags.has("conflict")) throw new VersionConflictError(path);
|
|
1179
|
+
throw this.unexpected(failure);
|
|
1180
|
+
}
|
|
1181
|
+
const result = await response.json().catch(() => null);
|
|
1182
|
+
if (!isRecord7(result) || typeof result.rev !== "string") {
|
|
1183
|
+
throw this.invalid("The write returned no version.");
|
|
1184
|
+
}
|
|
1185
|
+
return { version: result.rev };
|
|
1186
|
+
}
|
|
1187
|
+
async remove(path, expectedVersion) {
|
|
1188
|
+
const response = await this.request(
|
|
1189
|
+
`${API4}/files/delete_v2`,
|
|
1190
|
+
{ "Content-Type": "application/json" },
|
|
1191
|
+
JSON.stringify({ path: `/${path}`, parent_rev: expectedVersion })
|
|
1192
|
+
);
|
|
1193
|
+
if (response.ok) return;
|
|
1194
|
+
const failure = await this.failure(response);
|
|
1195
|
+
if (failure.tags.has("conflict")) throw new VersionConflictError(path);
|
|
1196
|
+
if (failure.tags.has("not_found")) {
|
|
1197
|
+
if (await this.read(path) !== null) throw new VersionConflictError(path);
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
throw this.unexpected(failure);
|
|
1201
|
+
}
|
|
1202
|
+
async request(url, headers, body) {
|
|
1203
|
+
const token = await this.options.getToken();
|
|
1204
|
+
try {
|
|
1205
|
+
return await this.send(url, {
|
|
1206
|
+
method: "POST",
|
|
1207
|
+
headers: { Authorization: `Bearer ${token}`, ...headers },
|
|
1208
|
+
body
|
|
1209
|
+
});
|
|
1210
|
+
} catch {
|
|
1211
|
+
throw new ProviderError("network", "Dropbox could not be reached.");
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Reads a failed answer. Dropbox reports a failure that belongs to the call, such as a missing
|
|
1216
|
+
* file or a conflict, with status 409. The caller decides what that means. Every other status
|
|
1217
|
+
* is a failure of the request and is thrown here.
|
|
1218
|
+
*/
|
|
1219
|
+
async failure(response) {
|
|
1220
|
+
const { status } = response;
|
|
1221
|
+
const body = await response.clone().json().catch(() => null);
|
|
1222
|
+
const detail = await detailOf(response);
|
|
1223
|
+
if (status === 409) return { status, tags: tagsOf(isRecord7(body) ? body.error : null), detail };
|
|
1224
|
+
if (status === 401) {
|
|
1225
|
+
throw new ProviderError("auth", "The token is not valid.", status, void 0, detail);
|
|
1226
|
+
}
|
|
1227
|
+
if (status === 403) {
|
|
1228
|
+
throw new ProviderError(
|
|
1229
|
+
"auth",
|
|
1230
|
+
"The token has no permission for Dropbox.",
|
|
1231
|
+
status,
|
|
1232
|
+
void 0,
|
|
1233
|
+
detail
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
if (status === 429) {
|
|
1237
|
+
throw new ProviderError(
|
|
1238
|
+
"rate-limit",
|
|
1239
|
+
"Dropbox limits the calls.",
|
|
1240
|
+
status,
|
|
1241
|
+
retryAfterOf2(response, body),
|
|
1242
|
+
detail
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
if (status >= 500) {
|
|
1246
|
+
throw new ProviderError("server", "Dropbox failed.", status, void 0, detail);
|
|
1247
|
+
}
|
|
1248
|
+
throw this.invalid(`Dropbox answered ${status}.`, status, detail);
|
|
1249
|
+
}
|
|
1250
|
+
unexpected({ status, tags, detail }) {
|
|
1251
|
+
if (tags.has("too_many_write_operations")) {
|
|
1252
|
+
return new ProviderError(
|
|
1253
|
+
"rate-limit",
|
|
1254
|
+
"Dropbox limits the writes.",
|
|
1255
|
+
status,
|
|
1256
|
+
void 0,
|
|
1257
|
+
detail
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
if (tags.has("no_write_permission")) {
|
|
1261
|
+
return new ProviderError(
|
|
1262
|
+
"auth",
|
|
1263
|
+
"The token has no permission for Dropbox.",
|
|
1264
|
+
status,
|
|
1265
|
+
void 0,
|
|
1266
|
+
detail
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
if (tags.has("insufficient_space")) {
|
|
1270
|
+
return this.invalid("Dropbox has no space left.", status, detail);
|
|
1271
|
+
}
|
|
1272
|
+
return this.invalid(`Dropbox answered ${status}.`, status, detail);
|
|
1273
|
+
}
|
|
1274
|
+
invalid(message, status, detail) {
|
|
1275
|
+
return new ProviderError("invalid", message, status, void 0, detail);
|
|
1276
|
+
}
|
|
1277
|
+
};
|
|
1278
|
+
|
|
1279
|
+
// src/provider/onedrive.ts
|
|
1280
|
+
var APP_FOLDER = "https://graph.microsoft.com/v1.0/me/drive/special/approot";
|
|
1281
|
+
var isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1282
|
+
var encodePath2 = (path) => path.split("/").map(encodeURIComponent).join("/");
|
|
1283
|
+
var retryAfterOf3 = (response) => {
|
|
1284
|
+
const header = response.headers.get("retry-after");
|
|
1285
|
+
return header !== null && Number.isFinite(Number(header)) ? Number(header) : void 0;
|
|
1286
|
+
};
|
|
1287
|
+
var OneDriveProvider = class {
|
|
1288
|
+
constructor(options) {
|
|
1289
|
+
this.options = options;
|
|
1290
|
+
this.send = options.fetch ?? ((...args) => globalThis.fetch(...args));
|
|
1291
|
+
}
|
|
1292
|
+
options;
|
|
1293
|
+
send;
|
|
1294
|
+
async read(path) {
|
|
1295
|
+
const info = await this.info(path);
|
|
1296
|
+
if (info === null) return null;
|
|
1297
|
+
const content = await this.fetchContent(info.downloadUrl);
|
|
1298
|
+
return content === null ? null : { content, version: info.eTag };
|
|
1299
|
+
}
|
|
1300
|
+
async write(path, content, expectedVersion) {
|
|
1301
|
+
const url = expectedVersion === null ? `${this.itemUrl(path)}:/content?@microsoft.graph.conflictBehavior=fail` : `${this.itemUrl(path)}:/content`;
|
|
1302
|
+
const response = await this.request(
|
|
1303
|
+
"PUT",
|
|
1304
|
+
url,
|
|
1305
|
+
{ "Content-Type": "text/plain", ...this.ifMatch(expectedVersion) },
|
|
1306
|
+
content
|
|
1307
|
+
);
|
|
1308
|
+
const expectedFileIsGone = expectedVersion !== null && response.status === 404;
|
|
1309
|
+
if (response.status === 412 || response.status === 409 || expectedFileIsGone) {
|
|
1310
|
+
throw new VersionConflictError(path);
|
|
1311
|
+
}
|
|
1312
|
+
await this.assertOk(response);
|
|
1313
|
+
if (expectedVersion !== null && response.status === 201) throw new VersionConflictError(path);
|
|
1314
|
+
const result = await response.json().catch(() => null);
|
|
1315
|
+
if (!isRecord8(result) || typeof result.eTag !== "string") {
|
|
1316
|
+
throw this.invalid("The write returned no version.");
|
|
1317
|
+
}
|
|
1318
|
+
return { version: result.eTag };
|
|
1319
|
+
}
|
|
1320
|
+
async remove(path, expectedVersion) {
|
|
1321
|
+
const response = await this.request(
|
|
1322
|
+
"DELETE",
|
|
1323
|
+
this.itemUrl(path),
|
|
1324
|
+
this.ifMatch(expectedVersion)
|
|
1325
|
+
);
|
|
1326
|
+
if (response.status === 404) return;
|
|
1327
|
+
if (response.status === 412) throw new VersionConflictError(path);
|
|
1328
|
+
await this.assertOk(response);
|
|
1329
|
+
}
|
|
1330
|
+
itemUrl(path) {
|
|
1331
|
+
return `${APP_FOLDER}:/${encodePath2(path)}`;
|
|
1332
|
+
}
|
|
1333
|
+
ifMatch(version) {
|
|
1334
|
+
return version === null ? {} : { "If-Match": version };
|
|
1335
|
+
}
|
|
1336
|
+
async info(path) {
|
|
1337
|
+
const response = await this.request("GET", this.itemUrl(path));
|
|
1338
|
+
if (response.status === 404) return null;
|
|
1339
|
+
await this.assertOk(response);
|
|
1340
|
+
const body = await response.json().catch(() => null);
|
|
1341
|
+
if (!isRecord8(body) || typeof body.eTag !== "string") {
|
|
1342
|
+
throw this.invalid("The file has no version.");
|
|
1343
|
+
}
|
|
1344
|
+
const url = body["@microsoft.graph.downloadUrl"];
|
|
1345
|
+
if (typeof url !== "string") throw this.invalid("The file has no download address.");
|
|
1346
|
+
return { eTag: body.eTag, downloadUrl: url };
|
|
1347
|
+
}
|
|
1348
|
+
/** The download address is signed, so it must not carry the token. */
|
|
1349
|
+
async fetchContent(url) {
|
|
1350
|
+
let response;
|
|
1351
|
+
try {
|
|
1352
|
+
response = await this.send(url);
|
|
1353
|
+
} catch {
|
|
1354
|
+
throw new ProviderError("network", "OneDrive could not be reached.");
|
|
1355
|
+
}
|
|
1356
|
+
if (response.status === 404) return null;
|
|
1357
|
+
await this.assertOk(response);
|
|
1358
|
+
return response.text();
|
|
1359
|
+
}
|
|
1360
|
+
async request(method, url, headers = {}, body) {
|
|
1361
|
+
const token = await this.options.getToken();
|
|
1362
|
+
try {
|
|
1363
|
+
return await this.send(url, {
|
|
1364
|
+
method,
|
|
1365
|
+
headers: { Authorization: `Bearer ${token}`, ...headers },
|
|
1366
|
+
body
|
|
1367
|
+
});
|
|
1368
|
+
} catch {
|
|
1369
|
+
throw new ProviderError("network", "OneDrive could not be reached.");
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
async assertOk(response) {
|
|
1373
|
+
if (response.ok) return;
|
|
1374
|
+
const { status } = response;
|
|
1375
|
+
const detail = await detailOf(response);
|
|
1376
|
+
if (status === 401) {
|
|
1377
|
+
throw new ProviderError("auth", "The token is not valid.", status, void 0, detail);
|
|
1378
|
+
}
|
|
1379
|
+
if (status === 403) {
|
|
1380
|
+
throw new ProviderError(
|
|
1381
|
+
"auth",
|
|
1382
|
+
"The token has no permission for OneDrive.",
|
|
1383
|
+
status,
|
|
1384
|
+
void 0,
|
|
1385
|
+
detail
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
if (status === 429 || status === 503 && retryAfterOf3(response) !== void 0) {
|
|
1389
|
+
throw new ProviderError(
|
|
1390
|
+
"rate-limit",
|
|
1391
|
+
"OneDrive limits the calls.",
|
|
1392
|
+
status,
|
|
1393
|
+
retryAfterOf3(response),
|
|
1394
|
+
detail
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
if (status === 404) {
|
|
1398
|
+
throw new ProviderError(
|
|
1399
|
+
"not-found",
|
|
1400
|
+
"The app folder was not found.",
|
|
1401
|
+
status,
|
|
1402
|
+
void 0,
|
|
1403
|
+
detail
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
if (status >= 500) {
|
|
1407
|
+
throw new ProviderError("server", "OneDrive failed.", status, void 0, detail);
|
|
1408
|
+
}
|
|
1409
|
+
throw this.invalid(`OneDrive answered ${status}.`, status, detail);
|
|
1410
|
+
}
|
|
1411
|
+
invalid(message, status, detail) {
|
|
1412
|
+
return new ProviderError("invalid", message, status, void 0, detail);
|
|
1413
|
+
}
|
|
1414
|
+
};
|
|
1415
|
+
|
|
1416
|
+
// src/auth/dropbox.ts
|
|
1417
|
+
var DROPBOX_SCOPES = ["files.content.read", "files.content.write"];
|
|
1418
|
+
var dropboxAuthorizeUrl = (clientId, redirectUri, state) => {
|
|
1419
|
+
const query = new URLSearchParams({
|
|
1420
|
+
client_id: clientId,
|
|
1421
|
+
redirect_uri: redirectUri,
|
|
1422
|
+
response_type: "code",
|
|
1423
|
+
token_access_type: "offline",
|
|
1424
|
+
scope: DROPBOX_SCOPES.join(" "),
|
|
1425
|
+
state
|
|
1426
|
+
});
|
|
1427
|
+
return `https://www.dropbox.com/oauth2/authorize?${query}`;
|
|
1428
|
+
};
|
|
1429
|
+
|
|
1430
|
+
// src/auth/microsoft.ts
|
|
1431
|
+
var ONEDRIVE_FILE_SCOPE = "Files.ReadWrite.AppFolder";
|
|
1432
|
+
var ONEDRIVE_SCOPES = [ONEDRIVE_FILE_SCOPE, "offline_access"];
|
|
1433
|
+
var microsoftAuthorizeUrl = (clientId, redirectUri, state) => {
|
|
1434
|
+
const query = new URLSearchParams({
|
|
1435
|
+
client_id: clientId,
|
|
1436
|
+
redirect_uri: redirectUri,
|
|
1437
|
+
response_type: "code",
|
|
1438
|
+
response_mode: "query",
|
|
1439
|
+
scope: ONEDRIVE_SCOPES.join(" "),
|
|
1440
|
+
state
|
|
1441
|
+
});
|
|
1442
|
+
return `https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?${query}`;
|
|
1443
|
+
};
|
|
1107
1444
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1108
1445
|
0 && (module.exports = {
|
|
1109
1446
|
DATA_FILE,
|
|
1447
|
+
DROPBOX_SCOPES,
|
|
1448
|
+
DropboxProvider,
|
|
1110
1449
|
GITHUB_APP,
|
|
1111
1450
|
GOOGLE_DRIVE_SCOPE,
|
|
1112
1451
|
GitHubProvider,
|
|
@@ -1114,6 +1453,9 @@ var googleAuthorizeUrl = (clientId, redirectUri, state) => {
|
|
|
1114
1453
|
META_FILE,
|
|
1115
1454
|
META_FORMAT,
|
|
1116
1455
|
MemoryProvider,
|
|
1456
|
+
ONEDRIVE_FILE_SCOPE,
|
|
1457
|
+
ONEDRIVE_SCOPES,
|
|
1458
|
+
OneDriveProvider,
|
|
1117
1459
|
ProviderError,
|
|
1118
1460
|
ROOT_FOLDER,
|
|
1119
1461
|
RemoteDataError,
|
|
@@ -1123,6 +1465,7 @@ var googleAuthorizeUrl = (clientId, redirectUri, state) => {
|
|
|
1123
1465
|
createTokenSource,
|
|
1124
1466
|
dataPath,
|
|
1125
1467
|
decodeBase64,
|
|
1468
|
+
dropboxAuthorizeUrl,
|
|
1126
1469
|
emptyMeta,
|
|
1127
1470
|
encodeBase64,
|
|
1128
1471
|
equal,
|
|
@@ -1133,6 +1476,7 @@ var googleAuthorizeUrl = (clientId, redirectUri, state) => {
|
|
|
1133
1476
|
listInstalledRepositories,
|
|
1134
1477
|
merge,
|
|
1135
1478
|
metaPath,
|
|
1479
|
+
microsoftAuthorizeUrl,
|
|
1136
1480
|
newState,
|
|
1137
1481
|
parseMeta,
|
|
1138
1482
|
parseTokenResponse,
|