@drawbridge/drawbridge-utils 0.0.97 → 0.0.99
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/ai.cjs +11 -0
- package/dist/ai.js +11 -0
- package/dist/billing.cjs +11 -0
- package/dist/billing.d.cts +11 -0
- package/dist/billing.d.ts +11 -0
- package/dist/billing.js +11 -0
- package/dist/fetch.cjs +57 -2
- package/dist/fetch.d.cts +113 -1
- package/dist/fetch.d.ts +113 -1
- package/dist/fetch.js +55 -1
- package/dist/upload.cjs +27 -2
- package/dist/upload.d.cts +59 -1
- package/dist/upload.d.ts +59 -1
- package/dist/upload.js +23 -1
- package/package.json +2 -2
package/dist/ai.cjs
CHANGED
|
@@ -177,6 +177,17 @@ var cost = {
|
|
|
177
177
|
cached: 3,
|
|
178
178
|
input: 30,
|
|
179
179
|
output: 3e3
|
|
180
|
+
},
|
|
181
|
+
// gemini-3-pro-image-preview — verified against Google's pricing page
|
|
182
|
+
// 2026-08-12: $2.00 input / $12.00 text output per 1M, and image output
|
|
183
|
+
// tokens at ~$120/1M (a 1K-2K image is 1120 tokens = $0.134, a 4K image
|
|
184
|
+
// 2000 tokens = $0.24). Encoded the flash-image way: one flat output rate
|
|
185
|
+
// that reproduces the per-image price from the tokens usageMetadata
|
|
186
|
+
// reports. Growth's hero generation runs this model today.
|
|
187
|
+
"gemini-3-pro-image-preview": {
|
|
188
|
+
cached: 20,
|
|
189
|
+
input: 200,
|
|
190
|
+
output: 12e3
|
|
180
191
|
}
|
|
181
192
|
};
|
|
182
193
|
var toolCost = {
|
package/dist/ai.js
CHANGED
|
@@ -143,6 +143,17 @@ var cost = {
|
|
|
143
143
|
cached: 3,
|
|
144
144
|
input: 30,
|
|
145
145
|
output: 3e3
|
|
146
|
+
},
|
|
147
|
+
// gemini-3-pro-image-preview — verified against Google's pricing page
|
|
148
|
+
// 2026-08-12: $2.00 input / $12.00 text output per 1M, and image output
|
|
149
|
+
// tokens at ~$120/1M (a 1K-2K image is 1120 tokens = $0.134, a 4K image
|
|
150
|
+
// 2000 tokens = $0.24). Encoded the flash-image way: one flat output rate
|
|
151
|
+
// that reproduces the per-image price from the tokens usageMetadata
|
|
152
|
+
// reports. Growth's hero generation runs this model today.
|
|
153
|
+
"gemini-3-pro-image-preview": {
|
|
154
|
+
cached: 20,
|
|
155
|
+
input: 200,
|
|
156
|
+
output: 12e3
|
|
146
157
|
}
|
|
147
158
|
};
|
|
148
159
|
var toolCost = {
|
package/dist/billing.cjs
CHANGED
|
@@ -177,6 +177,17 @@ var cost = {
|
|
|
177
177
|
cached: 3,
|
|
178
178
|
input: 30,
|
|
179
179
|
output: 3e3
|
|
180
|
+
},
|
|
181
|
+
// gemini-3-pro-image-preview — verified against Google's pricing page
|
|
182
|
+
// 2026-08-12: $2.00 input / $12.00 text output per 1M, and image output
|
|
183
|
+
// tokens at ~$120/1M (a 1K-2K image is 1120 tokens = $0.134, a 4K image
|
|
184
|
+
// 2000 tokens = $0.24). Encoded the flash-image way: one flat output rate
|
|
185
|
+
// that reproduces the per-image price from the tokens usageMetadata
|
|
186
|
+
// reports. Growth's hero generation runs this model today.
|
|
187
|
+
"gemini-3-pro-image-preview": {
|
|
188
|
+
cached: 20,
|
|
189
|
+
input: 200,
|
|
190
|
+
output: 12e3
|
|
180
191
|
}
|
|
181
192
|
};
|
|
182
193
|
var toolCost = {
|
package/dist/billing.d.cts
CHANGED
|
@@ -48,6 +48,17 @@ const cost = {
|
|
|
48
48
|
cached : 3,
|
|
49
49
|
input : 30,
|
|
50
50
|
output : 3000
|
|
51
|
+
},
|
|
52
|
+
// gemini-3-pro-image-preview — verified against Google's pricing page
|
|
53
|
+
// 2026-08-12: $2.00 input / $12.00 text output per 1M, and image output
|
|
54
|
+
// tokens at ~$120/1M (a 1K-2K image is 1120 tokens = $0.134, a 4K image
|
|
55
|
+
// 2000 tokens = $0.24). Encoded the flash-image way: one flat output rate
|
|
56
|
+
// that reproduces the per-image price from the tokens usageMetadata
|
|
57
|
+
// reports. Growth's hero generation runs this model today.
|
|
58
|
+
'gemini-3-pro-image-preview' : {
|
|
59
|
+
cached : 20,
|
|
60
|
+
input : 200,
|
|
61
|
+
output : 12000
|
|
51
62
|
}
|
|
52
63
|
};
|
|
53
64
|
|
package/dist/billing.d.ts
CHANGED
|
@@ -48,6 +48,17 @@ const cost = {
|
|
|
48
48
|
cached : 3,
|
|
49
49
|
input : 30,
|
|
50
50
|
output : 3000
|
|
51
|
+
},
|
|
52
|
+
// gemini-3-pro-image-preview — verified against Google's pricing page
|
|
53
|
+
// 2026-08-12: $2.00 input / $12.00 text output per 1M, and image output
|
|
54
|
+
// tokens at ~$120/1M (a 1K-2K image is 1120 tokens = $0.134, a 4K image
|
|
55
|
+
// 2000 tokens = $0.24). Encoded the flash-image way: one flat output rate
|
|
56
|
+
// that reproduces the per-image price from the tokens usageMetadata
|
|
57
|
+
// reports. Growth's hero generation runs this model today.
|
|
58
|
+
'gemini-3-pro-image-preview' : {
|
|
59
|
+
cached : 20,
|
|
60
|
+
input : 200,
|
|
61
|
+
output : 12000
|
|
51
62
|
}
|
|
52
63
|
};
|
|
53
64
|
|
package/dist/billing.js
CHANGED
|
@@ -147,6 +147,17 @@ var cost = {
|
|
|
147
147
|
cached: 3,
|
|
148
148
|
input: 30,
|
|
149
149
|
output: 3e3
|
|
150
|
+
},
|
|
151
|
+
// gemini-3-pro-image-preview — verified against Google's pricing page
|
|
152
|
+
// 2026-08-12: $2.00 input / $12.00 text output per 1M, and image output
|
|
153
|
+
// tokens at ~$120/1M (a 1K-2K image is 1120 tokens = $0.134, a 4K image
|
|
154
|
+
// 2000 tokens = $0.24). Encoded the flash-image way: one flat output rate
|
|
155
|
+
// that reproduces the per-image price from the tokens usageMetadata
|
|
156
|
+
// reports. Growth's hero generation runs this model today.
|
|
157
|
+
"gemini-3-pro-image-preview": {
|
|
158
|
+
cached: 20,
|
|
159
|
+
input: 200,
|
|
160
|
+
output: 12e3
|
|
150
161
|
}
|
|
151
162
|
};
|
|
152
163
|
var toolCost = {
|
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/upload.cjs
CHANGED
|
@@ -22,7 +22,10 @@ __export(upload_exports, {
|
|
|
22
22
|
allowedMimes: () => allowedMimes,
|
|
23
23
|
allowedUploadTypes: () => allowedUploadTypes,
|
|
24
24
|
isAllowedMime: () => isAllowedMime,
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
16
|
+
"@drawbridge/drawbridge-agents": "0.1.33",
|
|
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.
|
|
182
|
+
"version": "0.0.99"
|
|
183
183
|
}
|