@finueva/drive 0.1.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/dist/server.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { O as UploadQuotaReservation, T as ReservedUploadQuota, a as CreateUploadInput, f as DriveServerClientConfig, n as AbortedUploadQuota, o as DriveClient, t as AbortUploadInput } from "./contract.js";
1
+ import { A as ProtectionClassification, C as ListFolderChildrenInput, H as UploadPartResult, I as UploadConfirmedPart, M as ReadyUpload, N as RecordUploadPartInput, S as GetUploadStatusInput, U as UploadSession, V as UploadPartDescriptor, W as UploadStatus, a as CreateUploadInput, b as FolderPage, g as DriveServerClientConfig, h as DriveServerClient, n as CompleteUploadInput, o as CreateUploadPartDescriptorInput, s as CurrentWorkspace, t as AbortUploadInput, x as GetCurrentWorkspaceInput, y as FolderItem } from "./contract.js";
2
2
  //#region src/server.d.ts
3
- declare function createDriveServerClient(config: DriveServerClientConfig): DriveClient;
3
+ declare function createDriveServerClient(config: DriveServerClientConfig): DriveServerClient;
4
4
  //#endregion
5
- export { type AbortUploadInput, type AbortedUploadQuota, type CreateUploadInput, type DriveClient, type DriveServerClientConfig, type ReservedUploadQuota, type UploadQuotaReservation, createDriveServerClient };
5
+ export { type AbortUploadInput, type CompleteUploadInput, type CreateUploadInput, type CreateUploadPartDescriptorInput, type CurrentWorkspace, type DriveServerClient, type DriveServerClientConfig, type FolderItem, type FolderPage, type GetCurrentWorkspaceInput, type GetUploadStatusInput, type ListFolderChildrenInput, type ProtectionClassification, type ReadyUpload, type RecordUploadPartInput, type UploadConfirmedPart, type UploadPartDescriptor, type UploadPartResult, type UploadSession, type UploadStatus, createDriveServerClient };
package/dist/server.js CHANGED
@@ -10,6 +10,7 @@ const DRIVE_API_ERROR_CODES = Object.freeze([
10
10
  "precondition_failed",
11
11
  "idempotency_conflict",
12
12
  "quota_exceeded",
13
+ "upload_recovery_required",
13
14
  "service_unavailable",
14
15
  "internal_error"
15
16
  ]);
@@ -24,6 +25,7 @@ const DRIVE_API_ERROR_MESSAGES = Object.freeze({
24
25
  precondition_failed: "The request precondition failed.",
25
26
  idempotency_conflict: "The idempotency key conflicts with an earlier request.",
26
27
  quota_exceeded: "The workspace storage quota would be exceeded.",
28
+ upload_recovery_required: "The upload requires recovery before it can continue.",
27
29
  service_unavailable: "The service is temporarily unavailable.",
28
30
  internal_error: "The request could not be completed."
29
31
  });
@@ -59,13 +61,13 @@ const REQUEST_ID = /^[A-Za-z0-9_-]{16,128}$/;
59
61
  const COLLECTION_ETAG = /^"drive\.permission-collection\.v1\.(0|[1-9][0-9]*)"$/;
60
62
  const GRANT_ETAG$1 = /^"drive\.permission-grant\.v1\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)"$/;
61
63
  const LIST_ETAG$1 = /^"drive\.permission-list\.v1\.[0-9a-f]{64}"$/;
62
- async function parseResponse(response, operationSignal, preconditionStatus, successStatuses, guard, allowsQuotaExceeded) {
64
+ async function parseResponse(response, operationSignal, preconditionStatus, successStatuses, guard, allowsQuotaExceeded, allowedErrorCodes) {
63
65
  const requestId = safeRequestId(response.headers.get("x-request-id"));
64
66
  if (requestId === null) {
65
67
  cancelResponse(response);
66
68
  throw new DriveError("invalid_response", { status: response.status });
67
69
  }
68
- if (!successStatuses.includes(response.status)) throw await normalizeError(response, requestId, operationSignal, preconditionStatus, allowsQuotaExceeded);
70
+ if (!successStatuses.includes(response.status)) throw await normalizeError(response, requestId, operationSignal, preconditionStatus, allowsQuotaExceeded, allowedErrorCodes);
69
71
  const { value } = await parseBoundedJson(response, SUCCESS_BODY_LIMIT, response.status, requestId, operationSignal);
70
72
  const status = response.status;
71
73
  if (!guard(value, status, response.headers)) throw new DriveError("invalid_response", {
@@ -79,6 +81,28 @@ async function parseResponse(response, operationSignal, preconditionStatus, succ
79
81
  validators: readValidators(response.headers)
80
82
  });
81
83
  }
84
+ async function parseNoContentResponse(response, operationSignal, preconditionStatus, successStatuses, allowedErrorCodes) {
85
+ const requestId = safeRequestId(response.headers.get("x-request-id"));
86
+ if (requestId === null) {
87
+ cancelResponse(response);
88
+ throw new DriveError("invalid_response", { status: response.status });
89
+ }
90
+ if (!successStatuses.includes(response.status)) throw await normalizeError(response, requestId, operationSignal, preconditionStatus, false, allowedErrorCodes);
91
+ const contentLength = response.headers.get("content-length");
92
+ if (response.body !== null || contentLength !== null && contentLength !== "0" || response.headers.has("content-type")) {
93
+ cancelResponse(response);
94
+ throw new DriveError("invalid_response", {
95
+ status: response.status,
96
+ requestId
97
+ });
98
+ }
99
+ return Object.freeze({
100
+ data: void 0,
101
+ status: response.status,
102
+ requestId,
103
+ validators: Object.freeze({})
104
+ });
105
+ }
82
106
  function parseCollectionEtag(value) {
83
107
  const match = COLLECTION_ETAG.exec(value);
84
108
  if (!match) return void 0;
@@ -92,7 +116,7 @@ function parseGrantEtag(value) {
92
116
  const grantRevision = Number(match[2]);
93
117
  return Number.isSafeInteger(collectionRevision) && Number.isSafeInteger(grantRevision) ? [collectionRevision, grantRevision] : void 0;
94
118
  }
95
- async function normalizeError(response, headerRequestId, signal, preconditionStatus, allowsQuotaExceeded) {
119
+ async function normalizeError(response, headerRequestId, signal, preconditionStatus, allowsQuotaExceeded, allowedErrorCodes) {
96
120
  let parsed;
97
121
  try {
98
122
  parsed = await parseBoundedJson(response, ERROR_BODY_LIMIT, response.status, headerRequestId, signal);
@@ -114,7 +138,7 @@ async function normalizeError(response, headerRequestId, signal, preconditionSta
114
138
  requestId: headerRequestId
115
139
  });
116
140
  const envelope = value.error;
117
- if (typeof envelope.code !== "string" || !DRIVE_API_ERROR_CODES.includes(envelope.code) || envelope.message !== DRIVE_API_ERROR_MESSAGES[envelope.code] || !REQUEST_ID.test(String(envelope.requestId)) || envelope.requestId !== headerRequestId || !isExactRecord$1(envelope.details, []) || !errorStatusMatches(envelope.code, response.status, preconditionStatus, allowsQuotaExceeded) || parsed.text !== JSON.stringify(value)) return new DriveError("invalid_response", {
141
+ if (typeof envelope.code !== "string" || !DRIVE_API_ERROR_CODES.includes(envelope.code) || envelope.message !== DRIVE_API_ERROR_MESSAGES[envelope.code] || !REQUEST_ID.test(String(envelope.requestId)) || envelope.requestId !== headerRequestId || !isExactRecord$1(envelope.details, []) || !errorStatusMatches(envelope.code, response.status, preconditionStatus, allowsQuotaExceeded, allowedErrorCodes) || parsed.text !== JSON.stringify(value)) return new DriveError("invalid_response", {
118
142
  status: response.status,
119
143
  requestId: headerRequestId
120
144
  });
@@ -123,7 +147,8 @@ async function normalizeError(response, headerRequestId, signal, preconditionSta
123
147
  requestId: envelope.requestId
124
148
  });
125
149
  }
126
- function errorStatusMatches(code, status, preconditionStatus, allowsQuotaExceeded) {
150
+ function errorStatusMatches(code, status, preconditionStatus, allowsQuotaExceeded, allowedErrorCodes) {
151
+ if (allowedErrorCodes && !allowedErrorCodes.includes(code)) return false;
127
152
  if (code === "quota_exceeded" && !allowsQuotaExceeded) return false;
128
153
  return {
129
154
  authentication_required: [401],
@@ -133,9 +158,10 @@ function errorStatusMatches(code, status, preconditionStatus, allowsQuotaExceede
133
158
  invalid_request: [422],
134
159
  invalid_pagination_cursor: [422],
135
160
  invalid_idempotency_key: [422],
136
- precondition_failed: [preconditionStatus],
161
+ precondition_failed: preconditionStatus === null ? [] : [preconditionStatus],
137
162
  idempotency_conflict: [409],
138
163
  quota_exceeded: [409],
164
+ upload_recovery_required: [409],
139
165
  service_unavailable: [503],
140
166
  internal_error: [500]
141
167
  }[code].includes(status);
@@ -246,6 +272,7 @@ const WORKSPACE_ID$1 = /^drive\.workspace\.[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-
246
272
  const VOLUME_ID = /^drive\.volume\.[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
247
273
  const GRANT_ID$1 = /^drive\.grant\.[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
248
274
  const UPLOAD_ID$1 = /^drive\.upload\.[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
275
+ const VERSION_ID = /^drive\.version\.[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
249
276
  const CURSOR$1 = /^drive\.cursor\.v1\.[A-Za-z0-9_-]+$/;
250
277
  const AUTH_ID$1 = /^[A-Za-z0-9_-]{1,128}$/;
251
278
  const GRANT_ETAG = /^"drive\.permission-grant\.v1\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)"$/;
@@ -268,7 +295,18 @@ const EFFECTIVE_ACTIONS = Object.freeze([
268
295
  "items:update"
269
296
  ]);
270
297
  const MAX_UPLOAD_BYTE_LENGTH$1 = 33554432e4;
271
- const UPLOAD_QUOTA_EXPIRY_MILLISECONDS = 864e5;
298
+ const UPLOAD_PART_BYTE_LENGTH$1 = 32 * 1024 * 1024;
299
+ const CONTENT_DIGEST$1 = /^sha256:[0-9a-f]{64}$/;
300
+ function isCurrentWorkspace(value) {
301
+ if (!isExactRecord(value, [
302
+ "workspaceId",
303
+ "rootVolumeId",
304
+ "rootItemId",
305
+ "workspaceReference"
306
+ ]) || typeof value.workspaceId !== "string" || !WORKSPACE_ID$1.test(value.workspaceId) || typeof value.rootVolumeId !== "string" || !VOLUME_ID.test(value.rootVolumeId) || typeof value.rootItemId !== "string" || !ITEM_ID$1.test(value.rootItemId)) return false;
307
+ const reference = value.workspaceReference;
308
+ return isExactRecord(reference, ["type", "userId"]) && reference.type === "personal" && typeof reference.userId === "string" && AUTH_ID$1.test(reference.userId) || isExactRecord(reference, ["type", "organizationId"]) && reference.type === "organization" && typeof reference.organizationId === "string" && AUTH_ID$1.test(reference.organizationId);
309
+ }
272
310
  function isFolderPage(value, workspaceId, parentItemId) {
273
311
  if (!(isExactRecord(value, ["items", "nextCursor"]) && Array.isArray(value.items) && value.items.length <= 50 && value.items.every(isFolderItem) && (value.nextCursor === null || typeof value.nextCursor === "string" && value.nextCursor.length <= 699 && CURSOR$1.test(value.nextCursor)))) return false;
274
312
  return value.items.every((item) => item.workspaceId === workspaceId && item.parentItemId === parentItemId) && isStrictlyOrdered(value.items, (item) => item.itemId);
@@ -339,18 +377,93 @@ function isEffectivePermissionResponse(value, targetItemId) {
339
377
  }
340
378
  return true;
341
379
  }
342
- function isUploadQuotaReservation(value) {
343
- return isExactRecord(value, [
344
- "workspaceId",
380
+ function isUploadSession(value, expectedByteLength) {
381
+ if (!isExactRecord(value, [
382
+ "itemId",
383
+ "versionId",
345
384
  "uploadId",
346
- "parentItemId",
347
- "expectedByteLength",
348
- "state",
349
- "terminalReason",
350
- "createdAtEpochMilliseconds",
385
+ "mode",
386
+ "partByteLength",
387
+ "partCount",
388
+ "revision",
389
+ "expiresAtEpochMilliseconds"
390
+ ]) || typeof value.itemId !== "string" || !ITEM_ID$1.test(value.itemId) || typeof value.versionId !== "string" || !VERSION_ID.test(value.versionId) || typeof value.uploadId !== "string" || !UPLOAD_ID$1.test(value.uploadId) || value.mode !== "presigned-multipart" && value.mode !== "server-mediated-zero-byte" || value.partByteLength !== UPLOAD_PART_BYTE_LENGTH$1 || !isSafeInteger$1(value.partCount) || value.partCount > 1e4 || value.partCount !== Math.ceil(expectedByteLength / UPLOAD_PART_BYTE_LENGTH$1) || (expectedByteLength === 0 ? value.mode !== "server-mediated-zero-byte" : value.mode !== "presigned-multipart") || !isSafeInteger$1(value.revision) || !isSafeInteger$1(value.expiresAtEpochMilliseconds) || value.expiresAtEpochMilliseconds <= Date.now()) return false;
391
+ Object.freeze(value);
392
+ return true;
393
+ }
394
+ function isUploadStatus(value, afterPartNumber, limit) {
395
+ if (!isExactRecord(value, [
396
+ "itemId",
397
+ "versionId",
398
+ "uploadId",
399
+ "mode",
400
+ "lifecycle",
401
+ "revision",
402
+ "partPolicy",
403
+ "confirmedParts",
404
+ "nextAfterPartNumber",
351
405
  "expiresAtEpochMilliseconds",
352
- "terminalAtEpochMilliseconds"
353
- ]) && typeof value.workspaceId === "string" && WORKSPACE_ID$1.test(value.workspaceId) && typeof value.uploadId === "string" && UPLOAD_ID$1.test(value.uploadId) && typeof value.parentItemId === "string" && ITEM_ID$1.test(value.parentItemId) && typeof value.expectedByteLength === "number" && Number.isSafeInteger(value.expectedByteLength) && value.expectedByteLength >= 0 && value.expectedByteLength <= MAX_UPLOAD_BYTE_LENGTH$1 && (value.state === "reserved" || value.state === "converted" || value.state === "released") && (value.terminalReason === null || value.terminalReason === "completed" || value.terminalReason === "aborted" || value.terminalReason === "failed" || value.terminalReason === "expired") && isSafeInteger$1(value.createdAtEpochMilliseconds) && isSafeInteger$1(value.expiresAtEpochMilliseconds) && value.expiresAtEpochMilliseconds - value.createdAtEpochMilliseconds === UPLOAD_QUOTA_EXPIRY_MILLISECONDS && (value.terminalAtEpochMilliseconds === null || isSafeInteger$1(value.terminalAtEpochMilliseconds) && value.terminalAtEpochMilliseconds >= value.createdAtEpochMilliseconds);
406
+ "ready"
407
+ ]) || typeof value.itemId !== "string" || !ITEM_ID$1.test(value.itemId) || typeof value.versionId !== "string" || !VERSION_ID.test(value.versionId) || typeof value.uploadId !== "string" || !UPLOAD_ID$1.test(value.uploadId) || value.mode !== "presigned-multipart" && value.mode !== "server-mediated-zero-byte" || ![
408
+ "initiating",
409
+ "recovery-required",
410
+ "uploading",
411
+ "completing",
412
+ "provider-completed",
413
+ "ready",
414
+ "aborting",
415
+ "aborted"
416
+ ].includes(String(value.lifecycle)) || !isSafeInteger$1(value.revision) || !isExactRecord(value.partPolicy, [
417
+ "partByteLength",
418
+ "partCount",
419
+ "maxParts"
420
+ ]) || value.partPolicy.partByteLength !== UPLOAD_PART_BYTE_LENGTH$1 || !isSafeInteger$1(value.partPolicy.partCount) || value.partPolicy.partCount > 1e4 || value.partPolicy.maxParts !== 1e4 || (value.mode === "server-mediated-zero-byte" ? value.partPolicy.partCount !== 0 : value.partPolicy.partCount < 1) || !Array.isArray(value.confirmedParts) || value.confirmedParts.length > limit || !isSafeInteger$1(value.expiresAtEpochMilliseconds)) return false;
421
+ let previousPartNumber = afterPartNumber;
422
+ for (const part of value.confirmedParts) {
423
+ if (!isExactRecord(part, ["partNumber", "checksumSha256"]) || !isSafeInteger$1(part.partNumber) || part.partNumber <= previousPartNumber || part.partNumber > value.partPolicy.partCount || !isPartChecksum$1(part.checksumSha256)) return false;
424
+ previousPartNumber = part.partNumber;
425
+ }
426
+ if (!(value.nextAfterPartNumber === null || isSafeInteger$1(value.nextAfterPartNumber) && value.confirmedParts.length === limit && value.nextAfterPartNumber === previousPartNumber) || value.mode === "server-mediated-zero-byte" && (value.confirmedParts.length !== 0 || value.nextAfterPartNumber !== null) || value.lifecycle === "ready" !== (value.ready !== null) || value.ready !== null && (!isReadyUpload(value.ready, { uploadId: value.uploadId }) || value.ready.itemId !== value.itemId || value.ready.versionId !== value.versionId)) return false;
427
+ Object.freeze(value.partPolicy);
428
+ for (const part of value.confirmedParts) Object.freeze(part);
429
+ Object.freeze(value.confirmedParts);
430
+ Object.freeze(value);
431
+ return true;
432
+ }
433
+ function isUploadPartDescriptor(value, expectedPartNumber, checksumSha256, allowInsecureHttp) {
434
+ if (!isExactRecord(value, [
435
+ "url",
436
+ "method",
437
+ "headers",
438
+ "partNumber",
439
+ "expectedByteLength",
440
+ "expiresAtEpochMilliseconds"
441
+ ]) || !isSignedTransferUrl(value.url, allowInsecureHttp) || value.method !== "PUT" || !isExactRecord(value.headers, ["x-amz-checksum-sha256"]) || value.headers["x-amz-checksum-sha256"] !== checksumSha256 || value.partNumber !== expectedPartNumber || !isSafeInteger$1(value.expectedByteLength) || value.expectedByteLength < 1 || value.expectedByteLength > UPLOAD_PART_BYTE_LENGTH$1 || !isSafeInteger$1(value.expiresAtEpochMilliseconds) || value.expiresAtEpochMilliseconds <= Date.now()) return false;
442
+ Object.freeze(value.headers);
443
+ Object.freeze(value);
444
+ return true;
445
+ }
446
+ function isUploadPartResult(value, expectedPartNumber, expectedUploadRevision) {
447
+ if (!isExactRecord(value, ["partNumber", "revision"]) || value.partNumber !== expectedPartNumber || value.revision !== expectedUploadRevision + 1 || !isSafeInteger$1(value.revision)) return false;
448
+ Object.freeze(value);
449
+ return true;
450
+ }
451
+ function isReadyUpload(value, expected) {
452
+ if (!isExactRecord(value, [
453
+ "itemId",
454
+ "versionId",
455
+ "uploadId",
456
+ "byteLength",
457
+ "callerDeclaredContentDigest",
458
+ "displayFilename",
459
+ "mediaType",
460
+ "state"
461
+ ]) || typeof value.itemId !== "string" || !ITEM_ID$1.test(value.itemId) || typeof value.versionId !== "string" || !VERSION_ID.test(value.versionId) || value.uploadId !== expected.uploadId || !isSafeInteger$1(value.byteLength) || value.byteLength < 0 || value.byteLength > MAX_UPLOAD_BYTE_LENGTH$1 || typeof value.callerDeclaredContentDigest !== "string" || !CONTENT_DIGEST$1.test(value.callerDeclaredContentDigest) || !isSafeFilename$1(value.displayFilename) || !isMediaType$1(value.mediaType) || value.state !== "ready" || expected.byteLength !== void 0 && value.byteLength !== expected.byteLength || expected.callerDeclaredContentDigest !== void 0 && value.callerDeclaredContentDigest !== expected.callerDeclaredContentDigest || expected.displayFilename !== void 0 && value.displayFilename !== expected.displayFilename || expected.mediaType !== void 0 && value.mediaType !== expected.mediaType) return false;
462
+ Object.freeze(value);
463
+ return true;
464
+ }
465
+ function isPartChecksum$1(value) {
466
+ return typeof value === "string" && /^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw048]=$/.test(value);
354
467
  }
355
468
  function mutationValidatorsAreCoherent(headers) {
356
469
  const collection = headers.get("drive-permission-collection-etag");
@@ -407,6 +520,19 @@ function isSafeFilename$1(value) {
407
520
  }
408
521
  return true;
409
522
  }
523
+ function isMediaType$1(value) {
524
+ return typeof value === "string" && value.length <= 255 && /^[a-z0-9](?:[a-z0-9!#$&^_.+-]{0,125}[a-z0-9])?\/[a-z0-9](?:[a-z0-9!#$&^_.+-]{0,125}[a-z0-9])?$/.test(value);
525
+ }
526
+ function isSignedTransferUrl(value, allowInsecureHttp) {
527
+ if (typeof value !== "string" || value.length < 1 || value.length > 8192 || value !== value.trim()) return false;
528
+ try {
529
+ const url = new URL(value);
530
+ const loopbackHost = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
531
+ return (url.protocol === "https:" || url.protocol === "http:" && allowInsecureHttp && loopbackHost) && url.username === "" && url.password === "" && url.hash === "" && url.host !== "";
532
+ } catch {
533
+ return false;
534
+ }
535
+ }
410
536
  function filenameIsReserved$1(value) {
411
537
  const base = value.split(".", 1)[0].toUpperCase();
412
538
  return value === "." || value === ".." || [
@@ -446,7 +572,14 @@ const PERMISSION_ACTIONS = Object.freeze([
446
572
  "items:update"
447
573
  ]);
448
574
  const IDEMPOTENCY_KEY = /^[A-Za-z0-9_-]{16,128}$/;
575
+ const CONTENT_DIGEST = /^sha256:[0-9a-f]{64}$/;
576
+ const PART_CHECKSUM = /^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw048]=$/;
449
577
  const MAX_UPLOAD_BYTE_LENGTH = 33554432e4;
578
+ const UPLOAD_PART_BYTE_LENGTH = 32 * 1024 * 1024;
579
+ function validateCurrentWorkspaceInput(value) {
580
+ const signal = validateSignal(captureRecord(value === void 0 ? {} : value, [], ["signal"]).signal);
581
+ return signal === void 0 ? {} : { signal };
582
+ }
450
583
  function validateResourceInput(value) {
451
584
  const input = captureRecord(value, ["workspaceId", "itemId"], ["signal"]);
452
585
  const workspaceId = input.workspaceId;
@@ -581,28 +714,93 @@ function validateCreateUploadInput(value) {
581
714
  "workspaceId",
582
715
  "parentItemId",
583
716
  "expectedByteLength",
717
+ "displayFilename",
718
+ "mediaType",
719
+ "callerDeclaredContentDigest",
720
+ "protectionClassification",
584
721
  "idempotencyKey"
585
722
  ], ["signal"]);
586
723
  const signal = validateSignal(input.signal);
587
- if (typeof input.workspaceId !== "string" || !WORKSPACE_ID.test(input.workspaceId) || typeof input.parentItemId !== "string" || !ITEM_ID.test(input.parentItemId) || typeof input.expectedByteLength !== "number" || !Number.isSafeInteger(input.expectedByteLength) || input.expectedByteLength < 0 || input.expectedByteLength > MAX_UPLOAD_BYTE_LENGTH || !isIdempotencyKey(input.idempotencyKey)) throw new DriveError("invalid_input");
724
+ if (typeof input.workspaceId !== "string" || !WORKSPACE_ID.test(input.workspaceId) || typeof input.parentItemId !== "string" || !ITEM_ID.test(input.parentItemId) || typeof input.expectedByteLength !== "number" || !Number.isSafeInteger(input.expectedByteLength) || input.expectedByteLength < 0 || input.expectedByteLength > MAX_UPLOAD_BYTE_LENGTH || !isSafeFilename(input.displayFilename) || !isMediaType(input.mediaType) || typeof input.callerDeclaredContentDigest !== "string" || !CONTENT_DIGEST.test(input.callerDeclaredContentDigest) || input.protectionClassification !== "ordinary" && input.protectionClassification !== "opaque-encrypted" || !isIdempotencyKey(input.idempotencyKey)) throw new DriveError("invalid_input");
588
725
  return {
589
726
  workspaceId: input.workspaceId,
590
727
  parentItemId: input.parentItemId,
591
728
  expectedByteLength: input.expectedByteLength,
729
+ displayFilename: input.displayFilename,
730
+ mediaType: input.mediaType,
731
+ callerDeclaredContentDigest: input.callerDeclaredContentDigest,
732
+ protectionClassification: input.protectionClassification,
592
733
  idempotencyKey: input.idempotencyKey,
593
734
  ...signal === void 0 ? {} : { signal }
594
735
  };
595
736
  }
596
- function validateAbortUploadInput(value) {
597
- const input = captureRecord(value, ["workspaceId", "uploadId"], ["signal"]);
598
- const signal = validateSignal(input.signal);
599
- if (typeof input.workspaceId !== "string" || !WORKSPACE_ID.test(input.workspaceId) || typeof input.uploadId !== "string" || !UPLOAD_ID.test(input.uploadId)) throw new DriveError("invalid_input");
737
+ function validateGetUploadStatusInput(value) {
738
+ const input = captureRecord(value, ["workspaceId", "uploadId"], [
739
+ "afterPartNumber",
740
+ "limit",
741
+ "signal"
742
+ ]);
743
+ const upload = validateUploadPath(input.workspaceId, input.uploadId, input.signal);
744
+ if (input.afterPartNumber !== void 0 && (!isSafeInteger(input.afterPartNumber) || input.afterPartNumber > 1e4) || input.limit !== void 0 && (!isSafeInteger(input.limit) || input.limit < 1 || input.limit > 100)) throw new DriveError("invalid_input");
600
745
  return {
601
- workspaceId: input.workspaceId,
602
- uploadId: input.uploadId,
603
- ...signal === void 0 ? {} : { signal }
746
+ ...upload,
747
+ ...input.afterPartNumber === void 0 ? {} : { afterPartNumber: input.afterPartNumber },
748
+ ...input.limit === void 0 ? {} : { limit: input.limit }
749
+ };
750
+ }
751
+ function validateCreateUploadPartDescriptorInput(value) {
752
+ const input = captureRecord(value, [
753
+ "workspaceId",
754
+ "uploadId",
755
+ "partNumber",
756
+ "checksumSha256"
757
+ ], ["signal"]);
758
+ const upload = validateUploadPath(input.workspaceId, input.uploadId, input.signal);
759
+ if (!isPartNumber(input.partNumber) || !isPartChecksum(input.checksumSha256)) throw new DriveError("invalid_input");
760
+ return {
761
+ ...upload,
762
+ partNumber: input.partNumber,
763
+ checksumSha256: input.checksumSha256
764
+ };
765
+ }
766
+ function validateRecordUploadPartInput(value) {
767
+ const input = captureRecord(value, [
768
+ "workspaceId",
769
+ "uploadId",
770
+ "partNumber",
771
+ "expectedUploadRevision",
772
+ "byteLength",
773
+ "etag",
774
+ "checksumSha256"
775
+ ], ["signal"]);
776
+ const upload = validateUploadPath(input.workspaceId, input.uploadId, input.signal);
777
+ if (!isPartNumber(input.partNumber) || !isSafeInteger(input.expectedUploadRevision) || input.expectedUploadRevision >= Number.MAX_SAFE_INTEGER || !isSafeInteger(input.byteLength) || input.byteLength < 1 || input.byteLength > UPLOAD_PART_BYTE_LENGTH || !isEtag(input.etag) || !isPartChecksum(input.checksumSha256)) throw new DriveError("invalid_input");
778
+ return {
779
+ ...upload,
780
+ partNumber: input.partNumber,
781
+ expectedUploadRevision: input.expectedUploadRevision,
782
+ byteLength: input.byteLength,
783
+ etag: input.etag,
784
+ checksumSha256: input.checksumSha256
785
+ };
786
+ }
787
+ function validateCompleteUploadInput(value) {
788
+ const input = captureRecord(value, [
789
+ "workspaceId",
790
+ "uploadId",
791
+ "idempotencyKey"
792
+ ], ["signal"]);
793
+ const upload = validateUploadPath(input.workspaceId, input.uploadId, input.signal);
794
+ if (!isIdempotencyKey(input.idempotencyKey)) throw new DriveError("invalid_input");
795
+ return {
796
+ ...upload,
797
+ idempotencyKey: input.idempotencyKey
604
798
  };
605
799
  }
800
+ function validateAbortUploadInput(value) {
801
+ const input = captureRecord(value, ["workspaceId", "uploadId"], ["signal"]);
802
+ return validateUploadPath(input.workspaceId, input.uploadId, input.signal);
803
+ }
606
804
  function resourcePath(input) {
607
805
  return {
608
806
  workspaceId: input.workspaceId,
@@ -687,6 +885,27 @@ function captureArray(value) {
687
885
  function isIdempotencyKey(value) {
688
886
  return typeof value === "string" && IDEMPOTENCY_KEY.test(value);
689
887
  }
888
+ function validateUploadPath(workspaceId, uploadId, signalValue) {
889
+ const signal = validateSignal(signalValue);
890
+ if (typeof workspaceId !== "string" || !WORKSPACE_ID.test(workspaceId) || typeof uploadId !== "string" || !UPLOAD_ID.test(uploadId)) throw new DriveError("invalid_input");
891
+ return {
892
+ workspaceId,
893
+ uploadId,
894
+ ...signal === void 0 ? {} : { signal }
895
+ };
896
+ }
897
+ function isPartNumber(value) {
898
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 1 && value <= 1e4;
899
+ }
900
+ function isPartChecksum(value) {
901
+ return typeof value === "string" && PART_CHECKSUM.test(value);
902
+ }
903
+ function isEtag(value) {
904
+ return typeof value === "string" && value.length >= 1 && value.length <= 1024 && /^[\x20-\x7e]+$/.test(value);
905
+ }
906
+ function isMediaType(value) {
907
+ return typeof value === "string" && value.length <= 255 && /^[a-z0-9](?:[a-z0-9!#$&^_.+-]{0,125}[a-z0-9])?\/[a-z0-9](?:[a-z0-9!#$&^_.+-]{0,125}[a-z0-9])?$/.test(value);
908
+ }
690
909
  function isExpiry(value) {
691
910
  return value === null || isSafeInteger(value);
692
911
  }
@@ -1146,7 +1365,7 @@ function createClientRuntime(config) {
1146
1365
  if (typeof runtime.fetchImplementation !== "function" || typeof runtime.getCredential !== "function") throw new DriveError("invalid_input");
1147
1366
  return runtime;
1148
1367
  }
1149
- async function execute(runtime, signal, preconditionStatus, send, successStatuses, guard, allowsQuotaExceeded = false) {
1368
+ async function execute(runtime, signal, preconditionStatus, send, successStatuses, guard, allowsQuotaExceeded = false, allowedErrorCodes) {
1150
1369
  const operation = createOperationSignal(signal, runtime.timeoutMilliseconds);
1151
1370
  try {
1152
1371
  const operationSignal = operation.signal;
@@ -1173,7 +1392,7 @@ async function execute(runtime, signal, preconditionStatus, send, successStatuse
1173
1392
  signal: operationSignal
1174
1393
  });
1175
1394
  if (!capturedResponse) throw new DriveError("network_error");
1176
- return await parseResponse(capturedResponse, operationSignal, preconditionStatus, successStatuses, guard, allowsQuotaExceeded);
1395
+ return guard === null ? await parseNoContentResponse(capturedResponse, operationSignal, preconditionStatus, successStatuses, allowedErrorCodes) : await parseResponse(capturedResponse, operationSignal, preconditionStatus, successStatuses, guard, allowsQuotaExceeded, allowedErrorCodes);
1177
1396
  } catch (error) {
1178
1397
  if (operation.didTimeout()) throw new DriveError("timeout");
1179
1398
  if (error instanceof DriveError) throw error;
@@ -1253,9 +1472,45 @@ async function raceWithAbort(promise, signal) {
1253
1472
  }
1254
1473
  //#endregion
1255
1474
  //#region src/internal/client/operations.ts
1475
+ const CURRENT_WORKSPACE_ERROR_CODES = Object.freeze([
1476
+ "authentication_required",
1477
+ "identity_authority_unavailable",
1478
+ "permission_denied",
1479
+ "resource_not_found",
1480
+ "invalid_request",
1481
+ "service_unavailable"
1482
+ ]);
1483
+ const TRANSFER_ERROR_CODES = Object.freeze([
1484
+ "authentication_required",
1485
+ "identity_authority_unavailable",
1486
+ "permission_denied",
1487
+ "resource_not_found",
1488
+ "invalid_request",
1489
+ "precondition_failed",
1490
+ "upload_recovery_required",
1491
+ "service_unavailable"
1492
+ ]);
1493
+ const STATUS_ERROR_CODES = Object.freeze([
1494
+ "authentication_required",
1495
+ "identity_authority_unavailable",
1496
+ "permission_denied",
1497
+ "resource_not_found",
1498
+ "invalid_request",
1499
+ "service_unavailable"
1500
+ ]);
1501
+ const CREATE_UPLOAD_ERROR_CODES = Object.freeze([
1502
+ ...TRANSFER_ERROR_CODES,
1503
+ "invalid_idempotency_key",
1504
+ "idempotency_conflict",
1505
+ "quota_exceeded"
1506
+ ]);
1507
+ const COMPLETE_UPLOAD_ERROR_CODES = Object.freeze([...TRANSFER_ERROR_CODES, "invalid_idempotency_key"]);
1256
1508
  function createDriveClientInternal(config) {
1257
- const runtime = createClientRuntime(config);
1509
+ return createDriveOperations(createClientRuntime(config));
1510
+ }
1511
+ function createDriveOperations(runtime) {
1258
1512
  return Object.freeze({
1513
+ getCurrentWorkspace: (input) => getCurrentWorkspace(runtime, input),
1259
1514
  listFolderChildren: (input) => listFolderChildren(runtime, input),
1260
1515
  createFolder: (input) => createFolder(runtime, input),
1261
1516
  listItemPermissions: (input) => listItemPermissions(runtime, input),
@@ -1264,9 +1519,19 @@ function createDriveClientInternal(config) {
1264
1519
  updateItemPermission: (input) => updateItemPermission(runtime, input),
1265
1520
  revokeItemPermission: (input) => revokeItemPermission(runtime, input),
1266
1521
  createUpload: (input) => createUpload(runtime, input),
1522
+ getUploadStatus: (input) => getUploadStatus(runtime, input),
1523
+ createUploadPartDescriptor: (input) => createUploadPartDescriptor(runtime, input),
1524
+ recordUploadPart: (input) => recordUploadPart(runtime, input),
1525
+ completeUpload: (input) => completeUpload(runtime, input),
1267
1526
  abortUpload: (input) => abortUpload(runtime, input)
1268
1527
  });
1269
1528
  }
1529
+ async function getCurrentWorkspace(runtime, input) {
1530
+ return execute(runtime, validateCurrentWorkspaceInput(input).signal, null, (api, requestOptions) => api.GET("/api/v1/workspaces/current", {
1531
+ ...operationOptions("getCurrentWorkspace", { params: {} }),
1532
+ ...requestOptions
1533
+ }), [200], isCurrentWorkspace, false, CURRENT_WORKSPACE_ERROR_CODES);
1534
+ }
1270
1535
  async function listFolderChildren(runtime, input) {
1271
1536
  const validated = validatePageInput(input);
1272
1537
  return execute(runtime, validated.signal, 409, (api, requestOptions) => api.GET("/api/v1/workspaces/{workspaceId}/items/{itemId}/children", {
@@ -1375,11 +1640,81 @@ async function createUpload(runtime, input) {
1375
1640
  },
1376
1641
  body: {
1377
1642
  parentItemId: validated.parentItemId,
1378
- expectedByteLength: validated.expectedByteLength
1643
+ expectedByteLength: validated.expectedByteLength,
1644
+ displayFilename: validated.displayFilename,
1645
+ mediaType: validated.mediaType,
1646
+ callerDeclaredContentDigest: validated.callerDeclaredContentDigest,
1647
+ protectionClassification: validated.protectionClassification
1379
1648
  }
1380
1649
  }),
1381
1650
  ...requestOptions
1382
- }), [200, 201], (value) => isUploadQuotaReservation(value) && value.workspaceId === validated.workspaceId && value.parentItemId === validated.parentItemId && value.expectedByteLength === validated.expectedByteLength && value.state === "reserved" && value.terminalReason === null && value.terminalAtEpochMilliseconds === null, true);
1651
+ }), [200, 201], (value) => isUploadSession(value, validated.expectedByteLength), true, CREATE_UPLOAD_ERROR_CODES);
1652
+ }
1653
+ async function getUploadStatus(runtime, input) {
1654
+ const validated = validateGetUploadStatusInput(input);
1655
+ const afterPartNumber = validated.afterPartNumber ?? 0;
1656
+ const limit = validated.limit ?? 100;
1657
+ return execute(runtime, validated.signal, null, (api, requestOptions) => api.GET("/api/v1/workspaces/{workspaceId}/uploads/{uploadId}", {
1658
+ ...operationOptions("getUploadStatus", { params: {
1659
+ path: {
1660
+ workspaceId: validated.workspaceId,
1661
+ uploadId: validated.uploadId
1662
+ },
1663
+ query: {
1664
+ ...validated.afterPartNumber === void 0 ? {} : { afterPartNumber: String(validated.afterPartNumber) },
1665
+ ...validated.limit === void 0 ? {} : { limit: String(validated.limit) }
1666
+ }
1667
+ } }),
1668
+ ...requestOptions
1669
+ }), [200], (value) => isUploadStatus(value, afterPartNumber, limit), false, STATUS_ERROR_CODES);
1670
+ }
1671
+ async function createUploadPartDescriptor(runtime, input) {
1672
+ const validated = validateCreateUploadPartDescriptorInput(input);
1673
+ return execute(runtime, validated.signal, 409, (api, requestOptions) => api.POST("/api/v1/workspaces/{workspaceId}/uploads/{uploadId}/parts", {
1674
+ ...operationOptions("createUploadPartDescriptor", {
1675
+ params: { path: {
1676
+ workspaceId: validated.workspaceId,
1677
+ uploadId: validated.uploadId
1678
+ } },
1679
+ body: {
1680
+ partNumber: validated.partNumber,
1681
+ checksumSha256: validated.checksumSha256
1682
+ }
1683
+ }),
1684
+ ...requestOptions
1685
+ }), [200], (value) => isUploadPartDescriptor(value, validated.partNumber, validated.checksumSha256, new URL(runtime.baseUrl).protocol === "http:"), false, TRANSFER_ERROR_CODES);
1686
+ }
1687
+ async function recordUploadPart(runtime, input) {
1688
+ const validated = validateRecordUploadPartInput(input);
1689
+ return execute(runtime, validated.signal, 409, (api, requestOptions) => api.POST("/api/v1/workspaces/{workspaceId}/uploads/{uploadId}/parts/{partNumber}", {
1690
+ ...operationOptions("recordUploadPart", {
1691
+ params: { path: {
1692
+ workspaceId: validated.workspaceId,
1693
+ uploadId: validated.uploadId,
1694
+ partNumber: String(validated.partNumber)
1695
+ } },
1696
+ body: {
1697
+ expectedUploadRevision: validated.expectedUploadRevision,
1698
+ byteLength: validated.byteLength,
1699
+ etag: validated.etag,
1700
+ checksumSha256: validated.checksumSha256
1701
+ }
1702
+ }),
1703
+ ...requestOptions
1704
+ }), [200], (value) => isUploadPartResult(value, validated.partNumber, validated.expectedUploadRevision), false, TRANSFER_ERROR_CODES);
1705
+ }
1706
+ async function completeUpload(runtime, input) {
1707
+ const validated = validateCompleteUploadInput(input);
1708
+ return execute(runtime, validated.signal, 409, (api, requestOptions) => api.POST("/api/v1/workspaces/{workspaceId}/uploads/{uploadId}/complete", {
1709
+ ...operationOptions("completeUpload", { params: {
1710
+ path: {
1711
+ workspaceId: validated.workspaceId,
1712
+ uploadId: validated.uploadId
1713
+ },
1714
+ header: { "Idempotency-Key": validated.idempotencyKey }
1715
+ } }),
1716
+ ...requestOptions
1717
+ }), [200], (value) => isReadyUpload(value, { uploadId: validated.uploadId }), false, COMPLETE_UPLOAD_ERROR_CODES);
1383
1718
  }
1384
1719
  async function abortUpload(runtime, input) {
1385
1720
  const validated = validateAbortUploadInput(input);
@@ -1389,7 +1724,7 @@ async function abortUpload(runtime, input) {
1389
1724
  uploadId: validated.uploadId
1390
1725
  } } }),
1391
1726
  ...requestOptions
1392
- }), [200], (value) => isUploadQuotaReservation(value) && value.workspaceId === validated.workspaceId && value.uploadId === validated.uploadId && value.state === "released" && value.terminalReason === "aborted" && value.terminalAtEpochMilliseconds !== null);
1727
+ }), [204], null, false, TRANSFER_ERROR_CODES);
1393
1728
  }
1394
1729
  //#endregion
1395
1730
  //#region src/server.ts
package/dist/types.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { C as PermissionPage, D as UpdateItemPermissionInput, E as RevokeItemPermissionInput, O as UploadQuotaReservation, S as PermissionMutation, T as ReservedUploadQuota, _ as ListFolderChildrenInput, a as CreateUploadInput, b as PermissionGrant, c as DriveCredentialContext, d as DriveResponseValidators, f as DriveServerClientConfig, g as FolderPage, h as FolderItem, i as CreateItemPermissionInput, l as DriveCredentialProvider, m as EffectivePermissionResponse, n as AbortedUploadQuota, o as DriveClient, p as EffectivePermissionAction, r as CreateFolderInput, s as DriveClientConfig, t as AbortUploadInput, u as DriveOperationResult, v as ListItemPermissionsInput, w as ReadEffectiveItemPermissionsInput, x as PermissionGrantee, y as PermissionAction } from "./contract.js";
2
- export { AbortUploadInput, AbortedUploadQuota, CreateFolderInput, CreateItemPermissionInput, CreateUploadInput, DriveClient, DriveClientConfig, DriveCredentialContext, DriveCredentialProvider, DriveOperationResult, DriveResponseValidators, DriveServerClientConfig, EffectivePermissionAction, EffectivePermissionResponse, FolderItem, FolderPage, ListFolderChildrenInput, ListItemPermissionsInput, PermissionAction, PermissionGrant, PermissionGrantee, PermissionMutation, PermissionPage, ReadEffectiveItemPermissionsInput, ReservedUploadQuota, RevokeItemPermissionInput, UpdateItemPermissionInput, UploadQuotaReservation };
1
+ import { A as ProtectionClassification, B as UploadFileProgressCallback, C as ListFolderChildrenInput, D as PermissionGrantee, E as PermissionGrant, F as UpdateItemPermissionInput, H as UploadPartResult, I as UploadConfirmedPart, L as UploadFileInput, M as ReadyUpload, N as RecordUploadPartInput, O as PermissionMutation, P as RevokeItemPermissionInput, R as UploadFilePhase, S as GetUploadStatusInput, T as PermissionAction, U as UploadSession, V as UploadPartDescriptor, W as UploadStatus, _ as EffectivePermissionAction, a as CreateUploadInput, b as FolderPage, c as DriveClient, d as DriveCredentialProvider, f as DriveOperationResult, g as DriveServerClientConfig, h as DriveServerClient, i as CreateItemPermissionInput, j as ReadEffectiveItemPermissionsInput, k as PermissionPage, l as DriveClientConfig, m as DriveResponseValidators, n as CompleteUploadInput, o as CreateUploadPartDescriptorInput, p as DriveOperations, r as CreateFolderInput, s as CurrentWorkspace, t as AbortUploadInput, u as DriveCredentialContext, v as EffectivePermissionResponse, w as ListItemPermissionsInput, x as GetCurrentWorkspaceInput, y as FolderItem, z as UploadFileProgress } from "./contract.js";
2
+ export { AbortUploadInput, CompleteUploadInput, CreateFolderInput, CreateItemPermissionInput, CreateUploadInput, CreateUploadPartDescriptorInput, CurrentWorkspace, DriveClient, DriveClientConfig, DriveCredentialContext, DriveCredentialProvider, DriveOperationResult, DriveOperations, DriveResponseValidators, DriveServerClient, DriveServerClientConfig, EffectivePermissionAction, EffectivePermissionResponse, FolderItem, FolderPage, GetCurrentWorkspaceInput, GetUploadStatusInput, ListFolderChildrenInput, ListItemPermissionsInput, PermissionAction, PermissionGrant, PermissionGrantee, PermissionMutation, PermissionPage, ProtectionClassification, ReadEffectiveItemPermissionsInput, ReadyUpload, RecordUploadPartInput, RevokeItemPermissionInput, UpdateItemPermissionInput, UploadConfirmedPart, UploadFileInput, UploadFilePhase, UploadFileProgress, UploadFileProgressCallback, UploadPartDescriptor, UploadPartResult, UploadSession, UploadStatus };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finueva/drive",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Public browser, Worker, and Node client for Finueva Drive",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
@@ -52,9 +52,10 @@
52
52
  "generate": "node scripts/openapi/generate.mjs",
53
53
  "check:generated": "node scripts/openapi/generate.mjs verify",
54
54
  "prepack": "pnpm build",
55
- "test": "node scripts/quality/source.mjs && pnpm check:generated && pnpm test:package",
55
+ "test": "node scripts/quality/source.mjs && pnpm test:unit && pnpm check:generated && pnpm test:package",
56
56
  "test:artifact": "node scripts/package/artifact.mjs",
57
57
  "test:package": "node scripts/package/qualify.mjs",
58
+ "test:unit": "node scripts/quality/unit.mjs",
58
59
  "typecheck": "tsc --noEmit"
59
60
  },
60
61
  "devDependencies": {
@@ -68,5 +69,8 @@
68
69
  "rolldown-plugin-dts": "0.27.14",
69
70
  "typescript": "5.9.3",
70
71
  "ufo": "1.6.4"
72
+ },
73
+ "dependencies": {
74
+ "@noble/hashes": "2.3.0"
71
75
  }
72
76
  }