@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 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
- export { type CallbackResult, type Conflict, type ConflictKind, DATA_FILE, type DeviceRecord, type FirstSyncChoice, GITHUB_APP, GOOGLE_DRIVE_SCOPE, type GitHubOptions, GitHubProvider, type GoogleDriveOptions, GoogleDriveProvider, type InstalledRepository, META_FILE, META_FORMAT, MemoryProvider, type MergeInput, type MergeResult, 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, emptyMeta, encodeBase64, equal, googleAuthorizeUrl, installUrl, isAppId, isObject, listInstalledRepositories, merge, metaPath, newState, parseMeta, parseTokenResponse, readCallback, resolveConflict, restoreCloudValues, sync, toStoredTokens, withChiefRemoved, withChiefRevision, withDevice, withSettingsRevision };
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
- export { type CallbackResult, type Conflict, type ConflictKind, DATA_FILE, type DeviceRecord, type FirstSyncChoice, GITHUB_APP, GOOGLE_DRIVE_SCOPE, type GitHubOptions, GitHubProvider, type GoogleDriveOptions, GoogleDriveProvider, type InstalledRepository, META_FILE, META_FORMAT, MemoryProvider, type MergeInput, type MergeResult, 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, emptyMeta, encodeBase64, equal, googleAuthorizeUrl, installUrl, isAppId, isObject, listInstalledRepositories, merge, metaPath, newState, parseMeta, parseTokenResponse, readCallback, resolveConflict, restoreCloudValues, sync, toStoredTokens, withChiefRemoved, withChiefRevision, withDevice, withSettingsRevision };
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 message = isRecord2(body.error) ? body.error.message : body.message;
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,