@serve.zone/coremail 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/changelog.md +22 -0
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.auth.d.ts +19 -0
  4. package/dist_ts/classes.auth.js +58 -1
  5. package/dist_ts/classes.coremail.d.ts +3 -0
  6. package/dist_ts/classes.coremail.js +11 -4
  7. package/dist_ts/classes.gateway.d.ts +11 -1
  8. package/dist_ts/classes.gateway.js +101 -63
  9. package/dist_ts/classes.inbound.d.ts +10 -1
  10. package/dist_ts/classes.inbound.js +45 -7
  11. package/dist_ts/classes.maintenance.js +16 -1
  12. package/dist_ts/classes.models.d.ts +24 -2
  13. package/dist_ts/classes.models.js +72 -2
  14. package/dist_ts/classes.server.d.ts +6 -12
  15. package/dist_ts/classes.server.js +18 -78
  16. package/dist_ts/classes.smtpsubmission.d.ts +85 -0
  17. package/dist_ts/classes.smtpsubmission.js +379 -0
  18. package/dist_ts/classes.storage.d.ts +9 -0
  19. package/dist_ts/classes.storage.js +44 -1
  20. package/dist_ts/classes.submissions.d.ts +27 -1
  21. package/dist_ts/classes.submissions.js +164 -14
  22. package/dist_ts/coremail.errors.d.ts +18 -0
  23. package/dist_ts/coremail.errors.js +84 -0
  24. package/dist_ts/coremail.log.d.ts +1 -1
  25. package/dist_ts/coremail.log.js +1 -1
  26. package/dist_ts/coremail.mime.d.ts +15 -0
  27. package/dist_ts/coremail.mime.js +76 -1
  28. package/dist_ts/coremail.persistence.d.ts +38 -1
  29. package/dist_ts/coremail.persistence.js +124 -40
  30. package/dist_ts/coremail.stats.d.ts +72 -0
  31. package/dist_ts/coremail.stats.js +242 -0
  32. package/dist_ts/coremail.validation.d.ts +15 -2
  33. package/dist_ts/coremail.validation.js +26 -78
  34. package/dist_ts/interfaces.d.ts +18 -0
  35. package/dist_ts/plugins.d.ts +1 -0
  36. package/dist_ts/plugins.js +2 -1
  37. package/package.json +4 -3
  38. package/readme.md +51 -2
  39. package/ts/00_commitinfo_data.ts +1 -1
  40. package/ts/classes.auth.ts +77 -0
  41. package/ts/classes.coremail.ts +20 -0
  42. package/ts/classes.gateway.ts +121 -66
  43. package/ts/classes.inbound.ts +71 -6
  44. package/ts/classes.maintenance.ts +14 -0
  45. package/ts/classes.models.ts +67 -1
  46. package/ts/classes.server.ts +22 -86
  47. package/ts/classes.smtpsubmission.ts +466 -0
  48. package/ts/classes.storage.ts +54 -0
  49. package/ts/classes.submissions.ts +247 -12
  50. package/ts/coremail.errors.ts +111 -0
  51. package/ts/coremail.log.ts +1 -0
  52. package/ts/coremail.mime.ts +89 -0
  53. package/ts/coremail.persistence.ts +196 -48
  54. package/ts/coremail.stats.ts +342 -0
  55. package/ts/coremail.validation.ts +31 -103
  56. package/ts/interfaces.ts +20 -0
  57. package/ts/plugins.ts +1 -0
@@ -0,0 +1,466 @@
1
+ import * as plugins from './plugins.js';
2
+ import type { CoreMailAuth } from './classes.auth.js';
3
+ import { stableStringify } from './classes.gateway.js';
4
+ import type { CoreMailSubmissionService } from './classes.submissions.js';
5
+ import { logCoreMailFailure } from './coremail.log.js';
6
+ import { CoreMailQuotaExceededError } from './coremail.quota.js';
7
+ import type {
8
+ ICoreMailConfig,
9
+ ICoreMailWorkloadSession,
10
+ } from './interfaces.js';
11
+
12
+ type TDesiredState = plugins.serveZoneInterfaces.data.ICoreMailDesiredState;
13
+ type TSmtpDesired = plugins.serveZoneInterfaces.data.ICoreMailSmtpDesiredState;
14
+ type TBinding = plugins.serveZoneInterfaces.data.ICoreMailBindingDesiredState;
15
+
16
+ /** Session grants live no longer than smartmta's own hook-grant window. */
17
+ const sessionTtlMs = 60 * 60 * 1000;
18
+ /** Hard cap mirroring smartmta's hook-grant table. */
19
+ const sessionMaximum = 4_096;
20
+ const reconcileIntervalMs = 1_000;
21
+ /** First retry delay after a failed start. */
22
+ const startBackoffBaseMs = 1_000;
23
+ /** Ceiling for the consecutive-failure backoff. */
24
+ const startBackoffMaximumMs = 60_000;
25
+
26
+ interface ICoreMailSmtpSessionGrant {
27
+ session: ICoreMailWorkloadSession;
28
+ binding: TBinding;
29
+ expiresAt: number;
30
+ }
31
+
32
+ const waitForAbortableDelay = async (
33
+ delayMsArg: number,
34
+ signalArg: AbortSignal,
35
+ ): Promise<void> => {
36
+ if (signalArg.aborted) return;
37
+ await new Promise<void>((resolveArg) => {
38
+ const timer = setTimeout(() => {
39
+ signalArg.removeEventListener('abort', onAbort);
40
+ resolveArg();
41
+ }, delayMsArg);
42
+ const onAbort = () => {
43
+ clearTimeout(timer);
44
+ resolveArg();
45
+ };
46
+ signalArg.addEventListener('abort', onAbort, { once: true });
47
+ });
48
+ };
49
+
50
+ /**
51
+ * CoreMail's SMTP submission listener (MSA).
52
+ *
53
+ * smartmta owns the transport only: it terminates TLS, runs SMTP AUTH over an
54
+ * encrypted channel, and hands every accepted message to `onMessageData` as
55
+ * the exact received bytes. Nothing is stored, queued or forwarded by it —
56
+ * `onMessageData` returns without `continueProcessing`, so the message ends at
57
+ * `CoreMailSubmissionService.submitRaw` and CoreMail's own durable outbound
58
+ * pipeline. The `MemoryStorageManager` handed to smartmta is therefore
59
+ * expected to stay empty for the life of the process; a test asserts that.
60
+ *
61
+ * The listener fails closed. It runs only when the desired state enables it
62
+ * and both PEM secrets resolve, because smartmta's Rust listener refuses AUTH
63
+ * on a cleartext session (538 5.7.11) and a listener without certificate
64
+ * material would advertise neither STARTTLS nor AUTH.
65
+ */
66
+ export class CoreMailSmtpSubmissionServer {
67
+ private server: InstanceType<
68
+ typeof plugins.smartmta.UnifiedEmailServer
69
+ > | null = null;
70
+ private storageManager: InstanceType<
71
+ typeof plugins.smartmta.MemoryStorageManager
72
+ > | null = null;
73
+ private serverSignature = '';
74
+ private readonly stopController = new AbortController();
75
+ private reconcileTask: Promise<void> | null = null;
76
+ private readonly sessions = new Map<string, ICoreMailSmtpSessionGrant>();
77
+ private unavailableReason: string | null = 'CoreMail SMTP listener has not started.';
78
+ private loggedUnavailableSignature = '';
79
+ /** Signature of the configuration the last start was attempted for. */
80
+ private attemptedSignature = '';
81
+ private startFailures = 0;
82
+ private reportedUnavailable = 0;
83
+ private nextStartAttemptAt = 0;
84
+
85
+ public constructor(
86
+ private readonly config: ICoreMailConfig,
87
+ private readonly auth: CoreMailAuth,
88
+ private readonly submissions: CoreMailSubmissionService,
89
+ private readonly getDesiredState: () => Promise<TDesiredState | null>,
90
+ private readonly now: () => number = Date.now,
91
+ ) {}
92
+
93
+ public start(): void {
94
+ this.reconcileTask ??= this.runReconciliationLoop();
95
+ }
96
+
97
+ public async stop(): Promise<void> {
98
+ this.stopController.abort();
99
+ await this.reconcileTask?.catch(() => undefined);
100
+ this.reconcileTask = null;
101
+ await this.shutdownServer();
102
+ this.sessions.clear();
103
+ this.resetStartBackoff('');
104
+ }
105
+
106
+ /** Whether the listener is currently accepting submissions. */
107
+ public isReady(): boolean {
108
+ return this.server !== null && this.unavailableReason === null;
109
+ }
110
+
111
+ /**
112
+ * Operator-facing view of the start-retry state. A listener that cannot bind
113
+ * (busy port, unusable key pair, missing mailer-bin) backs off instead of
114
+ * respawning the Rust bridge once per reconcile tick.
115
+ */
116
+ public getListenerDiagnostics(): {
117
+ startFailures: number;
118
+ reportedUnavailable: number;
119
+ nextStartAttemptAt: number;
120
+ } {
121
+ return {
122
+ startFailures: this.startFailures,
123
+ reportedUnavailable: this.reportedUnavailable,
124
+ nextStartAttemptAt: this.nextStartAttemptAt,
125
+ };
126
+ }
127
+
128
+ /** In-memory storage handed to smartmta, for the emptiness assertion. */
129
+ public getStorageManager(): InstanceType<
130
+ typeof plugins.smartmta.MemoryStorageManager
131
+ > | null {
132
+ return this.storageManager;
133
+ }
134
+
135
+ private async runReconciliationLoop(): Promise<void> {
136
+ while (!this.stopController.signal.aborted) {
137
+ try {
138
+ await this.reconcile();
139
+ } catch (error) {
140
+ await logCoreMailFailure('smtp', 'RECONCILIATION_FAILED', error)
141
+ .catch(() => undefined);
142
+ }
143
+ await waitForAbortableDelay(
144
+ reconcileIntervalMs,
145
+ this.stopController.signal,
146
+ );
147
+ }
148
+ }
149
+
150
+ private async reconcile(): Promise<void> {
151
+ const desired = await this.getDesiredState();
152
+ const smtp = desired?.smtp;
153
+ if (!smtp || !smtp.enabled) {
154
+ await this.markUnavailable(
155
+ smtp ? 'CoreMail SMTP listener is disabled.' : 'CoreMail SMTP listener is not configured.',
156
+ stableStringify(smtp ?? null),
157
+ );
158
+ return;
159
+ }
160
+ const material = this.resolveTlsMaterial(smtp);
161
+ if (!material) {
162
+ await this.markUnavailable(
163
+ 'CoreMail SMTP listener TLS material does not resolve.',
164
+ stableStringify(smtp),
165
+ );
166
+ return;
167
+ }
168
+ const signature = stableStringify({ smtp, certPem: material.certPem });
169
+ if (this.server && signature === this.serverSignature) return;
170
+ if (signature === this.attemptedSignature) {
171
+ // The same configuration failed already; wait out its backoff rather
172
+ // than spawning and tearing down the Rust bridge every tick.
173
+ if (this.now() < this.nextStartAttemptAt) return;
174
+ } else {
175
+ // A changed smtp section always restarts immediately.
176
+ this.resetStartBackoff(signature);
177
+ }
178
+ await this.shutdownServer();
179
+ if (this.stopController.signal.aborted) return;
180
+ await this.startServer(smtp, material, signature);
181
+ }
182
+
183
+ private resetStartBackoff(signatureArg: string): void {
184
+ this.attemptedSignature = signatureArg;
185
+ this.startFailures = 0;
186
+ this.nextStartAttemptAt = 0;
187
+ }
188
+
189
+ /**
190
+ * Resolve both PEM secrets. A listener that cannot present a certificate
191
+ * must never come up: smartmta would then offer neither STARTTLS nor AUTH,
192
+ * and any client configured for submission would fail open into plaintext.
193
+ */
194
+ private resolveTlsMaterial(
195
+ smtpArg: TSmtpDesired,
196
+ ): { certPem: string; keyPem: string } | null {
197
+ try {
198
+ const certPem = this.config.resolveRuntimeSecret(
199
+ smtpArg.tls.certificatePemSecretKey,
200
+ );
201
+ const keyPem = this.config.resolveRuntimeSecret(
202
+ smtpArg.tls.privateKeyPemSecretKey,
203
+ );
204
+ if (
205
+ typeof certPem !== 'string'
206
+ || !certPem.includes('BEGIN')
207
+ || typeof keyPem !== 'string'
208
+ || !keyPem.includes('BEGIN')
209
+ ) {
210
+ return null;
211
+ }
212
+ return { certPem, keyPem };
213
+ } catch {
214
+ return null;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Record why the listener is not serving and log it once per distinct
220
+ * reason. Repeating the same reason every reconcile tick would drown the
221
+ * operator in noise without adding information.
222
+ */
223
+ private async reportUnavailable(
224
+ reasonArg: string,
225
+ signatureArg: string,
226
+ codeArg: 'LISTENER_UNAVAILABLE' | 'LISTENER_START_FAILED',
227
+ errorArg?: unknown,
228
+ ): Promise<void> {
229
+ await this.shutdownServer();
230
+ this.unavailableReason = reasonArg;
231
+ const logSignature = `${codeArg}:${signatureArg}`;
232
+ if (this.loggedUnavailableSignature === logSignature) return;
233
+ this.loggedUnavailableSignature = logSignature;
234
+ this.reportedUnavailable += 1;
235
+ await logCoreMailFailure('smtp', codeArg, errorArg, {
236
+ reason: reasonArg,
237
+ }).catch(() => undefined);
238
+ }
239
+
240
+ /**
241
+ * The configuration is not asking for a listener right now, so no start is
242
+ * pending and a later enable must take effect on its first tick.
243
+ */
244
+ private async markUnavailable(
245
+ reasonArg: string,
246
+ signatureArg: string,
247
+ ): Promise<void> {
248
+ this.resetStartBackoff('');
249
+ await this.reportUnavailable(reasonArg, signatureArg, 'LISTENER_UNAVAILABLE');
250
+ }
251
+
252
+ private async startServer(
253
+ smtpArg: TSmtpDesired,
254
+ materialArg: { certPem: string; keyPem: string },
255
+ signatureArg: string,
256
+ ): Promise<void> {
257
+ const storageManager = new plugins.smartmta.MemoryStorageManager();
258
+ const server = new plugins.smartmta.UnifiedEmailServer({ storageManager }, {
259
+ ports: [smtpArg.port],
260
+ hostname: smtpArg.hostname,
261
+ // CoreMail hosts no inbound domain of its own here: this listener only
262
+ // relays authenticated submissions to the durable outbound pipeline.
263
+ domains: [],
264
+ routes: [{
265
+ name: 'submission',
266
+ match: { authenticated: true },
267
+ action: { type: 'process', allowRelay: true },
268
+ }],
269
+ persistRoutes: false,
270
+ dkimKeyProvisioning: 'caller-managed',
271
+ tls: { certPem: materialArg.certPem, keyPem: materialArg.keyPem },
272
+ auth: { required: true },
273
+ queue: { storageMode: 'memory' },
274
+ smtp: {
275
+ maxConcurrentMessages: 16,
276
+ recipientValidation: false,
277
+ authenticationTimeoutMs: 4_000,
278
+ messageAcceptanceTimeoutMs: 30_000,
279
+ },
280
+ maxMessageSize:
281
+ plugins.serveZoneInterfaces.data.coreMailLimits.serializedMimeBytes,
282
+ hooks: {
283
+ onAuthenticate: (contextArg) => this.onAuthenticate(contextArg),
284
+ onMessageData: (contextArg) => this.onMessageData(contextArg),
285
+ },
286
+ });
287
+ try {
288
+ await server.start();
289
+ } catch (error) {
290
+ await server.stop().catch(() => undefined);
291
+ this.startFailures += 1;
292
+ this.nextStartAttemptAt = this.now() + Math.min(
293
+ startBackoffMaximumMs,
294
+ startBackoffBaseMs * 2 ** (this.startFailures - 1),
295
+ );
296
+ await this.reportUnavailable(
297
+ 'CoreMail SMTP listener failed to start.',
298
+ signatureArg,
299
+ 'LISTENER_START_FAILED',
300
+ error,
301
+ );
302
+ return;
303
+ }
304
+ this.server = server;
305
+ this.storageManager = storageManager;
306
+ this.serverSignature = signatureArg;
307
+ this.unavailableReason = null;
308
+ this.loggedUnavailableSignature = '';
309
+ this.startFailures = 0;
310
+ this.nextStartAttemptAt = 0;
311
+ }
312
+
313
+ private async shutdownServer(): Promise<void> {
314
+ const server = this.server;
315
+ this.server = null;
316
+ this.serverSignature = '';
317
+ this.sessions.clear();
318
+ if (!server) return;
319
+ await server.stop().catch(async (error) => {
320
+ await logCoreMailFailure('smtp', 'LISTENER_SHUTDOWN_FAILED', error)
321
+ .catch(() => undefined);
322
+ });
323
+ }
324
+
325
+ private pruneSessions(): void {
326
+ const now = this.now();
327
+ for (const [sessionId, grant] of this.sessions) {
328
+ if (grant.expiresAt <= now) this.sessions.delete(sessionId);
329
+ }
330
+ while (this.sessions.size > sessionMaximum) {
331
+ const oldest = this.sessions.keys().next();
332
+ if (oldest.done) break;
333
+ this.sessions.delete(oldest.value);
334
+ }
335
+ }
336
+
337
+ private async onAuthenticate(
338
+ contextArg: plugins.smartmta.ISmtpAuthenticateContext,
339
+ ): Promise<plugins.smartmta.ISmtpAuthenticateDecision> {
340
+ // The SMTP username is the binding id; the password is any accepted
341
+ // credential secret of that binding.
342
+ const authenticated = await this.auth.authenticateSmtpSubmission(
343
+ contextArg.username,
344
+ contextArg.password,
345
+ );
346
+ if (!authenticated) {
347
+ return { accepted: false, message: 'CoreMail binding credential rejected.' };
348
+ }
349
+ const { binding } = authenticated;
350
+ const session: ICoreMailWorkloadSession = {
351
+ kind: 'workload',
352
+ peerId: `smtp:${contextArg.sessionId}`,
353
+ tenantId: binding.tenantId,
354
+ serviceId: binding.serviceId,
355
+ bindingId: binding.bindingId,
356
+ bindingRevision: binding.revision,
357
+ configEpoch: authenticated.configEpoch,
358
+ credentialId: authenticated.credentialId,
359
+ credentialVersion: authenticated.credentialVersion,
360
+ bindingState: binding.state === 'draining' ? 'draining' : 'active',
361
+ capabilities: [...binding.capabilities],
362
+ allowedOperations: [
363
+ ...plugins.serveZoneInterfaces.data.resolveCoreMailWorkloadOperations(
364
+ binding.state,
365
+ binding.capabilities,
366
+ ),
367
+ ],
368
+ coreMailTransferOrigin: '-',
369
+ authenticatedAt: this.now(),
370
+ };
371
+ this.sessions.delete(contextArg.sessionId);
372
+ this.sessions.set(contextArg.sessionId, {
373
+ session,
374
+ binding,
375
+ expiresAt: this.now() + sessionTtlMs,
376
+ });
377
+ this.pruneSessions();
378
+ return {
379
+ accepted: true,
380
+ // smartmta enforces the same sender scope CoreMail enforces again in
381
+ // submitRaw, so an out-of-scope MAIL FROM is refused at RCPT time.
382
+ scope: { senders: [...binding.allowedSenders] },
383
+ };
384
+ }
385
+
386
+ private async onMessageData(
387
+ contextArg: plugins.smartmta.IMessageAcceptanceContext,
388
+ ): Promise<plugins.smartmta.IMessageAcceptanceDecision> {
389
+ const grant = this.sessions.get(contextArg.session.id);
390
+ if (!grant || grant.expiresAt <= this.now()) {
391
+ this.sessions.delete(contextArg.session.id);
392
+ return {
393
+ accepted: false,
394
+ smtpCode: 451,
395
+ smtpMessage: '4.7.1 Authentication scope unavailable; authenticate again',
396
+ };
397
+ }
398
+ try {
399
+ // smartmta strips the CRLF that preceded the DATA terminator; the
400
+ // durable raw queue requires canonical CRLF-terminated bytes.
401
+ const rawMime = new Uint8Array(contextArg.rawMessage.byteLength + 2);
402
+ rawMime.set(contextArg.rawMessage, 0);
403
+ rawMime.set([0x0d, 0x0a], contextArg.rawMessage.byteLength);
404
+ await this.submissions.submitRaw(grant.session, grant.binding, {
405
+ envelope: {
406
+ from: contextArg.session.envelope.mailFrom.address,
407
+ to: contextArg.session.envelope.rcptTo.map(
408
+ (recipientArg) => recipientArg.address,
409
+ ),
410
+ },
411
+ rawMime,
412
+ });
413
+ // No continueProcessing: smartmta stores, queues and forwards nothing.
414
+ return { accepted: true };
415
+ } catch (error) {
416
+ return this.refusalFor(error);
417
+ }
418
+ }
419
+
420
+ private refusalFor(
421
+ errorArg: unknown,
422
+ ): plugins.smartmta.IMessageAcceptanceDecision {
423
+ if (errorArg instanceof CoreMailQuotaExceededError) {
424
+ return {
425
+ accepted: false,
426
+ smtpCode: 452,
427
+ smtpMessage: '4.2.1 Mail quota exceeded; try again later',
428
+ };
429
+ }
430
+ const message = errorArg instanceof Error ? errorArg.message : '';
431
+ if (message.includes('sender is not authorized')) {
432
+ return {
433
+ accepted: false,
434
+ smtpCode: 553,
435
+ smtpMessage: '5.7.1 Sender address not permitted',
436
+ };
437
+ }
438
+ if (message.includes('byte budget')) {
439
+ return {
440
+ accepted: false,
441
+ smtpCode: 552,
442
+ smtpMessage: '5.3.4 Message exceeds its size limit',
443
+ };
444
+ }
445
+ if (
446
+ errorArg instanceof Error
447
+ && (
448
+ errorArg.name === 'CoreMailCapabilityDeniedError'
449
+ || errorArg.name === 'CoreMailAuthorizationError'
450
+ )
451
+ ) {
452
+ return {
453
+ accepted: false,
454
+ smtpCode: 550,
455
+ smtpMessage: '5.7.1 Submission is not permitted by this authority',
456
+ };
457
+ }
458
+ void logCoreMailFailure('smtp', 'SUBMISSION_FAILED', errorArg)
459
+ .catch(() => undefined);
460
+ return {
461
+ accepted: false,
462
+ smtpCode: 451,
463
+ smtpMessage: '4.3.0 Submission temporarily unavailable',
464
+ };
465
+ }
466
+ }
@@ -372,6 +372,60 @@ export class CoreMailStorage {
372
372
  );
373
373
  }
374
374
 
375
+ /**
376
+ * Read at most `headBytesArg` leading bytes of a stored object.
377
+ *
378
+ * The object's integrity was already proven when its transfer grant was
379
+ * consumed, so a header scan does not need to re-hash the whole payload — it
380
+ * reads a bounded prefix and cancels. The verified full-object readers above
381
+ * remain the only way to obtain content that is acted on as a whole.
382
+ */
383
+ public async readObjectHeadBytes(
384
+ objectKeyArg: string,
385
+ lengthBytesArg: number,
386
+ sha256Arg: TCoreMailSha256,
387
+ maximumBytesArg: number,
388
+ headBytesArg: number,
389
+ signalArg?: AbortSignal,
390
+ ): Promise<Uint8Array> {
391
+ if (!Number.isSafeInteger(headBytesArg) || headBytesArg < 1) {
392
+ throw new Error('CoreMail object head read length is invalid.');
393
+ }
394
+ const verified = await this.getVerifiedStream(
395
+ objectKeyArg,
396
+ lengthBytesArg,
397
+ sha256Arg,
398
+ maximumBytesArg,
399
+ signalArg,
400
+ );
401
+ const reader = verified.stream.getReader();
402
+ const chunks: Uint8Array[] = [];
403
+ let total = 0;
404
+ try {
405
+ while (total < headBytesArg) {
406
+ const next = await reader.read();
407
+ if (next.done) break;
408
+ chunks.push(next.value);
409
+ total += next.value.byteLength;
410
+ }
411
+ } finally {
412
+ await reader.cancel().catch(() => undefined);
413
+ reader.releaseLock();
414
+ await verified.cancel().catch(() => undefined);
415
+ // The digest is deliberately not awaited: only a prefix was consumed.
416
+ verified.completion.catch(() => undefined);
417
+ }
418
+ const head = new Uint8Array(Math.min(total, headBytesArg));
419
+ let offset = 0;
420
+ for (const chunk of chunks) {
421
+ if (offset >= head.byteLength) break;
422
+ const slice = chunk.subarray(0, head.byteLength - offset);
423
+ head.set(slice, offset);
424
+ offset += slice.byteLength;
425
+ }
426
+ return head;
427
+ }
428
+
375
429
  public async getVerifiedBytes(
376
430
  objectKeyArg: string,
377
431
  lengthBytesArg: number,