@takosjp/yurucommu-core 3.3.0 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/migrations/0022_inbound_dispatch_claims.sql +17 -0
  2. package/package.json +3 -2
  3. package/packages/api/package.json +1 -1
  4. package/packages/api/src/index.ts +1 -0
  5. package/packages/api/src/lib/api/notifications.ts +1 -0
  6. package/packages/api/src/lib/api/posts.ts +1 -0
  7. package/packages/api/src/lib/rtc-client.ts +1 -3
  8. package/packages/api/src/types/call.ts +2 -10
  9. package/packages/api/src/types/index.ts +3 -0
  10. package/packages/api/src/types/realtime.ts +139 -0
  11. package/src/backend/index.ts +54 -12
  12. package/src/backend/lib/delivery/queue-batching.ts +53 -36
  13. package/src/backend/lib/delivery/queue-delivery.ts +3 -3
  14. package/src/backend/lib/delivery/queue.ts +17 -9
  15. package/src/backend/lib/delivery/types.ts +12 -0
  16. package/src/backend/lib/notification-push.ts +2 -2
  17. package/src/backend/lib/oauth-providers.ts +9 -0
  18. package/src/backend/lib/strip-image-metadata.ts +50 -30
  19. package/src/backend/lib/unread-counts.ts +63 -2
  20. package/src/backend/middleware/bearer-auth.ts +24 -9
  21. package/src/backend/public.ts +25 -1
  22. package/src/backend/routes/activitypub/handlers/actor-inbox-handlers.ts +4 -3
  23. package/src/backend/routes/activitypub/handlers/inbox-content-handlers.ts +204 -5
  24. package/src/backend/routes/activitypub/handlers/inbox-follow-handlers.ts +78 -53
  25. package/src/backend/routes/activitypub/handlers/inbox-interaction-handlers.ts +66 -2
  26. package/src/backend/routes/activitypub/handlers/inbox-shared-helpers.ts +35 -12
  27. package/src/backend/routes/activitypub/inbox-addressing.ts +236 -0
  28. package/src/backend/routes/activitypub/inbox-types.ts +8 -0
  29. package/src/backend/routes/activitypub/inbox.ts +410 -205
  30. package/src/backend/routes/activitypub/outbox.ts +0 -0
  31. package/src/backend/routes/auth.ts +2 -1
  32. package/src/backend/routes/communities/messages.ts +53 -17
  33. package/src/backend/routes/dm/messages.ts +47 -7
  34. package/src/backend/routes/dm/read-archive.ts +27 -0
  35. package/src/backend/routes/dm/typing.ts +16 -0
  36. package/src/backend/routes/notifications.ts +25 -0
  37. package/src/backend/routes/posts/post-helpers.ts +42 -23
  38. package/src/backend/routes/realtime/index.ts +67 -0
  39. package/src/backend/routes/rtc/index.ts +5 -1
  40. package/src/backend/runtime/call-hub-core.ts +13 -3
  41. package/src/backend/runtime/cloudflare.ts +63 -2
  42. package/src/backend/runtime/managed-relational.ts +197 -0
  43. package/src/backend/runtime/managed-runtime.ts +631 -0
  44. package/src/backend/runtime/queue.ts +40 -0
  45. package/src/backend/runtime/realtime-hub.ts +257 -0
  46. package/src/backend/runtime/realtime-stream-do.ts +323 -0
  47. package/src/backend/server.ts +15 -18
  48. package/src/backend/types.ts +13 -2
  49. package/src/db/d1-write.ts +270 -0
  50. package/src/db/index.ts +17 -0
  51. package/src/db/schema/federation.ts +19 -0
  52. package/src/db/schema/index.ts +1 -0
@@ -0,0 +1,631 @@
1
+ import {
2
+ TAKOSUMI_MANAGED_RUNTIME_INVOKE_PERMISSION,
3
+ managedRuntimeConnection,
4
+ managedRuntimeGatewayFailure,
5
+ managedRuntimeKeyValueListRequest,
6
+ managedRuntimeKeyValueRequest,
7
+ managedRuntimeObjectListRequest,
8
+ managedRuntimeObjectRequest,
9
+ managedRuntimeQueueBatchSendGatewayRequest,
10
+ managedRuntimeQueueSendGatewayRequest,
11
+ parseManagedRuntimeKeyValueListResponse,
12
+ parseManagedRuntimeObjectListResponse,
13
+ parseManagedRuntimeConnectionMaterialization,
14
+ parseManagedRuntimeQueueSendResponse,
15
+ type ManagedRuntimeConnectionMaterialization,
16
+ } from "@takosjp/takosumi-contract/managed-runtime-connections";
17
+
18
+ import type {
19
+ IKeyValueStore,
20
+ IObjectStorage,
21
+ ListObjectsResult,
22
+ ObjectMetadata,
23
+ StorageObject,
24
+ } from "./types.ts";
25
+ import type {
26
+ IQueueProducer,
27
+ QueueBatchItem,
28
+ QueueSendOptions,
29
+ } from "./queue.ts";
30
+
31
+ const DEFAULT_MAX_GATEWAY_RESPONSE_BYTES = 16 * 1024;
32
+ const DEFAULT_MAX_VALUE_RESPONSE_BYTES = 25 * 1024 * 1024;
33
+ const DEFAULT_MAX_OBJECT_RESPONSE_BYTES = 16 * 1024 * 1024;
34
+
35
+ export interface ManagedRuntimeGateway {
36
+ fetch(request: Request): Promise<Response>;
37
+ }
38
+
39
+ export class ManagedRuntimeGatewayError extends Error {
40
+ constructor(
41
+ readonly code: string,
42
+ readonly status: number,
43
+ readonly retryable: boolean,
44
+ ) {
45
+ super(code);
46
+ this.name = "ManagedRuntimeGatewayError";
47
+ }
48
+ }
49
+
50
+ export interface ManagedRuntimeQueueProducerOptions {
51
+ readonly materialization: unknown;
52
+ readonly gateway: ManagedRuntimeGateway;
53
+ readonly alias: string;
54
+ readonly idempotencyKey?: () => string;
55
+ readonly maxResponseBytes?: number;
56
+ }
57
+
58
+ export interface ManagedRuntimeDataAdapterOptions {
59
+ readonly materialization: unknown;
60
+ readonly gateway: ManagedRuntimeGateway;
61
+ readonly alias: string;
62
+ readonly idempotencyKey?: () => string;
63
+ readonly maxMetadataResponseBytes?: number;
64
+ readonly maxValueResponseBytes?: number;
65
+ }
66
+
67
+ export function createManagedRuntimeKeyValueStore(
68
+ options: ManagedRuntimeDataAdapterOptions,
69
+ ): IKeyValueStore {
70
+ const materialization = parseManagedRuntimeConnectionMaterialization(
71
+ options.materialization,
72
+ );
73
+ const connection = managedRuntimeConnection(materialization, options.alias, {
74
+ expectedKind: "KeyValueStore",
75
+ requiredPermission: TAKOSUMI_MANAGED_RUNTIME_INVOKE_PERMISSION,
76
+ });
77
+ return new ManagedRuntimeKeyValueStore({
78
+ authority: connection.authority,
79
+ gateway: options.gateway,
80
+ idempotencyKey:
81
+ options.idempotencyKey ?? (() => `yurucommu.kv:${crypto.randomUUID()}`),
82
+ maxMetadataResponseBytes:
83
+ options.maxMetadataResponseBytes ?? DEFAULT_MAX_GATEWAY_RESPONSE_BYTES,
84
+ maxValueResponseBytes:
85
+ options.maxValueResponseBytes ?? DEFAULT_MAX_VALUE_RESPONSE_BYTES,
86
+ });
87
+ }
88
+
89
+ export function createManagedRuntimeObjectStorage(
90
+ options: ManagedRuntimeDataAdapterOptions,
91
+ ): IObjectStorage {
92
+ const materialization = parseManagedRuntimeConnectionMaterialization(
93
+ options.materialization,
94
+ );
95
+ const connection = managedRuntimeConnection(materialization, options.alias, {
96
+ expectedKind: "ObjectBucket",
97
+ requiredPermission: TAKOSUMI_MANAGED_RUNTIME_INVOKE_PERMISSION,
98
+ });
99
+ return new ManagedRuntimeObjectStorage({
100
+ authority: connection.authority,
101
+ gateway: options.gateway,
102
+ idempotencyKey:
103
+ options.idempotencyKey ??
104
+ (() => `yurucommu.object:${crypto.randomUUID()}`),
105
+ maxMetadataResponseBytes:
106
+ options.maxMetadataResponseBytes ?? DEFAULT_MAX_GATEWAY_RESPONSE_BYTES,
107
+ maxValueResponseBytes:
108
+ options.maxValueResponseBytes ?? DEFAULT_MAX_OBJECT_RESPONSE_BYTES,
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Creates the queue producer selected by an exact host-issued materialization.
114
+ *
115
+ * The materialization and Fetch-compatible gateway are injected separately:
116
+ * portable application configuration never contains provider ids or bearer
117
+ * credentials, and a managed selection never falls back to native bindings.
118
+ */
119
+ export function createManagedRuntimeQueueProducer<T>(
120
+ options: ManagedRuntimeQueueProducerOptions,
121
+ ): IQueueProducer<T> {
122
+ const materialization = parseManagedRuntimeConnectionMaterialization(
123
+ options.materialization,
124
+ );
125
+ const connection = managedRuntimeConnection(materialization, options.alias, {
126
+ expectedKind: "Queue",
127
+ requiredPermission: TAKOSUMI_MANAGED_RUNTIME_INVOKE_PERMISSION,
128
+ });
129
+ return new ManagedRuntimeQueueProducer<T>({
130
+ materialization,
131
+ authority: connection.authority,
132
+ gateway: options.gateway,
133
+ idempotencyKey:
134
+ options.idempotencyKey ??
135
+ (() => `yurucommu.queue:${crypto.randomUUID()}`),
136
+ maxResponseBytes:
137
+ options.maxResponseBytes ?? DEFAULT_MAX_GATEWAY_RESPONSE_BYTES,
138
+ });
139
+ }
140
+
141
+ class ManagedRuntimeQueueProducer<T> implements IQueueProducer<T> {
142
+ constructor(
143
+ private readonly options: {
144
+ readonly materialization: ManagedRuntimeConnectionMaterialization;
145
+ readonly authority: ManagedRuntimeConnectionMaterialization["connections"][number]["authority"];
146
+ readonly gateway: ManagedRuntimeGateway;
147
+ readonly idempotencyKey: () => string;
148
+ readonly maxResponseBytes: number;
149
+ },
150
+ ) {
151
+ if (
152
+ !Number.isSafeInteger(options.maxResponseBytes) ||
153
+ options.maxResponseBytes < 1
154
+ ) {
155
+ throw new TypeError("managed_runtime_response_limit_invalid");
156
+ }
157
+ }
158
+
159
+ async send(body: T, options?: QueueSendOptions): Promise<void> {
160
+ const request = managedRuntimeQueueSendGatewayRequest(
161
+ this.options.authority,
162
+ {
163
+ message: { type: "json", body },
164
+ ...(options?.delaySeconds === undefined
165
+ ? {}
166
+ : { delaySeconds: options.delaySeconds }),
167
+ },
168
+ this.options.idempotencyKey(),
169
+ );
170
+ await this.sendRequest(request);
171
+ }
172
+
173
+ async sendBatch(
174
+ messages: readonly QueueBatchItem<T>[],
175
+ options?: QueueSendOptions,
176
+ ): Promise<void> {
177
+ const request = managedRuntimeQueueBatchSendGatewayRequest(
178
+ this.options.authority,
179
+ {
180
+ messages: messages.map(({ body, delaySeconds }) => ({
181
+ message: { type: "json" as const, body },
182
+ ...(delaySeconds === undefined ? {} : { delaySeconds }),
183
+ })),
184
+ ...(options?.delaySeconds === undefined
185
+ ? {}
186
+ : { defaultDelaySeconds: options.delaySeconds }),
187
+ },
188
+ this.options.idempotencyKey(),
189
+ );
190
+ await this.sendRequest(request);
191
+ }
192
+
193
+ private async sendRequest(request: Request): Promise<void> {
194
+ const response = await boundedResponse(
195
+ await this.options.gateway.fetch(request),
196
+ this.options.maxResponseBytes,
197
+ );
198
+ const failure = await managedRuntimeGatewayFailure(response.clone());
199
+ if (failure) {
200
+ throw new ManagedRuntimeGatewayError(
201
+ failure.code,
202
+ failure.status,
203
+ failure.retryable,
204
+ );
205
+ }
206
+ parseManagedRuntimeQueueSendResponse(await response.json());
207
+ }
208
+ }
209
+
210
+ type ManagedRuntimeAuthority =
211
+ ManagedRuntimeConnectionMaterialization["connections"][number]["authority"];
212
+
213
+ type ManagedRuntimeDataClientOptions = {
214
+ readonly authority: ManagedRuntimeAuthority;
215
+ readonly gateway: ManagedRuntimeGateway;
216
+ readonly idempotencyKey: () => string;
217
+ readonly maxMetadataResponseBytes: number;
218
+ readonly maxValueResponseBytes: number;
219
+ };
220
+
221
+ class ManagedRuntimeKeyValueStore implements IKeyValueStore {
222
+ constructor(private readonly options: ManagedRuntimeDataClientOptions) {
223
+ assertResponseLimit(options.maxMetadataResponseBytes);
224
+ assertResponseLimit(options.maxValueResponseBytes);
225
+ }
226
+
227
+ get(key: string, options?: { type?: "text" }): Promise<string | null>;
228
+ get<T = unknown>(key: string, options: { type: "json" }): Promise<T | null>;
229
+ get(
230
+ key: string,
231
+ options: { type: "arrayBuffer" },
232
+ ): Promise<ArrayBuffer | null>;
233
+ async get<T = unknown>(
234
+ key: string,
235
+ options?: { type?: "text" | "json" | "arrayBuffer" },
236
+ ): Promise<string | ArrayBuffer | T | null> {
237
+ const request = managedRuntimeKeyValueRequest(this.options.authority, {
238
+ method: "GET",
239
+ key,
240
+ idempotencyKey: this.options.idempotencyKey(),
241
+ });
242
+ const raw = await this.options.gateway.fetch(request);
243
+ if (raw.status === 404) {
244
+ await raw.body?.cancel().catch(() => undefined);
245
+ return null;
246
+ }
247
+ const response = await checkedResponse(
248
+ raw,
249
+ this.options.maxValueResponseBytes,
250
+ );
251
+ const type = options?.type ?? "text";
252
+ if (type === "arrayBuffer") return await response.arrayBuffer();
253
+ if (type === "json") {
254
+ try {
255
+ return JSON.parse(await response.text()) as T;
256
+ } catch {
257
+ throw new ManagedRuntimeGatewayError(
258
+ "managed_runtime_kv_json_invalid",
259
+ 502,
260
+ false,
261
+ );
262
+ }
263
+ }
264
+ return await response.text();
265
+ }
266
+
267
+ async put(
268
+ key: string,
269
+ value: string | ArrayBuffer | ReadableStream,
270
+ options?: {
271
+ expirationTtl?: number;
272
+ expiration?: number;
273
+ metadata?: Record<string, unknown>;
274
+ },
275
+ ): Promise<void> {
276
+ const request = managedRuntimeKeyValueRequest(this.options.authority, {
277
+ method: "PUT",
278
+ key,
279
+ idempotencyKey: this.options.idempotencyKey(),
280
+ value: value as BodyInit,
281
+ ...(options === undefined ? {} : { options }),
282
+ });
283
+ await expectOkResponse(
284
+ await this.options.gateway.fetch(request),
285
+ this.options.maxMetadataResponseBytes,
286
+ );
287
+ }
288
+
289
+ async delete(key: string): Promise<void> {
290
+ const request = managedRuntimeKeyValueRequest(this.options.authority, {
291
+ method: "DELETE",
292
+ key,
293
+ idempotencyKey: this.options.idempotencyKey(),
294
+ });
295
+ await expectOkResponse(
296
+ await this.options.gateway.fetch(request),
297
+ this.options.maxMetadataResponseBytes,
298
+ );
299
+ }
300
+
301
+ async list(options?: {
302
+ prefix?: string;
303
+ limit?: number;
304
+ cursor?: string;
305
+ }): Promise<{
306
+ keys: Array<{ name: string; expiration?: number; metadata?: unknown }>;
307
+ list_complete: boolean;
308
+ cursor?: string;
309
+ }> {
310
+ const request = managedRuntimeKeyValueListRequest(this.options.authority, {
311
+ idempotencyKey: this.options.idempotencyKey(),
312
+ ...options,
313
+ });
314
+ const response = await checkedResponse(
315
+ await this.options.gateway.fetch(request),
316
+ this.options.maxMetadataResponseBytes,
317
+ );
318
+ const parsed = parseManagedRuntimeKeyValueListResponse(
319
+ await response.json(),
320
+ );
321
+ return {
322
+ keys: [...parsed.keys],
323
+ list_complete: parsed.cursor === undefined,
324
+ ...(parsed.cursor === undefined ? {} : { cursor: parsed.cursor }),
325
+ };
326
+ }
327
+ }
328
+
329
+ class ManagedRuntimeObjectStorage implements IObjectStorage {
330
+ constructor(private readonly options: ManagedRuntimeDataClientOptions) {
331
+ assertResponseLimit(options.maxMetadataResponseBytes);
332
+ assertResponseLimit(options.maxValueResponseBytes);
333
+ }
334
+
335
+ async put(
336
+ key: string,
337
+ value: ReadableStream | ArrayBuffer | string,
338
+ options?: {
339
+ httpMetadata?: ObjectMetadata["httpMetadata"];
340
+ customMetadata?: Record<string, string>;
341
+ },
342
+ ): Promise<void> {
343
+ const request = managedRuntimeObjectRequest(this.options.authority, {
344
+ method: "PUT",
345
+ key,
346
+ idempotencyKey: this.options.idempotencyKey(),
347
+ value: value as BodyInit,
348
+ ...(options?.httpMetadata === undefined
349
+ ? {}
350
+ : { httpMetadata: options.httpMetadata }),
351
+ ...(options?.customMetadata === undefined
352
+ ? {}
353
+ : { customMetadata: options.customMetadata }),
354
+ });
355
+ await expectOkResponse(
356
+ await this.options.gateway.fetch(request),
357
+ this.options.maxMetadataResponseBytes,
358
+ );
359
+ }
360
+
361
+ async get(key: string): Promise<StorageObject | null> {
362
+ const request = managedRuntimeObjectRequest(this.options.authority, {
363
+ method: "GET",
364
+ key,
365
+ idempotencyKey: this.options.idempotencyKey(),
366
+ });
367
+ const raw = await this.options.gateway.fetch(request);
368
+ if (raw.status === 404) {
369
+ await raw.body?.cancel().catch(() => undefined);
370
+ return null;
371
+ }
372
+ return new ManagedRuntimeStorageObject(
373
+ key,
374
+ await checkedResponse(raw, this.options.maxValueResponseBytes),
375
+ );
376
+ }
377
+
378
+ async delete(key: string | string[]): Promise<void> {
379
+ for (const entry of Array.isArray(key) ? key : [key]) {
380
+ const request = managedRuntimeObjectRequest(this.options.authority, {
381
+ method: "DELETE",
382
+ key: entry,
383
+ idempotencyKey: this.options.idempotencyKey(),
384
+ });
385
+ await expectOkResponse(
386
+ await this.options.gateway.fetch(request),
387
+ this.options.maxMetadataResponseBytes,
388
+ );
389
+ }
390
+ }
391
+
392
+ async list(options?: {
393
+ prefix?: string;
394
+ limit?: number;
395
+ cursor?: string;
396
+ delimiter?: string;
397
+ }): Promise<ListObjectsResult> {
398
+ const request = managedRuntimeObjectListRequest(this.options.authority, {
399
+ idempotencyKey: this.options.idempotencyKey(),
400
+ ...options,
401
+ });
402
+ const response = await checkedResponse(
403
+ await this.options.gateway.fetch(request),
404
+ this.options.maxMetadataResponseBytes,
405
+ );
406
+ const parsed = parseManagedRuntimeObjectListResponse(await response.json());
407
+ return {
408
+ objects: parsed.objects.map((entry) => ({
409
+ key: entry.key,
410
+ size: entry.size,
411
+ uploaded: new Date(entry.uploaded),
412
+ ...(entry.etag === undefined ? {} : { etag: entry.etag }),
413
+ })),
414
+ truncated: parsed.truncated,
415
+ ...(parsed.cursor === undefined ? {} : { cursor: parsed.cursor }),
416
+ ...(parsed.delimitedPrefixes === undefined
417
+ ? {}
418
+ : { delimitedPrefixes: [...parsed.delimitedPrefixes] }),
419
+ };
420
+ }
421
+
422
+ async head(key: string): Promise<ObjectMetadata | null> {
423
+ const request = managedRuntimeObjectRequest(this.options.authority, {
424
+ method: "HEAD",
425
+ key,
426
+ idempotencyKey: this.options.idempotencyKey(),
427
+ });
428
+ const raw = await this.options.gateway.fetch(request);
429
+ if (raw.status === 404) {
430
+ await raw.body?.cancel().catch(() => undefined);
431
+ return null;
432
+ }
433
+ const response = await checkedResponse(
434
+ raw,
435
+ this.options.maxMetadataResponseBytes,
436
+ );
437
+ return objectMetadata(response.headers);
438
+ }
439
+ }
440
+
441
+ class ManagedRuntimeStorageObject implements StorageObject {
442
+ constructor(
443
+ readonly key: string,
444
+ private readonly response: Response,
445
+ ) {}
446
+
447
+ get body(): ReadableStream | null {
448
+ return this.response.body;
449
+ }
450
+
451
+ get bodyUsed(): boolean {
452
+ return this.response.bodyUsed;
453
+ }
454
+
455
+ get httpEtag(): string | undefined {
456
+ return this.response.headers.get("etag") ?? undefined;
457
+ }
458
+
459
+ get httpMetadata(): ObjectMetadata["httpMetadata"] {
460
+ return objectMetadata(this.response.headers).httpMetadata;
461
+ }
462
+
463
+ get customMetadata(): Record<string, string> | undefined {
464
+ return objectMetadata(this.response.headers).customMetadata;
465
+ }
466
+
467
+ arrayBuffer(): Promise<ArrayBuffer> {
468
+ return this.response.arrayBuffer();
469
+ }
470
+
471
+ text(): Promise<string> {
472
+ return this.response.text();
473
+ }
474
+
475
+ async json<T = unknown>(): Promise<T> {
476
+ return (await this.response.json()) as T;
477
+ }
478
+ }
479
+
480
+ function objectMetadata(headers: Headers): ObjectMetadata {
481
+ const contentLength = headers.get("content-length");
482
+ const custom = headers.get("x-takosumi-object-custom-metadata");
483
+ let customMetadata: Record<string, string> | undefined;
484
+ if (custom !== null) {
485
+ try {
486
+ const decoded = JSON.parse(decodeURIComponent(custom)) as unknown;
487
+ if (
488
+ decoded === null ||
489
+ typeof decoded !== "object" ||
490
+ Array.isArray(decoded) ||
491
+ Object.values(decoded).some((value) => typeof value !== "string")
492
+ ) {
493
+ throw new Error("invalid");
494
+ }
495
+ customMetadata = decoded as Record<string, string>;
496
+ } catch {
497
+ throw new ManagedRuntimeGatewayError(
498
+ "managed_runtime_object_metadata_invalid",
499
+ 502,
500
+ false,
501
+ );
502
+ }
503
+ }
504
+ const httpMetadata = {
505
+ ...(headers.get("content-type")
506
+ ? { contentType: headers.get("content-type")! }
507
+ : {}),
508
+ ...(headers.get("cache-control")
509
+ ? { cacheControl: headers.get("cache-control")! }
510
+ : {}),
511
+ ...(headers.get("content-disposition")
512
+ ? { contentDisposition: headers.get("content-disposition")! }
513
+ : {}),
514
+ ...(headers.get("content-encoding")
515
+ ? { contentEncoding: headers.get("content-encoding")! }
516
+ : {}),
517
+ ...(headers.get("content-language")
518
+ ? { contentLanguage: headers.get("content-language")! }
519
+ : {}),
520
+ };
521
+ return {
522
+ ...(headers.get("content-type")
523
+ ? { contentType: headers.get("content-type")! }
524
+ : {}),
525
+ ...(contentLength !== null &&
526
+ /^\d+$/u.test(contentLength) &&
527
+ Number.isSafeInteger(Number(contentLength))
528
+ ? { contentLength: Number(contentLength) }
529
+ : {}),
530
+ ...(headers.get("etag") ? { etag: headers.get("etag")! } : {}),
531
+ ...(Object.keys(httpMetadata).length === 0 ? {} : { httpMetadata }),
532
+ ...(customMetadata === undefined ? {} : { customMetadata }),
533
+ };
534
+ }
535
+
536
+ async function checkedResponse(
537
+ response: Response,
538
+ maxBytes: number,
539
+ ): Promise<Response> {
540
+ const bounded = await boundedResponse(response, maxBytes);
541
+ const failure = await managedRuntimeGatewayFailure(bounded.clone());
542
+ if (failure) {
543
+ throw new ManagedRuntimeGatewayError(
544
+ failure.code,
545
+ failure.status,
546
+ failure.retryable,
547
+ );
548
+ }
549
+ return bounded;
550
+ }
551
+
552
+ async function expectOkResponse(
553
+ response: Response,
554
+ maxBytes: number,
555
+ ): Promise<void> {
556
+ const bounded = await checkedResponse(response, maxBytes);
557
+ const body = (await bounded.json().catch(() => undefined)) as
558
+ { readonly ok?: unknown } | undefined;
559
+ if (body?.ok !== true || Object.keys(body).some((key) => key !== "ok")) {
560
+ throw new ManagedRuntimeGatewayError(
561
+ "managed_runtime_response_invalid",
562
+ 502,
563
+ false,
564
+ );
565
+ }
566
+ }
567
+
568
+ function assertResponseLimit(value: number): void {
569
+ if (!Number.isSafeInteger(value) || value < 1) {
570
+ throw new TypeError("managed_runtime_response_limit_invalid");
571
+ }
572
+ }
573
+
574
+ async function boundedResponse(
575
+ response: Response,
576
+ maxBytes: number,
577
+ ): Promise<Response> {
578
+ const declaredLength = response.headers.get("content-length");
579
+ if (
580
+ declaredLength !== null &&
581
+ (!/^\d+$/u.test(declaredLength) || Number(declaredLength) > maxBytes)
582
+ ) {
583
+ throw new ManagedRuntimeGatewayError(
584
+ "managed_runtime_response_too_large",
585
+ 502,
586
+ false,
587
+ );
588
+ }
589
+
590
+ const reader = response.body?.getReader();
591
+ if (!reader) {
592
+ return new Response(null, {
593
+ status: response.status,
594
+ statusText: response.statusText,
595
+ headers: response.headers,
596
+ });
597
+ }
598
+
599
+ const chunks: Uint8Array[] = [];
600
+ let size = 0;
601
+ try {
602
+ while (true) {
603
+ const { done, value } = await reader.read();
604
+ if (done) break;
605
+ size += value.byteLength;
606
+ if (size > maxBytes) {
607
+ await reader.cancel("managed_runtime_response_too_large");
608
+ throw new ManagedRuntimeGatewayError(
609
+ "managed_runtime_response_too_large",
610
+ 502,
611
+ false,
612
+ );
613
+ }
614
+ chunks.push(value);
615
+ }
616
+ } finally {
617
+ reader.releaseLock();
618
+ }
619
+
620
+ const body = new Uint8Array(size);
621
+ let offset = 0;
622
+ for (const chunk of chunks) {
623
+ body.set(chunk, offset);
624
+ offset += chunk.byteLength;
625
+ }
626
+ return new Response(body, {
627
+ status: response.status,
628
+ statusText: response.statusText,
629
+ headers: response.headers,
630
+ });
631
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Runtime-neutral queue ports used by the application.
3
+ *
4
+ * These deliberately model only the semantics Yurucommu uses. A runtime may
5
+ * back them with Cloudflare Queues, a local scheduler, or a host gateway, but
6
+ * app code never receives a provider-native queue object.
7
+ */
8
+
9
+ export interface QueueSendOptions {
10
+ readonly delaySeconds?: number;
11
+ }
12
+
13
+ export interface QueueBatchItem<T> {
14
+ readonly body: T;
15
+ readonly delaySeconds?: number;
16
+ }
17
+
18
+ export interface IQueueProducer<T> {
19
+ send(body: T, options?: QueueSendOptions): Promise<void>;
20
+ sendBatch(
21
+ messages: readonly QueueBatchItem<T>[],
22
+ options?: QueueSendOptions,
23
+ ): Promise<void>;
24
+ }
25
+
26
+ export interface IQueueMessage<T> {
27
+ readonly id: string;
28
+ readonly timestamp: Date;
29
+ readonly body: T;
30
+ readonly attempts: number;
31
+ ack(): void;
32
+ retry(options?: QueueSendOptions): void;
33
+ }
34
+
35
+ export interface IQueueBatch<T> {
36
+ readonly queue: string;
37
+ readonly messages: readonly IQueueMessage<T>[];
38
+ ackAll(): void;
39
+ retryAll(options?: QueueSendOptions): void;
40
+ }