@ethisyscore/extension-runtime 1.137.0 → 1.138.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/{MockPluginStorage-BndTX2RR.d.ts → MockPluginStorage-DF3DL1dI.d.ts} +25 -1
- package/dist/{MockPluginStorage-BM3bq99B.d.cts → MockPluginStorage-QKfz8CMt.d.cts} +25 -1
- package/dist/host/index.cjs +29 -2
- package/dist/host/index.cjs.map +1 -1
- package/dist/host/index.js +29 -2
- package/dist/host/index.js.map +1 -1
- package/dist/mock-host/cli.cjs +196 -129
- package/dist/mock-host/cli.cjs.map +1 -1
- package/dist/mock-host/cli.d.cts +1 -1
- package/dist/mock-host/cli.d.ts +1 -1
- package/dist/mock-host/cli.js +196 -129
- package/dist/mock-host/cli.js.map +1 -1
- package/dist/mock-host/index.cjs +40 -0
- package/dist/mock-host/index.cjs.map +1 -1
- package/dist/mock-host/index.d.cts +2 -2
- package/dist/mock-host/index.d.ts +2 -2
- package/dist/mock-host/index.js +40 -0
- package/dist/mock-host/index.js.map +1 -1
- package/dist/plugin/index.cjs +31 -2
- package/dist/plugin/index.cjs.map +1 -1
- package/dist/plugin/index.d.cts +17 -2
- package/dist/plugin/index.d.ts +17 -2
- package/dist/plugin/index.js +31 -2
- package/dist/plugin/index.js.map +1 -1
- package/package.json +2 -2
package/dist/mock-host/cli.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SduiNode } from '@ethisyscore/protocol';
|
|
2
2
|
import { i as WorkerCtor, M as McpHttpClient, l as WorkerRemoteDomTransport } from '../transport-D1lE-5nt.cjs';
|
|
3
|
-
import { M as MockPluginStorage } from '../MockPluginStorage-
|
|
3
|
+
import { M as MockPluginStorage } from '../MockPluginStorage-QKfz8CMt.cjs';
|
|
4
4
|
import '../bridge-envelopes-BcKu-nQm.cjs';
|
|
5
5
|
|
|
6
6
|
/**
|
package/dist/mock-host/cli.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SduiNode } from '@ethisyscore/protocol';
|
|
2
2
|
import { i as WorkerCtor, M as McpHttpClient, l as WorkerRemoteDomTransport } from '../transport-INVtDo2s.js';
|
|
3
|
-
import { M as MockPluginStorage } from '../MockPluginStorage-
|
|
3
|
+
import { M as MockPluginStorage } from '../MockPluginStorage-DF3DL1dI.js';
|
|
4
4
|
import '../bridge-envelopes-BcKu-nQm.js';
|
|
5
5
|
|
|
6
6
|
/**
|
package/dist/mock-host/cli.js
CHANGED
|
@@ -123,6 +123,157 @@ function classifyHostError(err) {
|
|
|
123
123
|
return "internal";
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
// src/plugin/pluginStorageUpload.ts
|
|
127
|
+
var PLUGIN_STORAGE_UPLOAD_MAX_BYTES = 30 * 1024 * 1024;
|
|
128
|
+
var PLUGIN_STORAGE_PREFIX_MAX_LENGTH = 84;
|
|
129
|
+
var PLUGIN_STORAGE_PATH_MAX_LENGTH = 128;
|
|
130
|
+
var PluginStorageUploadError = class extends McpToolError {
|
|
131
|
+
/** What went wrong, in terms a caller branches on. */
|
|
132
|
+
reason;
|
|
133
|
+
/** HTTP status from the server, when the host surfaced one. */
|
|
134
|
+
status;
|
|
135
|
+
/**
|
|
136
|
+
* Seconds the server asked the caller to wait (`Retry-After`), on a `rate_limited` failure
|
|
137
|
+
* when the host forwarded the header. Absent otherwise.
|
|
138
|
+
*/
|
|
139
|
+
retryAfterSeconds;
|
|
140
|
+
/** Structural marker for {@link isPluginStorageUploadError}. */
|
|
141
|
+
isPluginStorageUploadError = true;
|
|
142
|
+
constructor(message, reason, options = {}) {
|
|
143
|
+
super(message, options.code ?? codeForReason(reason, options.status));
|
|
144
|
+
this.name = "PluginStorageUploadError";
|
|
145
|
+
this.reason = reason;
|
|
146
|
+
this.status = options.status;
|
|
147
|
+
if (options.retryAfterSeconds !== void 0) {
|
|
148
|
+
this.retryAfterSeconds = options.retryAfterSeconds;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
function parsePluginStorageRetryAfter(raw, nowMs = Date.now()) {
|
|
153
|
+
if (typeof raw === "number") {
|
|
154
|
+
return Number.isFinite(raw) && raw >= 0 ? Math.ceil(raw) : void 0;
|
|
155
|
+
}
|
|
156
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
157
|
+
return void 0;
|
|
158
|
+
}
|
|
159
|
+
const text = raw.trim();
|
|
160
|
+
if (/^\d+$/.test(text)) {
|
|
161
|
+
return Number(text);
|
|
162
|
+
}
|
|
163
|
+
const at = /[A-Za-z]/.test(text) ? Date.parse(text) : Number.NaN;
|
|
164
|
+
return Number.isNaN(at) ? void 0 : Math.max(0, Math.ceil((at - nowMs) / 1e3));
|
|
165
|
+
}
|
|
166
|
+
function validatePluginStoragePrefix(raw) {
|
|
167
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
168
|
+
return { ok: false, error: "A storage path prefix is required." };
|
|
169
|
+
}
|
|
170
|
+
if (raw.length > PLUGIN_STORAGE_PREFIX_MAX_LENGTH) {
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
error: `The storage path prefix must be at most ${PLUGIN_STORAGE_PREFIX_MAX_LENGTH} characters, so the stored path fits ${PLUGIN_STORAGE_PATH_MAX_LENGTH}.`
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (!/^[A-Za-z0-9_\-/]+$/.test(raw)) {
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
error: "The storage path prefix may contain only letters, digits, '-', '_' and '/' separators."
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (raw.split("/").some((segment) => segment.length === 0)) {
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
error: "The storage path prefix must not start or end with '/' or contain an empty segment."
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return { ok: true, prefix: raw };
|
|
189
|
+
}
|
|
190
|
+
var PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES = Object.freeze({
|
|
191
|
+
pdf: ["application/pdf"],
|
|
192
|
+
png: ["image/png"],
|
|
193
|
+
jpg: ["image/jpeg", "image/pjpeg", "image/jpg"],
|
|
194
|
+
jpeg: ["image/jpeg", "image/pjpeg", "image/jpg"],
|
|
195
|
+
gif: ["image/gif"],
|
|
196
|
+
webp: ["image/webp"],
|
|
197
|
+
doc: ["application/msword"],
|
|
198
|
+
docx: ["application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
|
|
199
|
+
xls: ["application/vnd.ms-excel"],
|
|
200
|
+
xlsx: ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"],
|
|
201
|
+
ppt: ["application/vnd.ms-powerpoint"],
|
|
202
|
+
pptx: ["application/vnd.openxmlformats-officedocument.presentationml.presentation"],
|
|
203
|
+
odt: ["application/vnd.oasis.opendocument.text"],
|
|
204
|
+
ods: ["application/vnd.oasis.opendocument.spreadsheet"],
|
|
205
|
+
odp: ["application/vnd.oasis.opendocument.presentation"],
|
|
206
|
+
txt: ["text/plain"],
|
|
207
|
+
csv: ["text/csv", "application/vnd.ms-excel", "application/csv", "text/plain"],
|
|
208
|
+
zip: ["application/zip", "application/x-zip-compressed", "application/x-zip"],
|
|
209
|
+
mp3: ["audio/mpeg", "audio/mp3"],
|
|
210
|
+
m4a: ["audio/mp4", "audio/x-m4a"],
|
|
211
|
+
mp4: ["video/mp4"]
|
|
212
|
+
});
|
|
213
|
+
var OCTET_STREAM = "application/octet-stream";
|
|
214
|
+
function pluginStorageSafeExtension(fileName) {
|
|
215
|
+
if (typeof fileName !== "string" || fileName.trim().length === 0) {
|
|
216
|
+
return "";
|
|
217
|
+
}
|
|
218
|
+
const leaf = fileName.split(/[\\/]/).pop() ?? "";
|
|
219
|
+
const dot = leaf.lastIndexOf(".");
|
|
220
|
+
if (dot < 0) {
|
|
221
|
+
return "";
|
|
222
|
+
}
|
|
223
|
+
const body = leaf.slice(dot + 1);
|
|
224
|
+
return /^[A-Za-z0-9]{1,10}$/.test(body) ? `.${body.toLowerCase()}` : "";
|
|
225
|
+
}
|
|
226
|
+
function resolvePluginStorageContentType(fileName, declaredContentType) {
|
|
227
|
+
const extension = pluginStorageSafeExtension(fileName).slice(1);
|
|
228
|
+
const allowed = extension.length > 0 && Object.prototype.hasOwnProperty.call(PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES, extension) ? PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES[extension] : void 0;
|
|
229
|
+
if (allowed === void 0) {
|
|
230
|
+
return {
|
|
231
|
+
ok: false,
|
|
232
|
+
error: "This file type is not allowed. Allowed: " + Object.keys(PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES).sort().join(", ") + "."
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const declared = mediaTypeOf(declaredContentType);
|
|
236
|
+
if (declared === void 0 || declared === OCTET_STREAM || allowed.includes(declared)) {
|
|
237
|
+
return { ok: true, contentType: allowed[0] };
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
ok: false,
|
|
241
|
+
error: `The declared content type '${declared}' does not match the file extension '.${extension}'.`
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function mediaTypeOf(raw) {
|
|
245
|
+
if (typeof raw !== "string") {
|
|
246
|
+
return void 0;
|
|
247
|
+
}
|
|
248
|
+
const mediaType = raw.split(";")[0].trim().toLowerCase();
|
|
249
|
+
return /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(mediaType) ? mediaType : void 0;
|
|
250
|
+
}
|
|
251
|
+
function codeForReason(reason, status) {
|
|
252
|
+
if (status !== void 0) {
|
|
253
|
+
return mcpErrorCodeFromHttpStatus(status);
|
|
254
|
+
}
|
|
255
|
+
switch (reason) {
|
|
256
|
+
case "invalid_prefix":
|
|
257
|
+
case "too_large":
|
|
258
|
+
case "unsupported_type":
|
|
259
|
+
return "invalid_request";
|
|
260
|
+
case "timeout":
|
|
261
|
+
return "timeout";
|
|
262
|
+
case "unauthorized":
|
|
263
|
+
return "unauthorized";
|
|
264
|
+
case "forbidden":
|
|
265
|
+
return "forbidden";
|
|
266
|
+
case "busy":
|
|
267
|
+
return "unavailable";
|
|
268
|
+
case "rate_limited":
|
|
269
|
+
return "rate_limited";
|
|
270
|
+
case "aborted":
|
|
271
|
+
return "timeout";
|
|
272
|
+
default:
|
|
273
|
+
return "internal";
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
126
277
|
// src/host/version-skew.ts
|
|
127
278
|
var EXTENSION_VERSION_HEADER = "x-extension-version";
|
|
128
279
|
var EXTENSION_ACTIVE_VERSION_HEADER = "x-extension-active-version";
|
|
@@ -707,7 +858,10 @@ var WorkerRemoteDomTransport = class {
|
|
|
707
858
|
// The HTTP status, on failure only. A bare number carries no host internals, and it
|
|
708
859
|
// is the only way the plugin can tell 413 (file too large) from 507 (quota) from 503
|
|
709
860
|
// (storage lock busy): the closed MCP code folds all three into `internal`/`unavailable`.
|
|
710
|
-
...!result.ok && typeof result.status === "number" ? { status: result.status } : {}
|
|
861
|
+
...!result.ok && typeof result.status === "number" ? { status: result.status } : {},
|
|
862
|
+
// The server's `Retry-After` (429), as whole seconds, so a plugin can show how long
|
|
863
|
+
// to wait. Never acted on here: an upload is not retried automatically.
|
|
864
|
+
...retryAfterField(result)
|
|
711
865
|
});
|
|
712
866
|
} catch (err) {
|
|
713
867
|
this.replyError(message.id, resultType, err, true);
|
|
@@ -716,16 +870,26 @@ var WorkerRemoteDomTransport = class {
|
|
|
716
870
|
replyError(id, type, err, includeStatus = false) {
|
|
717
871
|
const message = err instanceof Error ? err.message : "MCP request failed";
|
|
718
872
|
const status = includeStatus ? statusOf(err) : void 0;
|
|
873
|
+
const retryAfterSeconds = includeStatus && err !== null && typeof err === "object" ? parsePluginStorageRetryAfter(err.retryAfterSeconds) : void 0;
|
|
719
874
|
this.safePostMessage({
|
|
720
875
|
id,
|
|
721
876
|
type,
|
|
722
877
|
ok: false,
|
|
723
878
|
error: message,
|
|
724
879
|
code: classifyHostError(err),
|
|
725
|
-
...status !== void 0 ? { status } : {}
|
|
880
|
+
...status !== void 0 ? { status } : {},
|
|
881
|
+
...retryAfterSeconds !== void 0 ? { retryAfterSeconds } : {}
|
|
726
882
|
});
|
|
727
883
|
}
|
|
728
884
|
};
|
|
885
|
+
function retryAfterField(result) {
|
|
886
|
+
if (result.ok || !result.headers) {
|
|
887
|
+
return {};
|
|
888
|
+
}
|
|
889
|
+
const key = Object.keys(result.headers).find((k) => k.toLowerCase() === "retry-after");
|
|
890
|
+
const seconds = key === void 0 ? void 0 : parsePluginStorageRetryAfter(result.headers[key]);
|
|
891
|
+
return seconds === void 0 ? {} : { retryAfterSeconds: seconds };
|
|
892
|
+
}
|
|
729
893
|
function statusOf(err) {
|
|
730
894
|
if (err === null || typeof err !== "object") {
|
|
731
895
|
return void 0;
|
|
@@ -756,138 +920,20 @@ var CONTRACT_B_PRIMITIVES = [
|
|
|
756
920
|
];
|
|
757
921
|
new Set(CONTRACT_B_PRIMITIVES);
|
|
758
922
|
|
|
759
|
-
// src/plugin/pluginStorageUpload.ts
|
|
760
|
-
var PLUGIN_STORAGE_UPLOAD_MAX_BYTES = 30 * 1024 * 1024;
|
|
761
|
-
var PLUGIN_STORAGE_PREFIX_MAX_LENGTH = 84;
|
|
762
|
-
var PLUGIN_STORAGE_PATH_MAX_LENGTH = 128;
|
|
763
|
-
var PluginStorageUploadError = class extends McpToolError {
|
|
764
|
-
/** What went wrong, in terms a caller branches on. */
|
|
765
|
-
reason;
|
|
766
|
-
/** HTTP status from the server, when the host surfaced one. */
|
|
767
|
-
status;
|
|
768
|
-
/** Structural marker for {@link isPluginStorageUploadError}. */
|
|
769
|
-
isPluginStorageUploadError = true;
|
|
770
|
-
constructor(message, reason, options = {}) {
|
|
771
|
-
super(message, options.code ?? codeForReason(reason, options.status));
|
|
772
|
-
this.name = "PluginStorageUploadError";
|
|
773
|
-
this.reason = reason;
|
|
774
|
-
this.status = options.status;
|
|
775
|
-
}
|
|
776
|
-
};
|
|
777
|
-
function validatePluginStoragePrefix(raw) {
|
|
778
|
-
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
779
|
-
return { ok: false, error: "A storage path prefix is required." };
|
|
780
|
-
}
|
|
781
|
-
if (raw.length > PLUGIN_STORAGE_PREFIX_MAX_LENGTH) {
|
|
782
|
-
return {
|
|
783
|
-
ok: false,
|
|
784
|
-
error: `The storage path prefix must be at most ${PLUGIN_STORAGE_PREFIX_MAX_LENGTH} characters, so the stored path fits ${PLUGIN_STORAGE_PATH_MAX_LENGTH}.`
|
|
785
|
-
};
|
|
786
|
-
}
|
|
787
|
-
if (!/^[A-Za-z0-9_\-/]+$/.test(raw)) {
|
|
788
|
-
return {
|
|
789
|
-
ok: false,
|
|
790
|
-
error: "The storage path prefix may contain only letters, digits, '-', '_' and '/' separators."
|
|
791
|
-
};
|
|
792
|
-
}
|
|
793
|
-
if (raw.split("/").some((segment) => segment.length === 0)) {
|
|
794
|
-
return {
|
|
795
|
-
ok: false,
|
|
796
|
-
error: "The storage path prefix must not start or end with '/' or contain an empty segment."
|
|
797
|
-
};
|
|
798
|
-
}
|
|
799
|
-
return { ok: true, prefix: raw };
|
|
800
|
-
}
|
|
801
|
-
var PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES = Object.freeze({
|
|
802
|
-
pdf: ["application/pdf"],
|
|
803
|
-
png: ["image/png"],
|
|
804
|
-
jpg: ["image/jpeg", "image/pjpeg", "image/jpg"],
|
|
805
|
-
jpeg: ["image/jpeg", "image/pjpeg", "image/jpg"],
|
|
806
|
-
gif: ["image/gif"],
|
|
807
|
-
webp: ["image/webp"],
|
|
808
|
-
doc: ["application/msword"],
|
|
809
|
-
docx: ["application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
|
|
810
|
-
xls: ["application/vnd.ms-excel"],
|
|
811
|
-
xlsx: ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"],
|
|
812
|
-
ppt: ["application/vnd.ms-powerpoint"],
|
|
813
|
-
pptx: ["application/vnd.openxmlformats-officedocument.presentationml.presentation"],
|
|
814
|
-
odt: ["application/vnd.oasis.opendocument.text"],
|
|
815
|
-
ods: ["application/vnd.oasis.opendocument.spreadsheet"],
|
|
816
|
-
odp: ["application/vnd.oasis.opendocument.presentation"],
|
|
817
|
-
txt: ["text/plain"],
|
|
818
|
-
csv: ["text/csv", "application/vnd.ms-excel", "application/csv", "text/plain"],
|
|
819
|
-
zip: ["application/zip", "application/x-zip-compressed", "application/x-zip"],
|
|
820
|
-
mp3: ["audio/mpeg", "audio/mp3"],
|
|
821
|
-
m4a: ["audio/mp4", "audio/x-m4a"],
|
|
822
|
-
mp4: ["video/mp4"]
|
|
823
|
-
});
|
|
824
|
-
var OCTET_STREAM = "application/octet-stream";
|
|
825
|
-
function pluginStorageSafeExtension(fileName) {
|
|
826
|
-
if (typeof fileName !== "string" || fileName.trim().length === 0) {
|
|
827
|
-
return "";
|
|
828
|
-
}
|
|
829
|
-
const leaf = fileName.split(/[\\/]/).pop() ?? "";
|
|
830
|
-
const dot = leaf.lastIndexOf(".");
|
|
831
|
-
if (dot < 0) {
|
|
832
|
-
return "";
|
|
833
|
-
}
|
|
834
|
-
const body = leaf.slice(dot + 1);
|
|
835
|
-
return /^[A-Za-z0-9]{1,10}$/.test(body) ? `.${body.toLowerCase()}` : "";
|
|
836
|
-
}
|
|
837
|
-
function resolvePluginStorageContentType(fileName, declaredContentType) {
|
|
838
|
-
const extension = pluginStorageSafeExtension(fileName).slice(1);
|
|
839
|
-
const allowed = extension.length > 0 && Object.prototype.hasOwnProperty.call(PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES, extension) ? PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES[extension] : void 0;
|
|
840
|
-
if (allowed === void 0) {
|
|
841
|
-
return {
|
|
842
|
-
ok: false,
|
|
843
|
-
error: "This file type is not allowed. Allowed: " + Object.keys(PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES).sort().join(", ") + "."
|
|
844
|
-
};
|
|
845
|
-
}
|
|
846
|
-
const declared = mediaTypeOf(declaredContentType);
|
|
847
|
-
if (declared === void 0 || declared === OCTET_STREAM || allowed.includes(declared)) {
|
|
848
|
-
return { ok: true, contentType: allowed[0] };
|
|
849
|
-
}
|
|
850
|
-
return {
|
|
851
|
-
ok: false,
|
|
852
|
-
error: `The declared content type '${declared}' does not match the file extension '.${extension}'.`
|
|
853
|
-
};
|
|
854
|
-
}
|
|
855
|
-
function mediaTypeOf(raw) {
|
|
856
|
-
if (typeof raw !== "string") {
|
|
857
|
-
return void 0;
|
|
858
|
-
}
|
|
859
|
-
const mediaType = raw.split(";")[0].trim().toLowerCase();
|
|
860
|
-
return /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(mediaType) ? mediaType : void 0;
|
|
861
|
-
}
|
|
862
|
-
function codeForReason(reason, status) {
|
|
863
|
-
if (status !== void 0) {
|
|
864
|
-
return mcpErrorCodeFromHttpStatus(status);
|
|
865
|
-
}
|
|
866
|
-
switch (reason) {
|
|
867
|
-
case "invalid_prefix":
|
|
868
|
-
case "too_large":
|
|
869
|
-
case "unsupported_type":
|
|
870
|
-
return "invalid_request";
|
|
871
|
-
case "timeout":
|
|
872
|
-
return "timeout";
|
|
873
|
-
case "unauthorized":
|
|
874
|
-
return "unauthorized";
|
|
875
|
-
case "forbidden":
|
|
876
|
-
return "forbidden";
|
|
877
|
-
case "busy":
|
|
878
|
-
return "unavailable";
|
|
879
|
-
case "aborted":
|
|
880
|
-
return "timeout";
|
|
881
|
-
default:
|
|
882
|
-
return "internal";
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
|
|
886
923
|
// src/mock-host/MockPluginStorage.ts
|
|
887
924
|
var MockPluginStorage = class {
|
|
888
925
|
files = /* @__PURE__ */ new Map();
|
|
926
|
+
rateLimit;
|
|
927
|
+
now;
|
|
928
|
+
windowStartMs = 0;
|
|
929
|
+
windowCount = 0;
|
|
930
|
+
constructor(options = {}) {
|
|
931
|
+
this.rateLimit = options.rateLimit;
|
|
932
|
+
this.now = options.now ?? Date.now;
|
|
933
|
+
}
|
|
889
934
|
/** Store `buffer` under a new path below `meta.pathPrefix`. */
|
|
890
935
|
store(meta, buffer) {
|
|
936
|
+
this.enforceRateLimit();
|
|
891
937
|
const prefix = validatePluginStoragePrefix(meta.pathPrefix);
|
|
892
938
|
if (!prefix.ok) {
|
|
893
939
|
throw new PluginStorageUploadError(prefix.error, "invalid_prefix", { status: 400 });
|
|
@@ -909,6 +955,27 @@ var MockPluginStorage = class {
|
|
|
909
955
|
this.files.set(path2, { path: path2, fileName, contentType: type.contentType, bytes });
|
|
910
956
|
return { path: path2, fileName, contentType: type.contentType, sizeBytes: bytes.byteLength };
|
|
911
957
|
}
|
|
958
|
+
/** The kernel refuses before the handler runs, so this is checked before anything else. */
|
|
959
|
+
enforceRateLimit() {
|
|
960
|
+
if (this.rateLimit === void 0) {
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
const windowMs = (this.rateLimit.windowSeconds ?? 60) * 1e3;
|
|
964
|
+
const now = this.now();
|
|
965
|
+
if (this.windowCount === 0 || now - this.windowStartMs >= windowMs) {
|
|
966
|
+
this.windowStartMs = now;
|
|
967
|
+
this.windowCount = 0;
|
|
968
|
+
}
|
|
969
|
+
this.windowCount += 1;
|
|
970
|
+
if (this.windowCount > this.rateLimit.maxRequests) {
|
|
971
|
+
const retryAfterSeconds = Math.max(1, Math.ceil((this.windowStartMs + windowMs - now) / 1e3));
|
|
972
|
+
throw new PluginStorageUploadError(
|
|
973
|
+
"Rate limit exceeded. Please try again later.",
|
|
974
|
+
"rate_limited",
|
|
975
|
+
{ status: 429, retryAfterSeconds }
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
912
979
|
/** The stored file at `path`, or `undefined`. */
|
|
913
980
|
get(path2) {
|
|
914
981
|
return this.files.get(path2);
|