@gamerdexapp/gamerdex-sync 0.2.1 → 0.3.1
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 +20 -0
- package/README.md +1 -1
- package/dist/index.d.mts +86 -1
- package/dist/index.d.ts +86 -1
- package/dist/index.js +360 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +353 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.1] - 2026-10-18
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- `OneDriveProvider` no longer brings back a file that another device removed. OneDrive creates a
|
|
15
|
+
file when a write with `If-Match` finds none. The provider now removes that file again and reports
|
|
16
|
+
a version conflict.
|
|
17
|
+
|
|
18
|
+
## [0.3.0] - 2026-10-18
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `DropboxProvider` keeps the files in the app folder of the Dropbox of the player. It writes with
|
|
23
|
+
the revision that was read and fails with a version conflict when the file changed since.
|
|
24
|
+
- `OneDriveProvider` keeps the files in the app folder of the OneDrive of a personal Microsoft
|
|
25
|
+
account. It writes with the `eTag` that was read and creates a new file only when none exists.
|
|
26
|
+
- `dropboxAuthorizeUrl()` with `DROPBOX_SCOPES`, and `microsoftAuthorizeUrl()` with
|
|
27
|
+
`ONEDRIVE_FILE_SCOPE` and `ONEDRIVE_SCOPES`, for the two sign in pages.
|
|
28
|
+
- `ProviderError.detail` also reads the summary of a failed Dropbox request.
|
|
29
|
+
|
|
10
30
|
## [0.2.1] - 2026-10-18
|
|
11
31
|
|
|
12
32
|
### 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,89 @@ 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
|
+
/**
|
|
502
|
+
* OneDrive creates the file when a write with `If-Match` finds none, which would undo the
|
|
503
|
+
* removal that another device made. The file that was just created is removed again.
|
|
504
|
+
*/
|
|
505
|
+
private undoCreate;
|
|
506
|
+
private itemUrl;
|
|
507
|
+
private ifMatch;
|
|
508
|
+
private info;
|
|
509
|
+
/** The download address is signed, so it must not carry the token. */
|
|
510
|
+
private fetchContent;
|
|
511
|
+
private request;
|
|
512
|
+
private assertOk;
|
|
513
|
+
private invalid;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** The scopes that sync asks for. Together with the App folder access type, they reach one folder. */
|
|
517
|
+
declare const DROPBOX_SCOPES: readonly ["files.content.read", "files.content.write"];
|
|
518
|
+
/**
|
|
519
|
+
* The address of the Dropbox sign in page. `token_access_type=offline` makes Dropbox send a
|
|
520
|
+
* refresh token, which does not expire until the player removes the access.
|
|
521
|
+
*/
|
|
522
|
+
declare const dropboxAuthorizeUrl: (clientId: string, redirectUri: string, state: string) => string;
|
|
523
|
+
|
|
524
|
+
/** The file scope that sync asks for. It reaches only the app folder of the player. */
|
|
525
|
+
declare const ONEDRIVE_FILE_SCOPE = "Files.ReadWrite.AppFolder";
|
|
526
|
+
/** The file scope, and `offline_access`, which makes Microsoft send a refresh token. */
|
|
527
|
+
declare const ONEDRIVE_SCOPES: readonly ["Files.ReadWrite.AppFolder", "offline_access"];
|
|
528
|
+
/** The address of the Microsoft sign in page for personal accounts. */
|
|
529
|
+
declare const microsoftAuthorizeUrl: (clientId: string, redirectUri: string, state: string) => string;
|
|
530
|
+
|
|
531
|
+
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,89 @@ 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
|
+
/**
|
|
502
|
+
* OneDrive creates the file when a write with `If-Match` finds none, which would undo the
|
|
503
|
+
* removal that another device made. The file that was just created is removed again.
|
|
504
|
+
*/
|
|
505
|
+
private undoCreate;
|
|
506
|
+
private itemUrl;
|
|
507
|
+
private ifMatch;
|
|
508
|
+
private info;
|
|
509
|
+
/** The download address is signed, so it must not carry the token. */
|
|
510
|
+
private fetchContent;
|
|
511
|
+
private request;
|
|
512
|
+
private assertOk;
|
|
513
|
+
private invalid;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** The scopes that sync asks for. Together with the App folder access type, they reach one folder. */
|
|
517
|
+
declare const DROPBOX_SCOPES: readonly ["files.content.read", "files.content.write"];
|
|
518
|
+
/**
|
|
519
|
+
* The address of the Dropbox sign in page. `token_access_type=offline` makes Dropbox send a
|
|
520
|
+
* refresh token, which does not expire until the player removes the access.
|
|
521
|
+
*/
|
|
522
|
+
declare const dropboxAuthorizeUrl: (clientId: string, redirectUri: string, state: string) => string;
|
|
523
|
+
|
|
524
|
+
/** The file scope that sync asks for. It reaches only the app folder of the player. */
|
|
525
|
+
declare const ONEDRIVE_FILE_SCOPE = "Files.ReadWrite.AppFolder";
|
|
526
|
+
/** The file scope, and `offline_access`, which makes Microsoft send a refresh token. */
|
|
527
|
+
declare const ONEDRIVE_SCOPES: readonly ["Files.ReadWrite.AppFolder", "offline_access"];
|
|
528
|
+
/** The address of the Microsoft sign in page for personal accounts. */
|
|
529
|
+
declare const microsoftAuthorizeUrl: (clientId: string, redirectUri: string, state: string) => string;
|
|
530
|
+
|
|
531
|
+
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,355 @@ 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
|
+
const result = await response.json().catch(() => null);
|
|
1314
|
+
if (expectedVersion !== null && response.status === 201) {
|
|
1315
|
+
await this.undoCreate(path, result);
|
|
1316
|
+
throw new VersionConflictError(path);
|
|
1317
|
+
}
|
|
1318
|
+
if (!isRecord8(result) || typeof result.eTag !== "string") {
|
|
1319
|
+
throw this.invalid("The write returned no version.");
|
|
1320
|
+
}
|
|
1321
|
+
return { version: result.eTag };
|
|
1322
|
+
}
|
|
1323
|
+
async remove(path, expectedVersion) {
|
|
1324
|
+
const response = await this.request(
|
|
1325
|
+
"DELETE",
|
|
1326
|
+
this.itemUrl(path),
|
|
1327
|
+
this.ifMatch(expectedVersion)
|
|
1328
|
+
);
|
|
1329
|
+
if (response.status === 404) return;
|
|
1330
|
+
if (response.status === 412) throw new VersionConflictError(path);
|
|
1331
|
+
await this.assertOk(response);
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* OneDrive creates the file when a write with `If-Match` finds none, which would undo the
|
|
1335
|
+
* removal that another device made. The file that was just created is removed again.
|
|
1336
|
+
*/
|
|
1337
|
+
async undoCreate(path, created) {
|
|
1338
|
+
if (!isRecord8(created) || typeof created.eTag !== "string") return;
|
|
1339
|
+
try {
|
|
1340
|
+
await this.remove(path, created.eTag);
|
|
1341
|
+
} catch (error) {
|
|
1342
|
+
if (!(error instanceof VersionConflictError)) throw error;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
itemUrl(path) {
|
|
1346
|
+
return `${APP_FOLDER}:/${encodePath2(path)}`;
|
|
1347
|
+
}
|
|
1348
|
+
ifMatch(version) {
|
|
1349
|
+
return version === null ? {} : { "If-Match": version };
|
|
1350
|
+
}
|
|
1351
|
+
async info(path) {
|
|
1352
|
+
const response = await this.request("GET", this.itemUrl(path));
|
|
1353
|
+
if (response.status === 404) return null;
|
|
1354
|
+
await this.assertOk(response);
|
|
1355
|
+
const body = await response.json().catch(() => null);
|
|
1356
|
+
if (!isRecord8(body) || typeof body.eTag !== "string") {
|
|
1357
|
+
throw this.invalid("The file has no version.");
|
|
1358
|
+
}
|
|
1359
|
+
const url = body["@microsoft.graph.downloadUrl"];
|
|
1360
|
+
if (typeof url !== "string") throw this.invalid("The file has no download address.");
|
|
1361
|
+
return { eTag: body.eTag, downloadUrl: url };
|
|
1362
|
+
}
|
|
1363
|
+
/** The download address is signed, so it must not carry the token. */
|
|
1364
|
+
async fetchContent(url) {
|
|
1365
|
+
let response;
|
|
1366
|
+
try {
|
|
1367
|
+
response = await this.send(url);
|
|
1368
|
+
} catch {
|
|
1369
|
+
throw new ProviderError("network", "OneDrive could not be reached.");
|
|
1370
|
+
}
|
|
1371
|
+
if (response.status === 404) return null;
|
|
1372
|
+
await this.assertOk(response);
|
|
1373
|
+
return response.text();
|
|
1374
|
+
}
|
|
1375
|
+
async request(method, url, headers = {}, body) {
|
|
1376
|
+
const token = await this.options.getToken();
|
|
1377
|
+
try {
|
|
1378
|
+
return await this.send(url, {
|
|
1379
|
+
method,
|
|
1380
|
+
headers: { Authorization: `Bearer ${token}`, ...headers },
|
|
1381
|
+
body
|
|
1382
|
+
});
|
|
1383
|
+
} catch {
|
|
1384
|
+
throw new ProviderError("network", "OneDrive could not be reached.");
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
async assertOk(response) {
|
|
1388
|
+
if (response.ok) return;
|
|
1389
|
+
const { status } = response;
|
|
1390
|
+
const detail = await detailOf(response);
|
|
1391
|
+
if (status === 401) {
|
|
1392
|
+
throw new ProviderError("auth", "The token is not valid.", status, void 0, detail);
|
|
1393
|
+
}
|
|
1394
|
+
if (status === 403) {
|
|
1395
|
+
throw new ProviderError(
|
|
1396
|
+
"auth",
|
|
1397
|
+
"The token has no permission for OneDrive.",
|
|
1398
|
+
status,
|
|
1399
|
+
void 0,
|
|
1400
|
+
detail
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
if (status === 429 || status === 503 && retryAfterOf3(response) !== void 0) {
|
|
1404
|
+
throw new ProviderError(
|
|
1405
|
+
"rate-limit",
|
|
1406
|
+
"OneDrive limits the calls.",
|
|
1407
|
+
status,
|
|
1408
|
+
retryAfterOf3(response),
|
|
1409
|
+
detail
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
if (status === 404) {
|
|
1413
|
+
throw new ProviderError(
|
|
1414
|
+
"not-found",
|
|
1415
|
+
"The app folder was not found.",
|
|
1416
|
+
status,
|
|
1417
|
+
void 0,
|
|
1418
|
+
detail
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
if (status >= 500) {
|
|
1422
|
+
throw new ProviderError("server", "OneDrive failed.", status, void 0, detail);
|
|
1423
|
+
}
|
|
1424
|
+
throw this.invalid(`OneDrive answered ${status}.`, status, detail);
|
|
1425
|
+
}
|
|
1426
|
+
invalid(message, status, detail) {
|
|
1427
|
+
return new ProviderError("invalid", message, status, void 0, detail);
|
|
1428
|
+
}
|
|
1429
|
+
};
|
|
1430
|
+
|
|
1431
|
+
// src/auth/dropbox.ts
|
|
1432
|
+
var DROPBOX_SCOPES = ["files.content.read", "files.content.write"];
|
|
1433
|
+
var dropboxAuthorizeUrl = (clientId, redirectUri, state) => {
|
|
1434
|
+
const query = new URLSearchParams({
|
|
1435
|
+
client_id: clientId,
|
|
1436
|
+
redirect_uri: redirectUri,
|
|
1437
|
+
response_type: "code",
|
|
1438
|
+
token_access_type: "offline",
|
|
1439
|
+
scope: DROPBOX_SCOPES.join(" "),
|
|
1440
|
+
state
|
|
1441
|
+
});
|
|
1442
|
+
return `https://www.dropbox.com/oauth2/authorize?${query}`;
|
|
1443
|
+
};
|
|
1444
|
+
|
|
1445
|
+
// src/auth/microsoft.ts
|
|
1446
|
+
var ONEDRIVE_FILE_SCOPE = "Files.ReadWrite.AppFolder";
|
|
1447
|
+
var ONEDRIVE_SCOPES = [ONEDRIVE_FILE_SCOPE, "offline_access"];
|
|
1448
|
+
var microsoftAuthorizeUrl = (clientId, redirectUri, state) => {
|
|
1449
|
+
const query = new URLSearchParams({
|
|
1450
|
+
client_id: clientId,
|
|
1451
|
+
redirect_uri: redirectUri,
|
|
1452
|
+
response_type: "code",
|
|
1453
|
+
response_mode: "query",
|
|
1454
|
+
scope: ONEDRIVE_SCOPES.join(" "),
|
|
1455
|
+
state
|
|
1456
|
+
});
|
|
1457
|
+
return `https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?${query}`;
|
|
1458
|
+
};
|
|
1107
1459
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1108
1460
|
0 && (module.exports = {
|
|
1109
1461
|
DATA_FILE,
|
|
1462
|
+
DROPBOX_SCOPES,
|
|
1463
|
+
DropboxProvider,
|
|
1110
1464
|
GITHUB_APP,
|
|
1111
1465
|
GOOGLE_DRIVE_SCOPE,
|
|
1112
1466
|
GitHubProvider,
|
|
@@ -1114,6 +1468,9 @@ var googleAuthorizeUrl = (clientId, redirectUri, state) => {
|
|
|
1114
1468
|
META_FILE,
|
|
1115
1469
|
META_FORMAT,
|
|
1116
1470
|
MemoryProvider,
|
|
1471
|
+
ONEDRIVE_FILE_SCOPE,
|
|
1472
|
+
ONEDRIVE_SCOPES,
|
|
1473
|
+
OneDriveProvider,
|
|
1117
1474
|
ProviderError,
|
|
1118
1475
|
ROOT_FOLDER,
|
|
1119
1476
|
RemoteDataError,
|
|
@@ -1123,6 +1480,7 @@ var googleAuthorizeUrl = (clientId, redirectUri, state) => {
|
|
|
1123
1480
|
createTokenSource,
|
|
1124
1481
|
dataPath,
|
|
1125
1482
|
decodeBase64,
|
|
1483
|
+
dropboxAuthorizeUrl,
|
|
1126
1484
|
emptyMeta,
|
|
1127
1485
|
encodeBase64,
|
|
1128
1486
|
equal,
|
|
@@ -1133,6 +1491,7 @@ var googleAuthorizeUrl = (clientId, redirectUri, state) => {
|
|
|
1133
1491
|
listInstalledRepositories,
|
|
1134
1492
|
merge,
|
|
1135
1493
|
metaPath,
|
|
1494
|
+
microsoftAuthorizeUrl,
|
|
1136
1495
|
newState,
|
|
1137
1496
|
parseMeta,
|
|
1138
1497
|
parseTokenResponse,
|