@marktiderman/genesis-forms 0.7.0 → 0.8.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.
@@ -1,4 +1,4 @@
1
- import { J as Json, F as FormDefinition, a as FieldBlock } from './types-oAckGR_Z.cjs';
1
+ import { J as Json, F as FormDefinition, a as FieldBlock, C as Condition } from './types-jqDKcmP4.cjs';
2
2
 
3
3
  /**
4
4
  * The contract every destination keeps. A destination sends one checked
@@ -53,12 +53,26 @@ interface DeliveryOptions {
53
53
  redelivery?: boolean | undefined;
54
54
  /** Aborts the delivery. Each request also has its own timeout. */
55
55
  signal?: AbortSignal | undefined;
56
+ /**
57
+ * The submission was delivered before and this is its update (an edit).
58
+ * The sheet overwrites the response's row where it is (appending only when
59
+ * it is gone), and the webhook's event type becomes `form.updated`.
60
+ */
61
+ update?: boolean | undefined;
62
+ /**
63
+ * The app's id for the destination. Passed on to option functions (a
64
+ * secret, an account, an access token, a URL), so they can look up what
65
+ * is stored for it.
66
+ */
67
+ destinationId?: string | undefined;
56
68
  }
57
69
  /** One submission in a batch: each carries its own definition and delivery id. */
58
70
  interface DeliveryItem {
59
71
  submission: DeliverySubmission;
60
72
  definition: FormDefinition;
61
73
  deliveryId?: string | undefined;
74
+ /** This item is an update of a submission delivered before (see `DeliveryOptions.update`). */
75
+ update?: boolean | undefined;
62
76
  }
63
77
  type DeliveryResult = {
64
78
  ok: true;
@@ -66,6 +80,12 @@ type DeliveryResult = {
66
80
  state?: Record<string, Json>;
67
81
  /** Submission ids already there (a redelivery), so nothing was sent for them. */
68
82
  skipped?: string[];
83
+ /**
84
+ * Set when the destination deliberately sent nothing (no address to
85
+ * send to, a daily cap reached): the reason, safe to store. Not a
86
+ * failure and never retried; `createDeliveryRunner` records it as skipped.
87
+ */
88
+ notSent?: string;
69
89
  } | DeliveryFailure;
70
90
  interface DeliveryFailure {
71
91
  ok: false;
@@ -149,6 +169,15 @@ interface HttpDeps {
149
169
  /** Per request. Default `REQUEST_TIMEOUT_MS`. */
150
170
  timeoutMs?: number | undefined;
151
171
  }
172
+ /** What an option function is told besides the config. */
173
+ interface ResolveContext {
174
+ /** `DeliveryOptions.destinationId`, when the app gave one. */
175
+ destinationId?: string | undefined;
176
+ }
177
+ /** A factory option worked out per delivery from the destination's config. */
178
+ type OptionResolver<T> = (config: DestinationConfig, context: ResolveContext) => T | Promise<T>;
179
+ /** WebCrypto's key type, named without depending on the DOM lib. */
180
+ type WebCryptoKey = Awaited<ReturnType<typeof crypto.subtle.importKey>>;
152
181
 
153
182
  interface RowOptions {
154
183
  /**
@@ -342,8 +371,18 @@ interface SheetConfig {
342
371
  sheetName?: string;
343
372
  }
344
373
  interface SheetsDestinationOptions extends HttpDeps {
345
- /** The service account, or a function that finds the one for a destination's config. */
346
- account: GoogleServiceAccount | ((config: DestinationConfig) => GoogleServiceAccount | Promise<GoogleServiceAccount>);
374
+ /**
375
+ * The service account the sheet is shared with, or a function that finds
376
+ * the one for a destination's config. Give this or `accessToken`.
377
+ */
378
+ account?: GoogleServiceAccount | OptionResolver<GoogleServiceAccount> | undefined;
379
+ /**
380
+ * An access token for the sheet instead of a service account: a sheet a
381
+ * person connected with Google sign-in, say (see `googleOAuthAccessToken`).
382
+ * Gets the config and `{ destinationId }`; called once per delivery or
383
+ * batch, so cache inside it.
384
+ */
385
+ accessToken?: OptionResolver<string> | undefined;
347
386
  /**
348
387
  * Prefix text a spreadsheet could run as a formula with `'` (see
349
388
  * `escapeFormula`), headers included. Default false: writes are RAW, so
@@ -381,6 +420,47 @@ interface SheetState {
381
420
  }
382
421
  declare function sheetsDestination(options: SheetsDestinationOptions): Destination;
383
422
 
423
+ /**
424
+ * Where a Slack or webhook destination may post, checked against SSRF: https
425
+ * only, on the default port, with no credentials in the address, and only to
426
+ * a public host, never a private, loopback, link-local or otherwise internal
427
+ * one. `parseHookUrl` checks the text (use it when someone adds the address);
428
+ * `checkHookHost` checks every address the name resolves to right now (use it
429
+ * before every request: what a name points to can change).
430
+ *
431
+ * A check before the request still leaves a short window in which the name
432
+ * could be re-pointed (DNS rebinding). Where untrusted people choose URLs,
433
+ * also send from a network that cannot reach internal services.
434
+ */
435
+
436
+ type HookKind = "slack" | "webhook";
437
+ /** Resolves a host name to its addresses (IPv4 and IPv6), as the server's DNS sees them. In Node: `dns.promises.lookup(host, { all: true })` mapped to `.address`. */
438
+ type ResolveHost = (host: string) => Promise<string[]>;
439
+ declare const MAX_HOOK_URL_LENGTH = 2048;
440
+ /** The hosts a Slack incoming webhook lives on. */
441
+ declare const SLACK_HOSTS: ReadonlySet<string>;
442
+ /** An address that can't be used, with a reason safe to show and to store (never the address). */
443
+ declare class HookUrlError extends DeliveryError {
444
+ name: string;
445
+ }
446
+ /** Whether an IP address (v4 or v6) is a public one a hook may reach. Anything unparsable is not. */
447
+ declare function isPublicAddress(ip: string): boolean;
448
+ /**
449
+ * The address as it will be used (its fragment dropped), or a `HookUrlError`
450
+ * saying what is wrong with it. Checks the text only; `checkHookHost` checks
451
+ * where the name resolves. A `"slack"` address must be a Slack incoming
452
+ * webhook (`https://hooks.slack.com/services/…`).
453
+ */
454
+ declare function parseHookUrl(raw: string, kind?: HookKind): URL;
455
+ /**
456
+ * Checks that every address the URL's host resolves to is public. A host
457
+ * that resolves to nothing, or whose lookup fails, is refused too, as a
458
+ * retryable failure (DNS may be briefly down); a private address is final.
459
+ */
460
+ declare function checkHookHost(url: URL, resolve: ResolveHost): Promise<void>;
461
+ /** The host as a screen may show it (never the path: a Slack or webhook path can be the secret). */
462
+ declare function hookHost(url: URL): string;
463
+
384
464
  /** What the webhook destination reads from a destination's config. */
385
465
  interface WebhookConfig {
386
466
  url: string;
@@ -395,15 +475,28 @@ interface WebhookDestinationOptions extends HttpDeps {
395
475
  * base64 fails permanently. Or a function that finds the one for a
396
476
  * destination's config. Never put it in config.
397
477
  */
398
- secret: string | ((config: DestinationConfig) => string | Promise<string>);
478
+ secret: string | OptionResolver<string>;
399
479
  /** Allow `http:` URLs. Default false. */
400
480
  allowInsecureUrls?: boolean | undefined;
401
481
  /** Allow localhost and private, loopback and link-local address literals. Default false. */
402
482
  allowPrivateHosts?: boolean | undefined;
403
483
  /** Sensitive fields: `"include"` (default), `"mask"` or `"omit"`. */
404
484
  sensitive?: RowOptions["sensitive"];
405
- /** The event type in the payload. Default `"form.submitted"`. */
485
+ /** The event type in the payload. Default `"form.submitted"`, or `"form.updated"` for an update. */
406
486
  type?: string | undefined;
487
+ /**
488
+ * Where to post, when it is not `config.url`: the address kept apart from
489
+ * config (encrypted, say, since a webhook URL can carry a token). Gets the
490
+ * config and `{ destinationId }`. It passes the same checks.
491
+ */
492
+ url?: OptionResolver<string> | undefined;
493
+ /**
494
+ * Resolves the URL's host before every request, and the delivery is refused
495
+ * unless every address is public (see `checkHookHost`). This catches a
496
+ * hostname that points at a private address, which the URL checks alone
497
+ * cannot. Recommended wherever untrusted people choose URLs.
498
+ */
499
+ resolveHost?: ResolveHost | undefined;
407
500
  now?: (() => number) | undefined;
408
501
  }
409
502
  /** The JSON body a webhook receives. */
@@ -431,6 +524,12 @@ interface WebhookPayload {
431
524
  row: Record<string, Json>;
432
525
  };
433
526
  }
527
+ /**
528
+ * A new signing secret: `whsec_` and standard base64 of 32 random bytes, the
529
+ * form Standard Webhooks libraries read. Show it to the receiver once; keep it
530
+ * out of config.
531
+ */
532
+ declare function newWebhookSecret(): string;
434
533
  /** The `webhook-signature` value for a message: `v1,<base64>`. */
435
534
  declare function signWebhook(secret: string, id: string, timestamp: number, body: string): Promise<string>;
436
535
  interface VerifyWebhookInput {
@@ -462,4 +561,293 @@ declare function webhookPayload(submission: DeliverySubmission, definition: Form
462
561
  }): WebhookPayload;
463
562
  declare function webhookDestination(options: WebhookDestinationOptions): Destination;
464
563
 
465
- export { type Cell, DEDUPE_WINDOW, DEFAULT_SHEET_NAME, DeliveryError, type DeliveryFailure, type DeliveryItem, type DeliveryOptions, type DeliveryResult, type DeliverySubmission, type Destination, type DestinationConfig, type EmailConfig, type EmailContent, type EmailDestinationOptions, type EmailMessage, type EmailProvider, type EmailRenderInput, GOOGLE_TOKEN_URL, type GoogleServiceAccount, type GoogleTokenOptions, HEADER_SCAN_ROWS, type HttpDeps, MAX_RECIPIENTS, REQUEST_TIMEOUT_MS, RESEND_API, type ResendOptions, type ResponseRow, type RowOptions, SENSITIVE_MASK, SHEETS_API, SHEETS_SCOPE, type SheetConfig, type SheetState, type SheetsDestinationOptions, type VerifyWebhookInput, type WebhookConfig, type WebhookDestinationOptions, type WebhookPayload, answerCell, checkWebhookUrl, clearGoogleTokenCache, columnLetter, deliveryFailure, emailBody, emailDestination, escapeFormula, escapeHtml, googleAccessToken, headerSafe, httpError, idempotencyKey, isPrivateHost, isRetryableStatus, isSafeAddress, isSafeFrom, mergeHeader, renderEmail, resendProvider, responseRow, retryAfterSeconds, rowFor, sheetsDestination, signJwt, signWebhook, verifyWebhook, visibleInputs, webhookDestination, webhookPayload };
564
+ /**
565
+ * The Slack destination: one message per submission to a Slack incoming
566
+ * webhook (the channel is the webhook's). The message is a heading (the
567
+ * form's title), each answer as "label: value" in form order, and a footer
568
+ * with a link back to the app when one is given. Every value is escaped, so
569
+ * text a respondent typed can't mention `@channel` or make a link.
570
+ *
571
+ * The webhook URL is itself the credential, so it is usually kept out of
572
+ * config: pass `url` to look it up. It must be a Slack incoming webhook
573
+ * (`https://hooks.slack.com/services/…`); redirects are not followed.
574
+ */
575
+
576
+ /** Longest answer shown in full; longer ones are cut (the app has the rest). */
577
+ declare const SLACK_VALUE_LIMIT = 1500;
578
+ /** Text for Slack's mrkdwn: `&`, `<` and `>` escaped, which is all Slack asks. */
579
+ declare function escapeSlack(text: string): string;
580
+ interface SlackMessageOptions {
581
+ /** The heading, as plain text (it is escaped). Default `New response: <title>`, or `Updated response: …` for an update. */
582
+ heading?: string | undefined;
583
+ /** An update of a submission delivered before. */
584
+ update?: boolean | undefined;
585
+ /** Lines shown before the answers, as `[label, value]` plain text (a location, a session). */
586
+ lines?: readonly (readonly [string, string])[] | undefined;
587
+ /** A link back to the response in the app: http(s) only. */
588
+ link?: {
589
+ url: string;
590
+ text?: string | undefined;
591
+ } | null | undefined;
592
+ /** Sensitive fields: `"mask"` (default), `"omit"` or `"include"`. A channel is usually wider than an inbox. */
593
+ sensitive?: RowOptions["sensitive"];
594
+ /** Field keys left out (contact details, say). */
595
+ omit?: readonly string[] | undefined;
596
+ }
597
+ /** The message Slack gets for one submission: `{ text }` in Slack mrkdwn. */
598
+ declare function slackMessage(submission: DeliverySubmission, definition: FormDefinition, options?: SlackMessageOptions): {
599
+ text: string;
600
+ };
601
+ interface SlackMessageInput {
602
+ submission: DeliverySubmission;
603
+ definition: FormDefinition;
604
+ config: DestinationConfig;
605
+ /** `DeliveryOptions.update`. */
606
+ update: boolean;
607
+ destinationId?: string | undefined;
608
+ }
609
+ interface SlackDestinationOptions extends HttpDeps {
610
+ /**
611
+ * The incoming webhook URL, when it is not `config.url`. Gets the config
612
+ * and `{ destinationId }`.
613
+ */
614
+ url?: OptionResolver<string> | undefined;
615
+ /** Resolves the host before every request; the delivery is refused unless every address is public. */
616
+ resolveHost?: ResolveHost | undefined;
617
+ /** The message's options per delivery (heading, extra lines, a link back). */
618
+ message?: ((input: SlackMessageInput) => SlackMessageOptions | Promise<SlackMessageOptions>) | undefined;
619
+ }
620
+ declare function slackDestination(options?: SlackDestinationOptions): Destination;
621
+
622
+ /** How deep `all` / `any` / `not` may nest in a stored condition. */
623
+ declare const MAX_CONDITION_DEPTH = 10;
624
+ /** Whether a stored value has a `Condition`'s shape, nested at most `MAX_CONDITION_DEPTH` deep. */
625
+ declare function isCondition(v: unknown, depth?: number): v is Condition;
626
+ /**
627
+ * The destination's condition (`config.when`), or null when it has none. One
628
+ * that can't be read throws a permanent `DeliveryError` rather than meaning
629
+ * "send everything" or "send nothing".
630
+ */
631
+ declare function conditionOf(config: DestinationConfig): Condition | null;
632
+ /**
633
+ * Whether a submission matches its destination's condition (true when there
634
+ * is none). Throws a `DeliveryError` when the condition can't be read.
635
+ */
636
+ declare function passesCondition(submission: Pick<DeliverySubmission, "answers" | "hidden" | "context">, definition: FormDefinition, config: DestinationConfig): boolean;
637
+
638
+ /** Google's scope for files the person picked or created through the app. */
639
+ declare const DRIVE_FILE_SCOPE = "https://www.googleapis.com/auth/drive.file";
640
+ /** The app's OAuth client, from the Google Cloud console. Server-side only. */
641
+ interface GoogleOAuthClient {
642
+ clientId: string;
643
+ clientSecret: string;
644
+ }
645
+ interface GoogleOAuthTokenOptions extends HttpDeps {
646
+ now?: (() => number) | undefined;
647
+ signal?: AbortSignal | undefined;
648
+ }
649
+ interface RefreshedGoogleToken {
650
+ accessToken: string;
651
+ /** Seconds the access token is good for. */
652
+ expiresIn: number;
653
+ /** The scopes granted, space-separated. */
654
+ scope: string;
655
+ }
656
+ /**
657
+ * Trades a refresh token for an access token, uncached. `invalid_grant`
658
+ * (access revoked, or the account gone) is a permanent failure saying to
659
+ * connect the sheet again; 429 and 5xx are retryable. Never puts a token in
660
+ * an error message.
661
+ */
662
+ declare function refreshGoogleAccessToken(client: GoogleOAuthClient, refreshToken: string, options?: GoogleOAuthTokenOptions): Promise<RefreshedGoogleToken>;
663
+ /** Forgets cached OAuth access tokens (tests, or after a sheet is disconnected). */
664
+ declare function clearGoogleOAuthTokenCache(): void;
665
+ /** A cached or fresh access token for a refresh token, cached until a minute before it expires. */
666
+ declare function googleOAuthAccessToken(client: GoogleOAuthClient, refreshToken: string | null | undefined, options?: GoogleOAuthTokenOptions): Promise<string>;
667
+
668
+ /**
669
+ * Keeping a destination's secrets at rest: a webhook or Slack URL, a signing
670
+ * secret, a Google refresh token. AES-256-GCM (WebCrypto) under a key derived
671
+ * with HKDF-SHA-256 from one 32-byte app secret and a purpose string, so no
672
+ * two purposes share a key; each ciphertext is bound to associated data (the
673
+ * row it belongs to), so one moved onto another row does not decrypt.
674
+ *
675
+ * The sealed form is `v1.<iv>.<ciphertext and tag>`, base64url. Where it is
676
+ * stored is the app's; the key never is.
677
+ */
678
+
679
+ /**
680
+ * The 32 key bytes from base64 or base64url text (as `openssl rand -base64 32`
681
+ * prints), or the bytes themselves. Throws a `DeliveryError` naming the
682
+ * problem, never the key.
683
+ */
684
+ declare function secretKeyBytes(key: string | Uint8Array): Uint8Array<ArrayBuffer>;
685
+ /**
686
+ * An AES-GCM key for one purpose, derived from the app secret with HKDF-SHA-256
687
+ * (empty salt, `info` naming the purpose). Use a different `info` for each
688
+ * kind of secret; keep it fixed once anything is sealed with it.
689
+ */
690
+ declare function deriveSecretKey(key: string | Uint8Array, info: string): Promise<WebCryptoKey>;
691
+ /** Encrypts `plaintext` bound to `aad` (the row it belongs to): `v1.<iv>.<ciphertext>`. */
692
+ declare function sealSecret(key: WebCryptoKey, plaintext: string, aad: string): Promise<string>;
693
+ /**
694
+ * Decrypts what `sealSecret` sealed with the same key and `aad`. Throws a
695
+ * permanent `DeliveryError` for an unknown format, a wrong key (rotated?) or
696
+ * a ciphertext from another row; never echoes the ciphertext.
697
+ */
698
+ declare function openSecret(key: WebCryptoKey, sealed: string, aad: string): Promise<string>;
699
+
700
+ /**
701
+ * The outbox runner: claim a batch of due deliveries, send each to its
702
+ * destination, and record what happened. The store is the app's (a SQL
703
+ * outbox, a queue, a KV list): it hands over claimed deliveries and takes
704
+ * back outcomes, and decides how claims and leases work. The runner owns the
705
+ * rest: the destination's condition (`config.when`), batching deliveries to
706
+ * one destination into one request where the destination can, keeping its
707
+ * state, and turning each result into an outcome with a retry time.
708
+ *
709
+ * Outcomes:
710
+ * - `sent`: delivered (or already there, on a redelivery).
711
+ * - `filtered`: the submission doesn't match the destination's condition; nothing was sent, nothing to retry.
712
+ * - `skipped`: the destination deliberately sent nothing (`notSent`); never retried.
713
+ * - `retry`: a retryable failure. With the service's own Retry-After it is
714
+ * deferred without counting an attempt (`counted: false`); otherwise it backs
715
+ * off by attempt (`counted: true`), until `maxAttempts`.
716
+ * - `failed`: permanent, an unknown kind, or out of attempts.
717
+ *
718
+ * It never throws for a delivery's failure, and never logs answers, contact
719
+ * details or secrets: only ids, kinds and the destinations' safe messages.
720
+ */
721
+
722
+ /** One claimed delivery, as the app's store hands it over. */
723
+ interface ClaimedDelivery {
724
+ /** The delivery's id: stable across its retries (the email idempotency key, the webhook-id). */
725
+ id: string;
726
+ /** The destination's id. Deliveries to one destination are batched, and its state is kept by it. */
727
+ destinationId: string;
728
+ /** The destination's kind: a key of `destinations`. */
729
+ kind: string;
730
+ /** The destination's config (no secrets). */
731
+ config: DestinationConfig;
732
+ /** What the destination cached last time (`DeliveryResult.state`), as stored. */
733
+ state?: Record<string, Json> | undefined;
734
+ submission: DeliverySubmission;
735
+ definition: FormDefinition;
736
+ /** This delivery's attempt number, from 1 for its first. */
737
+ attempt: number;
738
+ /**
739
+ * How many times it was deferred so far (outcomes with `counted: false`).
740
+ * Keep and pass it back, so `maxDeferrals` can stop a service that answers
741
+ * "try later" forever. Default 0.
742
+ */
743
+ deferrals?: number | undefined;
744
+ /**
745
+ * An earlier attempt may have gone through (it was claimed before and timed
746
+ * out, or its lease ran out), so destinations that can look first do.
747
+ */
748
+ redelivery?: boolean | undefined;
749
+ /** An update of a submission delivered before (an edit). */
750
+ update?: boolean | undefined;
751
+ }
752
+ type DeliveryOutcome = {
753
+ status: "sent";
754
+ /** It was already there (a redelivery), so nothing was sent this time. */
755
+ alreadyThere?: boolean;
756
+ } | {
757
+ status: "filtered";
758
+ } | {
759
+ status: "skipped";
760
+ /** Why, safe to store. */
761
+ reason: string;
762
+ } | {
763
+ status: "retry";
764
+ error: string;
765
+ /** When to try again. */
766
+ retryAt: Date;
767
+ retryAfterSeconds: number;
768
+ /** Whether this attempt counts toward `maxAttempts`. False when the service said when to come back. */
769
+ counted: boolean;
770
+ httpStatus?: number;
771
+ } | {
772
+ status: "failed";
773
+ error: string;
774
+ httpStatus?: number;
775
+ };
776
+ interface ClaimRequest<Filter> {
777
+ /** Most deliveries to claim. */
778
+ limit: number;
779
+ /** What `run` was given: the app's own narrowing (one response, one account). */
780
+ filter?: Filter | undefined;
781
+ }
782
+ interface DeliveryRunnerOptions<Filter = undefined> {
783
+ /** Claims up to `limit` due deliveries, each held (leased) so no other runner sends it meanwhile. */
784
+ claim(request: ClaimRequest<Filter>): Promise<readonly ClaimedDelivery[]>;
785
+ /**
786
+ * Records an outcome. Resolve `false` when the claim was no longer held
787
+ * (its lease ran out and another runner took it): it is counted as stale.
788
+ * A rejection is logged and counted as unrecorded; the lease then runs out
789
+ * and the delivery comes back on its own.
790
+ */
791
+ record(delivery: ClaimedDelivery, outcome: DeliveryOutcome): Promise<void | boolean>;
792
+ /** Keeps a destination's new state (a sheet's header and last row). Failures are logged, never fatal. */
793
+ saveState?: ((destinationId: string, state: Record<string, Json>) => Promise<void>) | undefined;
794
+ /** The destinations by kind. */
795
+ destinations: Readonly<Record<string, Destination>>;
796
+ now?: (() => number) | undefined;
797
+ /** Safe lines (ids, kinds, short reasons). Default: nothing is logged. */
798
+ log?: ((message: string) => void) | undefined;
799
+ /** Deliveries claimed per run. Default 25. */
800
+ batchSize?: number | undefined;
801
+ /** Counted attempts before a retryable failure is given up. Default `DEFAULT_MAX_ATTEMPTS` (8). */
802
+ maxAttempts?: number | undefined;
803
+ /**
804
+ * Deferrals (Retry-After, no attempt counted) allowed before a further one
805
+ * counts as an attempt and backs off. Default `DEFAULT_MAX_DEFERRALS` (24).
806
+ */
807
+ maxDeferrals?: number | undefined;
808
+ /** Seconds to wait after a counted failed attempt. Default `defaultBackoff`. */
809
+ backoff?: ((attempt: number) => number) | undefined;
810
+ }
811
+ interface DeliveryReport {
812
+ claimed: number;
813
+ sent: number;
814
+ filtered: number;
815
+ skipped: number;
816
+ /** Retryable failures, backed off by attempt. */
817
+ retrying: number;
818
+ /** Retryable failures the service said when to retry; no attempt used. */
819
+ deferred: number;
820
+ failed: number;
821
+ /** Recorded after the claim was lost (`record` resolved false). */
822
+ stale: number;
823
+ /** `record` threw. */
824
+ unrecorded: number;
825
+ }
826
+ interface RunOptions<Filter> {
827
+ filter?: Filter | undefined;
828
+ /** Overrides `batchSize` for this run. */
829
+ limit?: number | undefined;
830
+ /** Aborts the sends in flight; their outcomes are recorded as retries. */
831
+ signal?: AbortSignal | undefined;
832
+ }
833
+ interface DeliveryRunner<Filter = undefined> {
834
+ /** One claim, its sends and their records. Never rejects for a delivery's failure; a failing `claim` rejects. */
835
+ run(options?: RunOptions<Filter>): Promise<DeliveryReport>;
836
+ }
837
+ declare const DEFAULT_MAX_ATTEMPTS = 8;
838
+ declare const DEFAULT_MAX_DEFERRALS = 24;
839
+ declare const DEFAULT_BATCH_SIZE = 25;
840
+ /** 1 minute, doubling each attempt, at most 6 hours: 1m, 2m, 4m … 6h. */
841
+ declare function defaultBackoff(attempt: number): number;
842
+ /** Turns a destination's result into an outcome for a delivery on its `attempt`. */
843
+ declare function deliveryOutcome(result: DeliveryResult, attempt: number, options?: {
844
+ now?: number | undefined;
845
+ maxAttempts?: number | undefined;
846
+ backoff?: ((attempt: number) => number) | undefined;
847
+ /** Deferrals so far (`ClaimedDelivery.deferrals`). */
848
+ deferrals?: number | undefined;
849
+ maxDeferrals?: number | undefined;
850
+ }): DeliveryOutcome;
851
+ declare function createDeliveryRunner<Filter = undefined>(options: DeliveryRunnerOptions<Filter>): DeliveryRunner<Filter>;
852
+
853
+ export { type Cell, type ClaimRequest, type ClaimedDelivery, DEDUPE_WINDOW, DEFAULT_BATCH_SIZE, DEFAULT_MAX_ATTEMPTS, DEFAULT_MAX_DEFERRALS, DEFAULT_SHEET_NAME, DRIVE_FILE_SCOPE, DeliveryError, type DeliveryFailure, type DeliveryItem, type DeliveryOptions, type DeliveryOutcome, type DeliveryReport, type DeliveryResult, type DeliveryRunner, type DeliveryRunnerOptions, type DeliverySubmission, type Destination, type DestinationConfig, type EmailConfig, type EmailContent, type EmailDestinationOptions, type EmailMessage, type EmailProvider, type EmailRenderInput, GOOGLE_TOKEN_URL, type GoogleOAuthClient, type GoogleOAuthTokenOptions, type GoogleServiceAccount, type GoogleTokenOptions, HEADER_SCAN_ROWS, type HookKind, HookUrlError, type HttpDeps, MAX_CONDITION_DEPTH, MAX_HOOK_URL_LENGTH, MAX_RECIPIENTS, type OptionResolver, REQUEST_TIMEOUT_MS, RESEND_API, type RefreshedGoogleToken, type ResendOptions, type ResolveContext, type ResolveHost, type ResponseRow, type RowOptions, type RunOptions, SENSITIVE_MASK, SHEETS_API, SHEETS_SCOPE, SLACK_HOSTS, SLACK_VALUE_LIMIT, type SheetConfig, type SheetState, type SheetsDestinationOptions, type SlackDestinationOptions, type SlackMessageInput, type SlackMessageOptions, type VerifyWebhookInput, type WebCryptoKey, type WebhookConfig, type WebhookDestinationOptions, type WebhookPayload, answerCell, checkHookHost, checkWebhookUrl, clearGoogleOAuthTokenCache, clearGoogleTokenCache, columnLetter, conditionOf, createDeliveryRunner, defaultBackoff, deliveryFailure, deliveryOutcome, deriveSecretKey, emailBody, emailDestination, escapeFormula, escapeHtml, escapeSlack, googleAccessToken, googleOAuthAccessToken, headerSafe, hookHost, httpError, idempotencyKey, isCondition, isPrivateHost, isPublicAddress, isRetryableStatus, isSafeAddress, isSafeFrom, mergeHeader, newWebhookSecret, openSecret, parseHookUrl, passesCondition, refreshGoogleAccessToken, renderEmail, resendProvider, responseRow, retryAfterSeconds, rowFor, sealSecret, secretKeyBytes, sheetsDestination, signJwt, signWebhook, slackDestination, slackMessage, verifyWebhook, visibleInputs, webhookDestination, webhookPayload };