@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/dist/index.mjs CHANGED
@@ -291,7 +291,8 @@ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array
291
291
  var detailOf = async (response) => {
292
292
  const body = await response.json().catch(() => null);
293
293
  if (!isRecord2(body)) return void 0;
294
- const message = isRecord2(body.error) ? body.error.message : body.message;
294
+ const nested = isRecord2(body.error) ? body.error.message : void 0;
295
+ const message = nested ?? body.message ?? body.error_summary;
295
296
  return typeof message === "string" ? message.slice(0, MAX_DETAIL_LENGTH) : void 0;
296
297
  };
297
298
 
@@ -1040,8 +1041,354 @@ var googleAuthorizeUrl = (clientId, redirectUri, state) => {
1040
1041
  });
1041
1042
  return `https://accounts.google.com/o/oauth2/v2/auth?${query}`;
1042
1043
  };
1044
+
1045
+ // src/provider/dropbox.ts
1046
+ var API4 = "https://api.dropboxapi.com/2";
1047
+ var CONTENT = "https://content.dropboxapi.com/2";
1048
+ var isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1049
+ var headerJson = (value) => JSON.stringify(value).replace(
1050
+ /[\u007f-￿]/g,
1051
+ (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`
1052
+ );
1053
+ var tagsOf = (value, found = /* @__PURE__ */ new Set()) => {
1054
+ if (!isRecord7(value)) return found;
1055
+ for (const [key, entry] of Object.entries(value)) {
1056
+ if (key === ".tag" && typeof entry === "string") found.add(entry);
1057
+ else tagsOf(entry, found);
1058
+ }
1059
+ return found;
1060
+ };
1061
+ var retryAfterOf2 = (response, body) => {
1062
+ const header = response.headers.get("retry-after");
1063
+ if (header !== null && Number.isFinite(Number(header))) return Number(header);
1064
+ const seconds = isRecord7(body) && isRecord7(body.error) ? body.error.retry_after : void 0;
1065
+ return typeof seconds === "number" ? seconds : void 0;
1066
+ };
1067
+ var DropboxProvider = class {
1068
+ constructor(options) {
1069
+ this.options = options;
1070
+ this.send = options.fetch ?? ((...args) => globalThis.fetch(...args));
1071
+ }
1072
+ options;
1073
+ send;
1074
+ async read(path) {
1075
+ const response = await this.request(`${CONTENT}/files/download`, {
1076
+ "Dropbox-API-Arg": headerJson({ path: `/${path}` })
1077
+ });
1078
+ if (!response.ok) {
1079
+ const failure = await this.failure(response);
1080
+ if (failure.tags.has("not_found")) return null;
1081
+ throw this.unexpected(failure);
1082
+ }
1083
+ const result = JSON.parse(response.headers.get("dropbox-api-result") ?? "null");
1084
+ if (!isRecord7(result) || typeof result.rev !== "string") {
1085
+ throw this.invalid("The file has no version.");
1086
+ }
1087
+ return { content: await response.text(), version: result.rev };
1088
+ }
1089
+ async write(path, content, expectedVersion) {
1090
+ const mode = expectedVersion === null ? { ".tag": "add" } : { ".tag": "update", update: expectedVersion };
1091
+ const response = await this.request(
1092
+ `${CONTENT}/files/upload`,
1093
+ {
1094
+ "Content-Type": "application/octet-stream",
1095
+ "Dropbox-API-Arg": headerJson({
1096
+ path: `/${path}`,
1097
+ mode,
1098
+ autorename: false,
1099
+ mute: true,
1100
+ strict_conflict: true
1101
+ })
1102
+ },
1103
+ content
1104
+ );
1105
+ if (!response.ok) {
1106
+ const failure = await this.failure(response);
1107
+ if (failure.tags.has("conflict")) throw new VersionConflictError(path);
1108
+ throw this.unexpected(failure);
1109
+ }
1110
+ const result = await response.json().catch(() => null);
1111
+ if (!isRecord7(result) || typeof result.rev !== "string") {
1112
+ throw this.invalid("The write returned no version.");
1113
+ }
1114
+ return { version: result.rev };
1115
+ }
1116
+ async remove(path, expectedVersion) {
1117
+ const response = await this.request(
1118
+ `${API4}/files/delete_v2`,
1119
+ { "Content-Type": "application/json" },
1120
+ JSON.stringify({ path: `/${path}`, parent_rev: expectedVersion })
1121
+ );
1122
+ if (response.ok) return;
1123
+ const failure = await this.failure(response);
1124
+ if (failure.tags.has("conflict")) throw new VersionConflictError(path);
1125
+ if (failure.tags.has("not_found")) {
1126
+ if (await this.read(path) !== null) throw new VersionConflictError(path);
1127
+ return;
1128
+ }
1129
+ throw this.unexpected(failure);
1130
+ }
1131
+ async request(url, headers, body) {
1132
+ const token = await this.options.getToken();
1133
+ try {
1134
+ return await this.send(url, {
1135
+ method: "POST",
1136
+ headers: { Authorization: `Bearer ${token}`, ...headers },
1137
+ body
1138
+ });
1139
+ } catch {
1140
+ throw new ProviderError("network", "Dropbox could not be reached.");
1141
+ }
1142
+ }
1143
+ /**
1144
+ * Reads a failed answer. Dropbox reports a failure that belongs to the call, such as a missing
1145
+ * file or a conflict, with status 409. The caller decides what that means. Every other status
1146
+ * is a failure of the request and is thrown here.
1147
+ */
1148
+ async failure(response) {
1149
+ const { status } = response;
1150
+ const body = await response.clone().json().catch(() => null);
1151
+ const detail = await detailOf(response);
1152
+ if (status === 409) return { status, tags: tagsOf(isRecord7(body) ? body.error : null), detail };
1153
+ if (status === 401) {
1154
+ throw new ProviderError("auth", "The token is not valid.", status, void 0, detail);
1155
+ }
1156
+ if (status === 403) {
1157
+ throw new ProviderError(
1158
+ "auth",
1159
+ "The token has no permission for Dropbox.",
1160
+ status,
1161
+ void 0,
1162
+ detail
1163
+ );
1164
+ }
1165
+ if (status === 429) {
1166
+ throw new ProviderError(
1167
+ "rate-limit",
1168
+ "Dropbox limits the calls.",
1169
+ status,
1170
+ retryAfterOf2(response, body),
1171
+ detail
1172
+ );
1173
+ }
1174
+ if (status >= 500) {
1175
+ throw new ProviderError("server", "Dropbox failed.", status, void 0, detail);
1176
+ }
1177
+ throw this.invalid(`Dropbox answered ${status}.`, status, detail);
1178
+ }
1179
+ unexpected({ status, tags, detail }) {
1180
+ if (tags.has("too_many_write_operations")) {
1181
+ return new ProviderError(
1182
+ "rate-limit",
1183
+ "Dropbox limits the writes.",
1184
+ status,
1185
+ void 0,
1186
+ detail
1187
+ );
1188
+ }
1189
+ if (tags.has("no_write_permission")) {
1190
+ return new ProviderError(
1191
+ "auth",
1192
+ "The token has no permission for Dropbox.",
1193
+ status,
1194
+ void 0,
1195
+ detail
1196
+ );
1197
+ }
1198
+ if (tags.has("insufficient_space")) {
1199
+ return this.invalid("Dropbox has no space left.", status, detail);
1200
+ }
1201
+ return this.invalid(`Dropbox answered ${status}.`, status, detail);
1202
+ }
1203
+ invalid(message, status, detail) {
1204
+ return new ProviderError("invalid", message, status, void 0, detail);
1205
+ }
1206
+ };
1207
+
1208
+ // src/provider/onedrive.ts
1209
+ var APP_FOLDER = "https://graph.microsoft.com/v1.0/me/drive/special/approot";
1210
+ var isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1211
+ var encodePath2 = (path) => path.split("/").map(encodeURIComponent).join("/");
1212
+ var retryAfterOf3 = (response) => {
1213
+ const header = response.headers.get("retry-after");
1214
+ return header !== null && Number.isFinite(Number(header)) ? Number(header) : void 0;
1215
+ };
1216
+ var OneDriveProvider = class {
1217
+ constructor(options) {
1218
+ this.options = options;
1219
+ this.send = options.fetch ?? ((...args) => globalThis.fetch(...args));
1220
+ }
1221
+ options;
1222
+ send;
1223
+ async read(path) {
1224
+ const info = await this.info(path);
1225
+ if (info === null) return null;
1226
+ const content = await this.fetchContent(info.downloadUrl);
1227
+ return content === null ? null : { content, version: info.eTag };
1228
+ }
1229
+ async write(path, content, expectedVersion) {
1230
+ const url = expectedVersion === null ? `${this.itemUrl(path)}:/content?@microsoft.graph.conflictBehavior=fail` : `${this.itemUrl(path)}:/content`;
1231
+ const response = await this.request(
1232
+ "PUT",
1233
+ url,
1234
+ { "Content-Type": "text/plain", ...this.ifMatch(expectedVersion) },
1235
+ content
1236
+ );
1237
+ const expectedFileIsGone = expectedVersion !== null && response.status === 404;
1238
+ if (response.status === 412 || response.status === 409 || expectedFileIsGone) {
1239
+ throw new VersionConflictError(path);
1240
+ }
1241
+ await this.assertOk(response);
1242
+ const result = await response.json().catch(() => null);
1243
+ if (expectedVersion !== null && response.status === 201) {
1244
+ await this.undoCreate(path, result);
1245
+ throw new VersionConflictError(path);
1246
+ }
1247
+ if (!isRecord8(result) || typeof result.eTag !== "string") {
1248
+ throw this.invalid("The write returned no version.");
1249
+ }
1250
+ return { version: result.eTag };
1251
+ }
1252
+ async remove(path, expectedVersion) {
1253
+ const response = await this.request(
1254
+ "DELETE",
1255
+ this.itemUrl(path),
1256
+ this.ifMatch(expectedVersion)
1257
+ );
1258
+ if (response.status === 404) return;
1259
+ if (response.status === 412) throw new VersionConflictError(path);
1260
+ await this.assertOk(response);
1261
+ }
1262
+ /**
1263
+ * OneDrive creates the file when a write with `If-Match` finds none, which would undo the
1264
+ * removal that another device made. The file that was just created is removed again.
1265
+ */
1266
+ async undoCreate(path, created) {
1267
+ if (!isRecord8(created) || typeof created.eTag !== "string") return;
1268
+ try {
1269
+ await this.remove(path, created.eTag);
1270
+ } catch (error) {
1271
+ if (!(error instanceof VersionConflictError)) throw error;
1272
+ }
1273
+ }
1274
+ itemUrl(path) {
1275
+ return `${APP_FOLDER}:/${encodePath2(path)}`;
1276
+ }
1277
+ ifMatch(version) {
1278
+ return version === null ? {} : { "If-Match": version };
1279
+ }
1280
+ async info(path) {
1281
+ const response = await this.request("GET", this.itemUrl(path));
1282
+ if (response.status === 404) return null;
1283
+ await this.assertOk(response);
1284
+ const body = await response.json().catch(() => null);
1285
+ if (!isRecord8(body) || typeof body.eTag !== "string") {
1286
+ throw this.invalid("The file has no version.");
1287
+ }
1288
+ const url = body["@microsoft.graph.downloadUrl"];
1289
+ if (typeof url !== "string") throw this.invalid("The file has no download address.");
1290
+ return { eTag: body.eTag, downloadUrl: url };
1291
+ }
1292
+ /** The download address is signed, so it must not carry the token. */
1293
+ async fetchContent(url) {
1294
+ let response;
1295
+ try {
1296
+ response = await this.send(url);
1297
+ } catch {
1298
+ throw new ProviderError("network", "OneDrive could not be reached.");
1299
+ }
1300
+ if (response.status === 404) return null;
1301
+ await this.assertOk(response);
1302
+ return response.text();
1303
+ }
1304
+ async request(method, url, headers = {}, body) {
1305
+ const token = await this.options.getToken();
1306
+ try {
1307
+ return await this.send(url, {
1308
+ method,
1309
+ headers: { Authorization: `Bearer ${token}`, ...headers },
1310
+ body
1311
+ });
1312
+ } catch {
1313
+ throw new ProviderError("network", "OneDrive could not be reached.");
1314
+ }
1315
+ }
1316
+ async assertOk(response) {
1317
+ if (response.ok) return;
1318
+ const { status } = response;
1319
+ const detail = await detailOf(response);
1320
+ if (status === 401) {
1321
+ throw new ProviderError("auth", "The token is not valid.", status, void 0, detail);
1322
+ }
1323
+ if (status === 403) {
1324
+ throw new ProviderError(
1325
+ "auth",
1326
+ "The token has no permission for OneDrive.",
1327
+ status,
1328
+ void 0,
1329
+ detail
1330
+ );
1331
+ }
1332
+ if (status === 429 || status === 503 && retryAfterOf3(response) !== void 0) {
1333
+ throw new ProviderError(
1334
+ "rate-limit",
1335
+ "OneDrive limits the calls.",
1336
+ status,
1337
+ retryAfterOf3(response),
1338
+ detail
1339
+ );
1340
+ }
1341
+ if (status === 404) {
1342
+ throw new ProviderError(
1343
+ "not-found",
1344
+ "The app folder was not found.",
1345
+ status,
1346
+ void 0,
1347
+ detail
1348
+ );
1349
+ }
1350
+ if (status >= 500) {
1351
+ throw new ProviderError("server", "OneDrive failed.", status, void 0, detail);
1352
+ }
1353
+ throw this.invalid(`OneDrive answered ${status}.`, status, detail);
1354
+ }
1355
+ invalid(message, status, detail) {
1356
+ return new ProviderError("invalid", message, status, void 0, detail);
1357
+ }
1358
+ };
1359
+
1360
+ // src/auth/dropbox.ts
1361
+ var DROPBOX_SCOPES = ["files.content.read", "files.content.write"];
1362
+ var dropboxAuthorizeUrl = (clientId, redirectUri, state) => {
1363
+ const query = new URLSearchParams({
1364
+ client_id: clientId,
1365
+ redirect_uri: redirectUri,
1366
+ response_type: "code",
1367
+ token_access_type: "offline",
1368
+ scope: DROPBOX_SCOPES.join(" "),
1369
+ state
1370
+ });
1371
+ return `https://www.dropbox.com/oauth2/authorize?${query}`;
1372
+ };
1373
+
1374
+ // src/auth/microsoft.ts
1375
+ var ONEDRIVE_FILE_SCOPE = "Files.ReadWrite.AppFolder";
1376
+ var ONEDRIVE_SCOPES = [ONEDRIVE_FILE_SCOPE, "offline_access"];
1377
+ var microsoftAuthorizeUrl = (clientId, redirectUri, state) => {
1378
+ const query = new URLSearchParams({
1379
+ client_id: clientId,
1380
+ redirect_uri: redirectUri,
1381
+ response_type: "code",
1382
+ response_mode: "query",
1383
+ scope: ONEDRIVE_SCOPES.join(" "),
1384
+ state
1385
+ });
1386
+ return `https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?${query}`;
1387
+ };
1043
1388
  export {
1044
1389
  DATA_FILE,
1390
+ DROPBOX_SCOPES,
1391
+ DropboxProvider,
1045
1392
  GITHUB_APP,
1046
1393
  GOOGLE_DRIVE_SCOPE,
1047
1394
  GitHubProvider,
@@ -1049,6 +1396,9 @@ export {
1049
1396
  META_FILE,
1050
1397
  META_FORMAT,
1051
1398
  MemoryProvider,
1399
+ ONEDRIVE_FILE_SCOPE,
1400
+ ONEDRIVE_SCOPES,
1401
+ OneDriveProvider,
1052
1402
  ProviderError,
1053
1403
  ROOT_FOLDER,
1054
1404
  RemoteDataError,
@@ -1058,6 +1408,7 @@ export {
1058
1408
  createTokenSource,
1059
1409
  dataPath,
1060
1410
  decodeBase64,
1411
+ dropboxAuthorizeUrl,
1061
1412
  emptyMeta,
1062
1413
  encodeBase64,
1063
1414
  equal,
@@ -1068,6 +1419,7 @@ export {
1068
1419
  listInstalledRepositories,
1069
1420
  merge,
1070
1421
  metaPath,
1422
+ microsoftAuthorizeUrl,
1071
1423
  newState,
1072
1424
  parseMeta,
1073
1425
  parseTokenResponse,