@drawbridge/drawbridge-utils 0.0.100 → 0.0.102

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/features.cjs CHANGED
@@ -68,7 +68,7 @@ var field = {
68
68
  feature: "Campaign name field"
69
69
  },
70
70
  number: {
71
- key: "campaign:field:name",
71
+ key: "campaign:field:number",
72
72
  error: "Plan does not include number field",
73
73
  feature: "Campaign number field"
74
74
  },
@@ -41,7 +41,7 @@ const field = {
41
41
  feature : 'Campaign name field',
42
42
  },
43
43
  number : {
44
- key : 'campaign:field:name',
44
+ key : 'campaign:field:number',
45
45
  error : 'Plan does not include number field',
46
46
  feature : 'Campaign number field',
47
47
  },
@@ -41,7 +41,7 @@ const field = {
41
41
  feature : 'Campaign name field',
42
42
  },
43
43
  number : {
44
- key : 'campaign:field:name',
44
+ key : 'campaign:field:number',
45
45
  error : 'Plan does not include number field',
46
46
  feature : 'Campaign number field',
47
47
  },
package/dist/features.js CHANGED
@@ -40,7 +40,7 @@ var field = {
40
40
  feature: "Campaign name field"
41
41
  },
42
42
  number: {
43
- key: "campaign:field:name",
43
+ key: "campaign:field:number",
44
44
  error: "Plan does not include number field",
45
45
  feature: "Campaign number field"
46
46
  },
package/dist/fetch.cjs CHANGED
@@ -32,7 +32,8 @@ __export(fetch_exports, {
32
32
  isTransientError: () => isTransientError,
33
33
  queryString: () => queryString,
34
34
  request: () => request,
35
- setMemoryToken: () => setMemoryToken
35
+ setMemoryToken: () => setMemoryToken,
36
+ upload: () => upload
36
37
  });
37
38
  module.exports = __toCommonJS(fetch_exports);
38
39
  var import_qs = __toESM(require("qs"), 1);
@@ -122,6 +123,59 @@ var request = async ({
122
123
  }
123
124
  return res;
124
125
  };
126
+ var upload = ({
127
+ body,
128
+ endpoint,
129
+ method = "POST",
130
+ onProgress,
131
+ signal,
132
+ token,
133
+ url = process.env.NEXT_PUBLIC_API_URI
134
+ }) => {
135
+ const authToken = token || (typeof window !== "undefined" ? memoryToken : null);
136
+ const csrf = readCsrf();
137
+ return new Promise((resolve, reject) => {
138
+ const xhr = new XMLHttpRequest();
139
+ xhr.open(method, url + endpoint, true);
140
+ xhr.setRequestHeader("accept", "application/json");
141
+ if (authToken) {
142
+ xhr.setRequestHeader("authorization", "Bearer " + authToken);
143
+ }
144
+ if (csrf) {
145
+ xhr.setRequestHeader("x-drawbridge-csrf", csrf);
146
+ }
147
+ xhr.upload.addEventListener("progress", (event) => {
148
+ if (!event.lengthComputable || !onProgress) return;
149
+ onProgress({
150
+ bytesTotal: event.total,
151
+ bytesUploaded: event.loaded
152
+ });
153
+ });
154
+ xhr.addEventListener("load", () => {
155
+ let data = null;
156
+ try {
157
+ data = JSON.parse(xhr.responseText);
158
+ } catch {
159
+ data = null;
160
+ }
161
+ if (xhr.status >= 200 && xhr.status < 300) return resolve(data);
162
+ const error = new Error((data == null ? void 0 : data.message) || xhr.statusText);
163
+ error.status = xhr.status;
164
+ error.data = data;
165
+ reject(error);
166
+ });
167
+ xhr.addEventListener("abort", () => {
168
+ const error = new Error("Upload cancelled");
169
+ error.name = "AbortError";
170
+ reject(error);
171
+ });
172
+ xhr.addEventListener("error", () => reject(new Error("Network error")));
173
+ if (signal) {
174
+ signal.addEventListener("abort", () => xhr.abort(), { once: true });
175
+ }
176
+ xhr.send(body);
177
+ });
178
+ };
125
179
  var isTransientError = (error) => {
126
180
  const status = error == null ? void 0 : error.status;
127
181
  return !status || status === 429 || status >= 500;
@@ -131,5 +185,6 @@ var isTransientError = (error) => {
131
185
  isTransientError,
132
186
  queryString,
133
187
  request,
134
- setMemoryToken
188
+ setMemoryToken,
189
+ upload
135
190
  });
package/dist/fetch.d.cts CHANGED
@@ -124,6 +124,118 @@ const request = async ({
124
124
 
125
125
  };
126
126
 
127
+ // fetch() has no upload-progress event and never will, which is the reason every
128
+ // upload in the dashboard could only ever show a spinner. XHR does, so anything
129
+ // that wants a real progress bar posts through here instead.
130
+ //
131
+ // It lives beside request() rather than in the dashboard because `memoryToken`
132
+ // above is module-private on purpose — a copy in app-web could only read it if
133
+ // the token were exported, which is the one thing setMemoryToken exists to
134
+ // prevent. The csrf cookie read is here too.
135
+ //
136
+ // The error shape is deliberately identical to request()'s — an Error carrying
137
+ // .status and .data — so isTransientError() and every existing catch block work
138
+ // against either transport without knowing which ran.
139
+ const upload = ({
140
+ body,
141
+ endpoint,
142
+ method = 'POST',
143
+ onProgress,
144
+ signal,
145
+ token,
146
+ url = process.env.NEXT_PUBLIC_API_URI
147
+ }) => {
148
+
149
+ const authToken = token || ( typeof window !== 'undefined' ? memoryToken : null );
150
+ const csrf = readCsrf();
151
+
152
+ return new Promise( ( resolve, reject ) => {
153
+
154
+ const xhr = new XMLHttpRequest();
155
+
156
+ xhr.open( method, url + endpoint, true );
157
+
158
+ // No content-type, ever. The browser has to write the multipart boundary
159
+ // itself and setting the header by hand drops it, which busboy then
160
+ // rejects. Nor origin: it is a forbidden header name, so the browser
161
+ // ignores setRequestHeader for it ( request() only sets it because its
162
+ // server-side callers run where fetch allows it ).
163
+ xhr.setRequestHeader( 'accept', 'application/json' );
164
+
165
+ if( authToken ){
166
+
167
+ xhr.setRequestHeader( 'authorization', 'Bearer ' + authToken );
168
+
169
+ }
170
+
171
+ if( csrf ){
172
+
173
+ xhr.setRequestHeader( 'x-drawbridge-csrf', csrf );
174
+
175
+ }
176
+
177
+ xhr.upload.addEventListener( 'progress', ( event ) => {
178
+
179
+ if( ! event.lengthComputable || ! onProgress ) return;
180
+
181
+ onProgress({
182
+ bytesTotal : event.total,
183
+ bytesUploaded : event.loaded
184
+ });
185
+
186
+ });
187
+
188
+ xhr.addEventListener( 'load', () => {
189
+
190
+ let data = null;
191
+
192
+ try {
193
+
194
+ data = JSON.parse( xhr.responseText );
195
+
196
+ } catch {
197
+
198
+ data = null;
199
+
200
+ }
201
+
202
+ if( xhr.status >= 200 && xhr.status < 300 ) return resolve( data );
203
+
204
+ const error = new Error( data?.message || xhr.statusText );
205
+
206
+ error.status = xhr.status;
207
+ error.data = data;
208
+
209
+ reject( error );
210
+
211
+ });
212
+
213
+ xhr.addEventListener( 'abort', () => {
214
+
215
+ const error = new Error( 'Upload cancelled' );
216
+
217
+ error.name = 'AbortError';
218
+
219
+ reject( error );
220
+
221
+ });
222
+
223
+ // No status on a network failure, which is exactly how isTransientError
224
+ // classifies it — worth a retry rather than a sign-in bounce.
225
+ xhr.addEventListener( 'error', () => reject( new Error( 'Network error' ) ) );
226
+
227
+ if( signal ){
228
+
229
+ signal.addEventListener( 'abort', () => xhr.abort(), { once : true } );
230
+
231
+ }
232
+
233
+ xhr.send( body );
234
+
235
+ });
236
+
237
+ };
238
+
127
239
  // Classifies an error thrown by request() — which carries `.status` from the
128
240
  // HTTP response, or no status for a network / parse failure. Transient errors
129
241
  // ( rate limits, server errors, connection blips ) may succeed on retry, so
@@ -139,4 +251,4 @@ const isTransientError = ( error ) => {
139
251
 
140
252
  };
141
253
 
142
- export { isTransientError, queryString, request, setMemoryToken };
254
+ export { isTransientError, queryString, request, setMemoryToken, upload };
package/dist/fetch.d.ts CHANGED
@@ -124,6 +124,118 @@ const request = async ({
124
124
 
125
125
  };
126
126
 
127
+ // fetch() has no upload-progress event and never will, which is the reason every
128
+ // upload in the dashboard could only ever show a spinner. XHR does, so anything
129
+ // that wants a real progress bar posts through here instead.
130
+ //
131
+ // It lives beside request() rather than in the dashboard because `memoryToken`
132
+ // above is module-private on purpose — a copy in app-web could only read it if
133
+ // the token were exported, which is the one thing setMemoryToken exists to
134
+ // prevent. The csrf cookie read is here too.
135
+ //
136
+ // The error shape is deliberately identical to request()'s — an Error carrying
137
+ // .status and .data — so isTransientError() and every existing catch block work
138
+ // against either transport without knowing which ran.
139
+ const upload = ({
140
+ body,
141
+ endpoint,
142
+ method = 'POST',
143
+ onProgress,
144
+ signal,
145
+ token,
146
+ url = process.env.NEXT_PUBLIC_API_URI
147
+ }) => {
148
+
149
+ const authToken = token || ( typeof window !== 'undefined' ? memoryToken : null );
150
+ const csrf = readCsrf();
151
+
152
+ return new Promise( ( resolve, reject ) => {
153
+
154
+ const xhr = new XMLHttpRequest();
155
+
156
+ xhr.open( method, url + endpoint, true );
157
+
158
+ // No content-type, ever. The browser has to write the multipart boundary
159
+ // itself and setting the header by hand drops it, which busboy then
160
+ // rejects. Nor origin: it is a forbidden header name, so the browser
161
+ // ignores setRequestHeader for it ( request() only sets it because its
162
+ // server-side callers run where fetch allows it ).
163
+ xhr.setRequestHeader( 'accept', 'application/json' );
164
+
165
+ if( authToken ){
166
+
167
+ xhr.setRequestHeader( 'authorization', 'Bearer ' + authToken );
168
+
169
+ }
170
+
171
+ if( csrf ){
172
+
173
+ xhr.setRequestHeader( 'x-drawbridge-csrf', csrf );
174
+
175
+ }
176
+
177
+ xhr.upload.addEventListener( 'progress', ( event ) => {
178
+
179
+ if( ! event.lengthComputable || ! onProgress ) return;
180
+
181
+ onProgress({
182
+ bytesTotal : event.total,
183
+ bytesUploaded : event.loaded
184
+ });
185
+
186
+ });
187
+
188
+ xhr.addEventListener( 'load', () => {
189
+
190
+ let data = null;
191
+
192
+ try {
193
+
194
+ data = JSON.parse( xhr.responseText );
195
+
196
+ } catch {
197
+
198
+ data = null;
199
+
200
+ }
201
+
202
+ if( xhr.status >= 200 && xhr.status < 300 ) return resolve( data );
203
+
204
+ const error = new Error( data?.message || xhr.statusText );
205
+
206
+ error.status = xhr.status;
207
+ error.data = data;
208
+
209
+ reject( error );
210
+
211
+ });
212
+
213
+ xhr.addEventListener( 'abort', () => {
214
+
215
+ const error = new Error( 'Upload cancelled' );
216
+
217
+ error.name = 'AbortError';
218
+
219
+ reject( error );
220
+
221
+ });
222
+
223
+ // No status on a network failure, which is exactly how isTransientError
224
+ // classifies it — worth a retry rather than a sign-in bounce.
225
+ xhr.addEventListener( 'error', () => reject( new Error( 'Network error' ) ) );
226
+
227
+ if( signal ){
228
+
229
+ signal.addEventListener( 'abort', () => xhr.abort(), { once : true } );
230
+
231
+ }
232
+
233
+ xhr.send( body );
234
+
235
+ });
236
+
237
+ };
238
+
127
239
  // Classifies an error thrown by request() — which carries `.status` from the
128
240
  // HTTP response, or no status for a network / parse failure. Transient errors
129
241
  // ( rate limits, server errors, connection blips ) may succeed on retry, so
@@ -139,4 +251,4 @@ const isTransientError = ( error ) => {
139
251
 
140
252
  };
141
253
 
142
- export { isTransientError, queryString, request, setMemoryToken };
254
+ export { isTransientError, queryString, request, setMemoryToken, upload };
package/dist/fetch.js CHANGED
@@ -86,6 +86,59 @@ var request = async ({
86
86
  }
87
87
  return res;
88
88
  };
89
+ var upload = ({
90
+ body,
91
+ endpoint,
92
+ method = "POST",
93
+ onProgress,
94
+ signal,
95
+ token,
96
+ url = process.env.NEXT_PUBLIC_API_URI
97
+ }) => {
98
+ const authToken = token || (typeof window !== "undefined" ? memoryToken : null);
99
+ const csrf = readCsrf();
100
+ return new Promise((resolve, reject) => {
101
+ const xhr = new XMLHttpRequest();
102
+ xhr.open(method, url + endpoint, true);
103
+ xhr.setRequestHeader("accept", "application/json");
104
+ if (authToken) {
105
+ xhr.setRequestHeader("authorization", "Bearer " + authToken);
106
+ }
107
+ if (csrf) {
108
+ xhr.setRequestHeader("x-drawbridge-csrf", csrf);
109
+ }
110
+ xhr.upload.addEventListener("progress", (event) => {
111
+ if (!event.lengthComputable || !onProgress) return;
112
+ onProgress({
113
+ bytesTotal: event.total,
114
+ bytesUploaded: event.loaded
115
+ });
116
+ });
117
+ xhr.addEventListener("load", () => {
118
+ let data = null;
119
+ try {
120
+ data = JSON.parse(xhr.responseText);
121
+ } catch {
122
+ data = null;
123
+ }
124
+ if (xhr.status >= 200 && xhr.status < 300) return resolve(data);
125
+ const error = new Error((data == null ? void 0 : data.message) || xhr.statusText);
126
+ error.status = xhr.status;
127
+ error.data = data;
128
+ reject(error);
129
+ });
130
+ xhr.addEventListener("abort", () => {
131
+ const error = new Error("Upload cancelled");
132
+ error.name = "AbortError";
133
+ reject(error);
134
+ });
135
+ xhr.addEventListener("error", () => reject(new Error("Network error")));
136
+ if (signal) {
137
+ signal.addEventListener("abort", () => xhr.abort(), { once: true });
138
+ }
139
+ xhr.send(body);
140
+ });
141
+ };
89
142
  var isTransientError = (error) => {
90
143
  const status = error == null ? void 0 : error.status;
91
144
  return !status || status === 429 || status >= 500;
@@ -94,5 +147,6 @@ export {
94
147
  isTransientError,
95
148
  queryString,
96
149
  request,
97
- setMemoryToken
150
+ setMemoryToken,
151
+ upload
98
152
  };
package/dist/plans.cjs CHANGED
@@ -78,7 +78,7 @@ var field = {
78
78
  feature: "Campaign name field"
79
79
  },
80
80
  number: {
81
- key: "campaign:field:name",
81
+ key: "campaign:field:number",
82
82
  error: "Plan does not include number field",
83
83
  feature: "Campaign number field"
84
84
  },
package/dist/plans.js CHANGED
@@ -40,7 +40,7 @@ var field = {
40
40
  feature: "Campaign name field"
41
41
  },
42
42
  number: {
43
- key: "campaign:field:name",
43
+ key: "campaign:field:number",
44
44
  error: "Plan does not include number field",
45
45
  feature: "Campaign number field"
46
46
  },
package/dist/upload.cjs CHANGED
@@ -22,7 +22,10 @@ __export(upload_exports, {
22
22
  allowedMimes: () => allowedMimes,
23
23
  allowedUploadTypes: () => allowedUploadTypes,
24
24
  isAllowedMime: () => isAllowedMime,
25
- resolveUploadType: () => resolveUploadType
25
+ maximumUploadBytes: () => maximumUploadBytes,
26
+ resolveUploadType: () => resolveUploadType,
27
+ uploadPartCount: () => uploadPartCount,
28
+ uploadPartSize: () => uploadPartSize
26
29
  });
27
30
  module.exports = __toCommonJS(upload_exports);
28
31
  var allowedUploadTypes = {
@@ -36,6 +39,11 @@ var allowedUploadTypes = {
36
39
  webp: "image/webp"
37
40
  },
38
41
  video: {
42
+ // iPhones record .mov (HEVC) by default, so this is the single most
43
+ // likely video a merchant uploads. sync's storage.js already computes
44
+ // needsReencode for hevc/h265 and re-encodes to h264, so allowing it
45
+ // here is the only thing the pipeline was waiting on.
46
+ mov: "video/quicktime",
39
47
  mp4: "video/mp4"
40
48
  }
41
49
  };
@@ -56,10 +64,27 @@ var resolveUploadType = (type, extension) => {
56
64
  return mime;
57
65
  };
58
66
  var isAllowedMime = (mime) => allowedMimes.has(mime);
67
+ var megabyte = 1024 * 1024;
68
+ var maximumUploadBytes = 2048 * megabyte;
69
+ var maximumParts = 9e3;
70
+ var minimumPartSize = 10 * megabyte;
71
+ var uploadPartSize = (bytes) => {
72
+ const size = Number(bytes) || 0;
73
+ const required = Math.ceil(size / maximumParts);
74
+ const rounded = Math.ceil(required / megabyte) * megabyte;
75
+ return Math.max(minimumPartSize, rounded);
76
+ };
77
+ var uploadPartCount = (bytes) => {
78
+ const size = Number(bytes) || 0;
79
+ return Math.max(1, Math.ceil(size / uploadPartSize(size)));
80
+ };
59
81
  // Annotate the CommonJS export names for ESM import in node:
60
82
  0 && (module.exports = {
61
83
  allowedMimes,
62
84
  allowedUploadTypes,
63
85
  isAllowedMime,
64
- resolveUploadType
86
+ maximumUploadBytes,
87
+ resolveUploadType,
88
+ uploadPartCount,
89
+ uploadPartSize
65
90
  });
package/dist/upload.d.cts CHANGED
@@ -15,6 +15,12 @@
15
15
  //
16
16
  // Why both helpers and not just one: api routes know (type, extension) but not
17
17
  // the canonical mime; sync workers have the mime but not the original type tuple.
18
+ //
19
+ // A third consumer joined with presigned multipart uploads: the dashboard reads
20
+ // this same object to build its file picker's accept attribute and the uploader's
21
+ // client-side restrictions, so a wrong type is refused before two gigabytes move
22
+ // rather than after. That is the whole reason the list is not copied into
23
+ // app-web — four views of one list can't drift.
18
24
 
19
25
  const allowedUploadTypes = {
20
26
  application : {
@@ -27,6 +33,11 @@ const allowedUploadTypes = {
27
33
  webp : 'image/webp'
28
34
  },
29
35
  video : {
36
+ // iPhones record .mov (HEVC) by default, so this is the single most
37
+ // likely video a merchant uploads. sync's storage.js already computes
38
+ // needsReencode for hevc/h265 and re-encodes to h264, so allowing it
39
+ // here is the only thing the pipeline was waiting on.
40
+ mov : 'video/quicktime',
30
41
  mp4 : 'video/mp4'
31
42
  }
32
43
  };
@@ -55,4 +66,51 @@ const resolveUploadType = ( type, extension ) => {
55
66
 
56
67
  const isAllowedMime = ( mime ) => allowedMimes.has( mime );
57
68
 
58
- export { allowedMimes, allowedUploadTypes, isAllowedMime, resolveUploadType };
69
+ const megabyte = ( 1024 * 1024 );
70
+
71
+ // The ceiling for a presigned multipart upload. Video streams browser-to-bucket
72
+ // on that path rather than through the api, so this is a product decision
73
+ // rather than the fleet-protection one that keeps the single-request busboy
74
+ // route at 50MB.
75
+ //
76
+ // It lives here because both sides have to agree on it exactly: the dashboard
77
+ // rejects before transferring and the api rejects authoritatively, and if the
78
+ // two numbers drift the client blocks files the api would happily accept.
79
+ const maximumUploadBytes = ( 2048 * megabyte );
80
+
81
+ // S3 caps a multipart upload at 10,000 parts and requires every part except the
82
+ // last to be at least 5MB. A fixed part size therefore puts a silent ceiling on
83
+ // the maximum file, so the size is derived from the declared length with
84
+ // headroom against the hard limit.
85
+ //
86
+ // The 10MB floor is a request-volume decision, not an S3 one: a 2GB upload at
87
+ // 5MB parts is 410 signing calls, at 10MB it is 205.
88
+ //
89
+ // Both the api and the dashboard call this, and the dashboard *must* — Uppy
90
+ // builds its chunk list in the MultipartUploader constructor, which runs before
91
+ // createMultipartUpload is ever called, so a part size returned from the create
92
+ // endpoint arrives too late to chunk with. One function, two callers, no
93
+ // possibility of disagreeing about where the part boundaries fall.
94
+ const maximumParts = 9000;
95
+ const minimumPartSize = ( 10 * megabyte );
96
+
97
+ const uploadPartSize = ( bytes ) => {
98
+
99
+ const size = Number( bytes ) || 0;
100
+ const required = Math.ceil( size / maximumParts );
101
+ const rounded = Math.ceil( required / megabyte ) * megabyte;
102
+
103
+ return Math.max( minimumPartSize, rounded );
104
+
105
+ };
106
+
107
+ // An empty upload is still one part — S3 has no zero-part upload.
108
+ const uploadPartCount = ( bytes ) => {
109
+
110
+ const size = Number( bytes ) || 0;
111
+
112
+ return Math.max( 1, Math.ceil( size / uploadPartSize( size ) ) );
113
+
114
+ };
115
+
116
+ export { allowedMimes, allowedUploadTypes, isAllowedMime, maximumUploadBytes, resolveUploadType, uploadPartCount, uploadPartSize };
package/dist/upload.d.ts CHANGED
@@ -15,6 +15,12 @@
15
15
  //
16
16
  // Why both helpers and not just one: api routes know (type, extension) but not
17
17
  // the canonical mime; sync workers have the mime but not the original type tuple.
18
+ //
19
+ // A third consumer joined with presigned multipart uploads: the dashboard reads
20
+ // this same object to build its file picker's accept attribute and the uploader's
21
+ // client-side restrictions, so a wrong type is refused before two gigabytes move
22
+ // rather than after. That is the whole reason the list is not copied into
23
+ // app-web — four views of one list can't drift.
18
24
 
19
25
  const allowedUploadTypes = {
20
26
  application : {
@@ -27,6 +33,11 @@ const allowedUploadTypes = {
27
33
  webp : 'image/webp'
28
34
  },
29
35
  video : {
36
+ // iPhones record .mov (HEVC) by default, so this is the single most
37
+ // likely video a merchant uploads. sync's storage.js already computes
38
+ // needsReencode for hevc/h265 and re-encodes to h264, so allowing it
39
+ // here is the only thing the pipeline was waiting on.
40
+ mov : 'video/quicktime',
30
41
  mp4 : 'video/mp4'
31
42
  }
32
43
  };
@@ -55,4 +66,51 @@ const resolveUploadType = ( type, extension ) => {
55
66
 
56
67
  const isAllowedMime = ( mime ) => allowedMimes.has( mime );
57
68
 
58
- export { allowedMimes, allowedUploadTypes, isAllowedMime, resolveUploadType };
69
+ const megabyte = ( 1024 * 1024 );
70
+
71
+ // The ceiling for a presigned multipart upload. Video streams browser-to-bucket
72
+ // on that path rather than through the api, so this is a product decision
73
+ // rather than the fleet-protection one that keeps the single-request busboy
74
+ // route at 50MB.
75
+ //
76
+ // It lives here because both sides have to agree on it exactly: the dashboard
77
+ // rejects before transferring and the api rejects authoritatively, and if the
78
+ // two numbers drift the client blocks files the api would happily accept.
79
+ const maximumUploadBytes = ( 2048 * megabyte );
80
+
81
+ // S3 caps a multipart upload at 10,000 parts and requires every part except the
82
+ // last to be at least 5MB. A fixed part size therefore puts a silent ceiling on
83
+ // the maximum file, so the size is derived from the declared length with
84
+ // headroom against the hard limit.
85
+ //
86
+ // The 10MB floor is a request-volume decision, not an S3 one: a 2GB upload at
87
+ // 5MB parts is 410 signing calls, at 10MB it is 205.
88
+ //
89
+ // Both the api and the dashboard call this, and the dashboard *must* — Uppy
90
+ // builds its chunk list in the MultipartUploader constructor, which runs before
91
+ // createMultipartUpload is ever called, so a part size returned from the create
92
+ // endpoint arrives too late to chunk with. One function, two callers, no
93
+ // possibility of disagreeing about where the part boundaries fall.
94
+ const maximumParts = 9000;
95
+ const minimumPartSize = ( 10 * megabyte );
96
+
97
+ const uploadPartSize = ( bytes ) => {
98
+
99
+ const size = Number( bytes ) || 0;
100
+ const required = Math.ceil( size / maximumParts );
101
+ const rounded = Math.ceil( required / megabyte ) * megabyte;
102
+
103
+ return Math.max( minimumPartSize, rounded );
104
+
105
+ };
106
+
107
+ // An empty upload is still one part — S3 has no zero-part upload.
108
+ const uploadPartCount = ( bytes ) => {
109
+
110
+ const size = Number( bytes ) || 0;
111
+
112
+ return Math.max( 1, Math.ceil( size / uploadPartSize( size ) ) );
113
+
114
+ };
115
+
116
+ export { allowedMimes, allowedUploadTypes, isAllowedMime, maximumUploadBytes, resolveUploadType, uploadPartCount, uploadPartSize };
package/dist/upload.js CHANGED
@@ -10,6 +10,11 @@ var allowedUploadTypes = {
10
10
  webp: "image/webp"
11
11
  },
12
12
  video: {
13
+ // iPhones record .mov (HEVC) by default, so this is the single most
14
+ // likely video a merchant uploads. sync's storage.js already computes
15
+ // needsReencode for hevc/h265 and re-encodes to h264, so allowing it
16
+ // here is the only thing the pipeline was waiting on.
17
+ mov: "video/quicktime",
13
18
  mp4: "video/mp4"
14
19
  }
15
20
  };
@@ -30,9 +35,26 @@ var resolveUploadType = (type, extension) => {
30
35
  return mime;
31
36
  };
32
37
  var isAllowedMime = (mime) => allowedMimes.has(mime);
38
+ var megabyte = 1024 * 1024;
39
+ var maximumUploadBytes = 2048 * megabyte;
40
+ var maximumParts = 9e3;
41
+ var minimumPartSize = 10 * megabyte;
42
+ var uploadPartSize = (bytes) => {
43
+ const size = Number(bytes) || 0;
44
+ const required = Math.ceil(size / maximumParts);
45
+ const rounded = Math.ceil(required / megabyte) * megabyte;
46
+ return Math.max(minimumPartSize, rounded);
47
+ };
48
+ var uploadPartCount = (bytes) => {
49
+ const size = Number(bytes) || 0;
50
+ return Math.max(1, Math.ceil(size / uploadPartSize(size)));
51
+ };
33
52
  export {
34
53
  allowedMimes,
35
54
  allowedUploadTypes,
36
55
  isAllowedMime,
37
- resolveUploadType
56
+ maximumUploadBytes,
57
+ resolveUploadType,
58
+ uploadPartCount,
59
+ uploadPartSize
38
60
  };
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "tinycolor2": "1.6.0"
14
14
  },
15
15
  "devDependencies": {
16
- "@drawbridge/drawbridge-agents": "0.1.33",
16
+ "@drawbridge/drawbridge-agents": "0.1.36",
17
17
  "@node-oauth/oauth2-server": "5.3.0",
18
18
  "tsup": "8.5.1",
19
19
  "typescript": "5.9.3"
@@ -179,5 +179,5 @@
179
179
  "test": ". \"$HOME/.nvm/nvm.sh\" && nvm use && node --test"
180
180
  },
181
181
  "types": "dist/index.d.ts",
182
- "version": "0.0.100"
182
+ "version": "0.0.102"
183
183
  }