@marktiderman/genesis-forms 0.7.0 → 0.9.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/README.md +386 -8
- package/dist/destinations.cjs +856 -53
- package/dist/destinations.cjs.map +1 -1
- package/dist/destinations.d.cts +452 -18
- package/dist/destinations.d.ts +452 -18
- package/dist/destinations.js +826 -54
- package/dist/destinations.js.map +1 -1
- package/dist/{engine-C5tb3mkf.d.cts → engine-BaHPC2-1.d.cts} +2 -2
- package/dist/{engine-DcNQ-6oJ.d.ts → engine-Da99Tv-E.d.ts} +2 -2
- package/dist/index.cjs +996 -167
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +288 -11
- package/dist/index.d.ts +288 -11
- package/dist/index.js +975 -168
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +563 -173
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +3 -474
- package/dist/server.d.ts +3 -474
- package/dist/server.js +558 -174
- package/dist/server.js.map +1 -1
- package/dist/submit-protocol-CFi-0Qd0.d.ts +840 -0
- package/dist/submit-protocol-D0cOckmK.d.cts +840 -0
- package/dist/testing.cjs +250 -154
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +4 -4
- package/dist/testing.d.ts +4 -4
- package/dist/testing.js +250 -154
- package/dist/testing.js.map +1 -1
- package/dist/{types-oAckGR_Z.d.cts → types-C0pUfew3.d.cts} +8 -2
- package/dist/{types-oAckGR_Z.d.ts → types-C0pUfew3.d.ts} +8 -2
- package/dist/{validate-BFpNJbjf.d.cts → validate-DpmBssFk.d.ts} +258 -140
- package/dist/{validate-qwkdcr3H.d.ts → validate-U5LoW3eR.d.cts} +258 -140
- package/package.json +2 -2
- package/dist/upload-protocol-Bw0CWRJc.d.cts +0 -122
- package/dist/upload-protocol-PX0jroTV.d.ts +0 -122
package/dist/destinations.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { J as Json, F as FormDefinition, a as FieldBlock } from './types-
|
|
1
|
+
import { J as Json, F as FormDefinition, a as FieldBlock, C as Condition } from './types-C0pUfew3.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;
|
|
@@ -83,6 +103,12 @@ interface DeliveryFailure {
|
|
|
83
103
|
retryAfterSeconds?: number;
|
|
84
104
|
/** The HTTP status that refused it, if one did. */
|
|
85
105
|
status?: number;
|
|
106
|
+
/**
|
|
107
|
+
* The receiver asked to be switched off (a webhook answered 410 Gone, as
|
|
108
|
+
* Standard Webhooks specifies): disable the destination rather than keep
|
|
109
|
+
* sending to it. Always with `retryable: false`.
|
|
110
|
+
*/
|
|
111
|
+
disable?: true;
|
|
86
112
|
/** Safe to store and log: a status and a short reason, never secrets, answers or contact details. */
|
|
87
113
|
error: string;
|
|
88
114
|
}
|
|
@@ -109,10 +135,13 @@ declare class DeliveryError extends Error {
|
|
|
109
135
|
readonly retryable: boolean;
|
|
110
136
|
readonly retryAfterSeconds: number | undefined;
|
|
111
137
|
readonly status: number | undefined;
|
|
138
|
+
/** The receiver asked to be switched off (`DeliveryFailure.disable`); never retryable. */
|
|
139
|
+
readonly disable: boolean;
|
|
112
140
|
constructor(message: string, options?: {
|
|
113
141
|
retryable?: boolean | undefined;
|
|
114
142
|
retryAfterSeconds?: number | undefined;
|
|
115
143
|
status?: number | undefined;
|
|
144
|
+
disable?: boolean | undefined;
|
|
116
145
|
});
|
|
117
146
|
}
|
|
118
147
|
/** Every outbound request gives up after this long, unless the destination is given another `timeoutMs`. */
|
|
@@ -149,6 +178,15 @@ interface HttpDeps {
|
|
|
149
178
|
/** Per request. Default `REQUEST_TIMEOUT_MS`. */
|
|
150
179
|
timeoutMs?: number | undefined;
|
|
151
180
|
}
|
|
181
|
+
/** What an option function is told besides the config. */
|
|
182
|
+
interface ResolveContext {
|
|
183
|
+
/** `DeliveryOptions.destinationId`, when the app gave one. */
|
|
184
|
+
destinationId?: string | undefined;
|
|
185
|
+
}
|
|
186
|
+
/** A factory option worked out per delivery from the destination's config. */
|
|
187
|
+
type OptionResolver<T> = (config: DestinationConfig, context: ResolveContext) => T | Promise<T>;
|
|
188
|
+
/** WebCrypto's key type, named without depending on the DOM lib. */
|
|
189
|
+
type WebCryptoKey = Awaited<ReturnType<typeof crypto.subtle.importKey>>;
|
|
152
190
|
|
|
153
191
|
interface RowOptions {
|
|
154
192
|
/**
|
|
@@ -342,8 +380,18 @@ interface SheetConfig {
|
|
|
342
380
|
sheetName?: string;
|
|
343
381
|
}
|
|
344
382
|
interface SheetsDestinationOptions extends HttpDeps {
|
|
345
|
-
/**
|
|
346
|
-
|
|
383
|
+
/**
|
|
384
|
+
* The service account the sheet is shared with, or a function that finds
|
|
385
|
+
* the one for a destination's config. Give this or `accessToken`.
|
|
386
|
+
*/
|
|
387
|
+
account?: GoogleServiceAccount | OptionResolver<GoogleServiceAccount> | undefined;
|
|
388
|
+
/**
|
|
389
|
+
* An access token for the sheet instead of a service account: a sheet a
|
|
390
|
+
* person connected with Google sign-in, say (see `googleOAuthAccessToken`).
|
|
391
|
+
* Gets the config and `{ destinationId }`; called once per delivery or
|
|
392
|
+
* batch, so cache inside it.
|
|
393
|
+
*/
|
|
394
|
+
accessToken?: OptionResolver<string> | undefined;
|
|
347
395
|
/**
|
|
348
396
|
* Prefix text a spreadsheet could run as a formula with `'` (see
|
|
349
397
|
* `escapeFormula`), headers included. Default false: writes are RAW, so
|
|
@@ -381,35 +429,94 @@ interface SheetState {
|
|
|
381
429
|
}
|
|
382
430
|
declare function sheetsDestination(options: SheetsDestinationOptions): Destination;
|
|
383
431
|
|
|
432
|
+
/**
|
|
433
|
+
* Where a Slack or webhook destination may post, checked against SSRF: https
|
|
434
|
+
* only, on the default port, with no credentials in the address, and only to
|
|
435
|
+
* a public host, never a private, loopback, link-local or otherwise internal
|
|
436
|
+
* one. `parseHookUrl` checks the text (use it when someone adds the address);
|
|
437
|
+
* `checkHookHost` checks every address the name resolves to right now (use it
|
|
438
|
+
* before every request: what a name points to can change).
|
|
439
|
+
*
|
|
440
|
+
* A check before the request still leaves a short window in which the name
|
|
441
|
+
* could be re-pointed (DNS rebinding). Where untrusted people choose URLs,
|
|
442
|
+
* also send from a network that cannot reach internal services.
|
|
443
|
+
*/
|
|
444
|
+
|
|
445
|
+
type HookKind = "slack" | "webhook";
|
|
446
|
+
/** 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`. */
|
|
447
|
+
type ResolveHost = (host: string) => Promise<string[]>;
|
|
448
|
+
declare const MAX_HOOK_URL_LENGTH = 2048;
|
|
449
|
+
/** The hosts a Slack incoming webhook lives on. */
|
|
450
|
+
declare const SLACK_HOSTS: ReadonlySet<string>;
|
|
451
|
+
/** An address that can't be used, with a reason safe to show and to store (never the address). */
|
|
452
|
+
declare class HookUrlError extends DeliveryError {
|
|
453
|
+
name: string;
|
|
454
|
+
}
|
|
455
|
+
/** Whether an IP address (v4 or v6) is a public one a hook may reach. Anything unparsable is not. */
|
|
456
|
+
declare function isPublicAddress(ip: string): boolean;
|
|
457
|
+
/**
|
|
458
|
+
* The address as it will be used (its fragment dropped), or a `HookUrlError`
|
|
459
|
+
* saying what is wrong with it. Checks the text only; `checkHookHost` checks
|
|
460
|
+
* where the name resolves. A `"slack"` address must be a Slack incoming
|
|
461
|
+
* webhook (`https://hooks.slack.com/services/…`).
|
|
462
|
+
*/
|
|
463
|
+
declare function parseHookUrl(raw: string, kind?: HookKind): URL;
|
|
464
|
+
/**
|
|
465
|
+
* Checks that every address the URL's host resolves to is public. A host
|
|
466
|
+
* that resolves to nothing, or whose lookup fails, is refused too, as a
|
|
467
|
+
* retryable failure (DNS may be briefly down); a private address is final.
|
|
468
|
+
*/
|
|
469
|
+
declare function checkHookHost(url: URL, resolve: ResolveHost): Promise<void>;
|
|
470
|
+
/** The host as a screen may show it (never the path: a Slack or webhook path can be the secret). */
|
|
471
|
+
declare function hookHost(url: URL): string;
|
|
472
|
+
|
|
384
473
|
/** What the webhook destination reads from a destination's config. */
|
|
385
474
|
interface WebhookConfig {
|
|
386
475
|
url: string;
|
|
387
476
|
}
|
|
388
477
|
interface WebhookDestinationOptions extends HttpDeps {
|
|
389
478
|
/**
|
|
390
|
-
* The signing secret: base64 of at least
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
479
|
+
* The signing secret: base64 of at least 24 random bytes (the spec
|
|
480
|
+
* recommends 24 to 64), usually with the `whsec_` prefix Standard Webhooks
|
|
481
|
+
* tools issue. The prefix is optional; the rest is always base64-decoded,
|
|
482
|
+
* as every Standard Webhooks library does, so a receiver verifies with the
|
|
483
|
+
* same string. A secret that is not base64, or is shorter, fails
|
|
484
|
+
* permanently. Several secrets (new and old, during a rotation) sign with
|
|
485
|
+
* each, space-separated in `webhook-signature`, so the receiver verifies
|
|
486
|
+
* with either. Or a function that finds them for a destination's config.
|
|
487
|
+
* Never put it in config.
|
|
397
488
|
*/
|
|
398
|
-
secret: string |
|
|
489
|
+
secret: string | readonly string[] | OptionResolver<string | readonly string[]>;
|
|
399
490
|
/** Allow `http:` URLs. Default false. */
|
|
400
491
|
allowInsecureUrls?: boolean | undefined;
|
|
401
492
|
/** Allow localhost and private, loopback and link-local address literals. Default false. */
|
|
402
493
|
allowPrivateHosts?: boolean | undefined;
|
|
403
494
|
/** Sensitive fields: `"include"` (default), `"mask"` or `"omit"`. */
|
|
404
495
|
sensitive?: RowOptions["sensitive"];
|
|
405
|
-
/** The event type in the payload. Default `"form.submitted"
|
|
496
|
+
/** The event type in the payload. Default `"form.submitted"`, or `"form.updated"` for an update. */
|
|
406
497
|
type?: string | undefined;
|
|
498
|
+
/**
|
|
499
|
+
* Where to post, when it is not `config.url`: the address kept apart from
|
|
500
|
+
* config (encrypted, say, since a webhook URL can carry a token). Gets the
|
|
501
|
+
* config and `{ destinationId }`. It passes the same checks.
|
|
502
|
+
*/
|
|
503
|
+
url?: OptionResolver<string> | undefined;
|
|
504
|
+
/**
|
|
505
|
+
* Resolves the URL's host before every request, and the delivery is refused
|
|
506
|
+
* unless every address is public (see `checkHookHost`). This catches a
|
|
507
|
+
* hostname that points at a private address, which the URL checks alone
|
|
508
|
+
* cannot. Recommended wherever untrusted people choose URLs.
|
|
509
|
+
*/
|
|
510
|
+
resolveHost?: ResolveHost | undefined;
|
|
407
511
|
now?: (() => number) | undefined;
|
|
408
512
|
}
|
|
409
513
|
/** The JSON body a webhook receives. */
|
|
410
514
|
interface WebhookPayload {
|
|
411
515
|
type: string;
|
|
412
|
-
/**
|
|
516
|
+
/**
|
|
517
|
+
* ISO time the event happened: the submission's `submittedAt`, or the send
|
|
518
|
+
* time when it has none. The `webhook-timestamp` header is the send time.
|
|
519
|
+
*/
|
|
413
520
|
timestamp: string;
|
|
414
521
|
data: {
|
|
415
522
|
form: {
|
|
@@ -431,11 +538,26 @@ interface WebhookPayload {
|
|
|
431
538
|
row: Record<string, Json>;
|
|
432
539
|
};
|
|
433
540
|
}
|
|
434
|
-
/** The
|
|
435
|
-
declare
|
|
541
|
+
/** The shortest signing secret, decoded: Standard Webhooks recommends 24 to 64 bytes. */
|
|
542
|
+
declare const MIN_WEBHOOK_SECRET_BYTES = 24;
|
|
543
|
+
/**
|
|
544
|
+
* A new signing secret: `whsec_` and standard base64 of 32 random bytes, the
|
|
545
|
+
* form Standard Webhooks libraries read. Show it to the receiver once; keep it
|
|
546
|
+
* out of config.
|
|
547
|
+
*/
|
|
548
|
+
declare function newWebhookSecret(): string;
|
|
549
|
+
/**
|
|
550
|
+
* The `webhook-signature` value for a message: `v1,<base64>`, or one such
|
|
551
|
+
* signature per secret, space-separated, when given several (a rotation).
|
|
552
|
+
*/
|
|
553
|
+
declare function signWebhook(secret: string | readonly string[], id: string, timestamp: number, body: string): Promise<string>;
|
|
436
554
|
interface VerifyWebhookInput {
|
|
437
|
-
/**
|
|
438
|
-
|
|
555
|
+
/**
|
|
556
|
+
* The same secret the sender signs with (base64, optionally
|
|
557
|
+
* `whsec_`-prefixed), or several (new and old, during a rotation): any one
|
|
558
|
+
* matching signature passes.
|
|
559
|
+
*/
|
|
560
|
+
secret: string | readonly string[];
|
|
439
561
|
/** The request's headers: a `Headers` or a plain object. */
|
|
440
562
|
headers: Headers | Record<string, string | string[] | undefined>;
|
|
441
563
|
/** The raw body, exactly as received. */
|
|
@@ -462,4 +584,316 @@ declare function webhookPayload(submission: DeliverySubmission, definition: Form
|
|
|
462
584
|
}): WebhookPayload;
|
|
463
585
|
declare function webhookDestination(options: WebhookDestinationOptions): Destination;
|
|
464
586
|
|
|
465
|
-
|
|
587
|
+
/**
|
|
588
|
+
* The Slack destination: one message per submission to a Slack incoming
|
|
589
|
+
* webhook (the channel is the webhook's). The message is a heading (the
|
|
590
|
+
* form's title), each answer as "label: value" in form order, and a footer
|
|
591
|
+
* with a link back to the app when one is given. Every value is escaped, so
|
|
592
|
+
* text a respondent typed can't mention `@channel` or make a link.
|
|
593
|
+
*
|
|
594
|
+
* The webhook URL is itself the credential, so it is usually kept out of
|
|
595
|
+
* config: pass `url` to look it up. It must be a Slack incoming webhook
|
|
596
|
+
* (`https://hooks.slack.com/services/…`); redirects are not followed.
|
|
597
|
+
*/
|
|
598
|
+
|
|
599
|
+
/** Longest answer shown in full; longer ones are cut (the app has the rest). */
|
|
600
|
+
declare const SLACK_VALUE_LIMIT = 1500;
|
|
601
|
+
/** Text for Slack's mrkdwn: `&`, `<` and `>` escaped, which is all Slack asks. */
|
|
602
|
+
declare function escapeSlack(text: string): string;
|
|
603
|
+
interface SlackMessageOptions {
|
|
604
|
+
/** The heading, as plain text (it is escaped). Default `New response: <title>`, or `Updated response: …` for an update. */
|
|
605
|
+
heading?: string | undefined;
|
|
606
|
+
/** An update of a submission delivered before. */
|
|
607
|
+
update?: boolean | undefined;
|
|
608
|
+
/** Lines shown before the answers, as `[label, value]` plain text (a location, a session). */
|
|
609
|
+
lines?: readonly (readonly [string, string])[] | undefined;
|
|
610
|
+
/** A link back to the response in the app: http(s) only. */
|
|
611
|
+
link?: {
|
|
612
|
+
url: string;
|
|
613
|
+
text?: string | undefined;
|
|
614
|
+
} | null | undefined;
|
|
615
|
+
/** Sensitive fields: `"mask"` (default), `"omit"` or `"include"`. A channel is usually wider than an inbox. */
|
|
616
|
+
sensitive?: RowOptions["sensitive"];
|
|
617
|
+
/** Field keys left out (contact details, say). */
|
|
618
|
+
omit?: readonly string[] | undefined;
|
|
619
|
+
}
|
|
620
|
+
/** The message Slack gets for one submission: `{ text }` in Slack mrkdwn. */
|
|
621
|
+
declare function slackMessage(submission: DeliverySubmission, definition: FormDefinition, options?: SlackMessageOptions): {
|
|
622
|
+
text: string;
|
|
623
|
+
};
|
|
624
|
+
interface SlackMessageInput {
|
|
625
|
+
submission: DeliverySubmission;
|
|
626
|
+
definition: FormDefinition;
|
|
627
|
+
config: DestinationConfig;
|
|
628
|
+
/** `DeliveryOptions.update`. */
|
|
629
|
+
update: boolean;
|
|
630
|
+
destinationId?: string | undefined;
|
|
631
|
+
}
|
|
632
|
+
interface SlackDestinationOptions extends HttpDeps {
|
|
633
|
+
/**
|
|
634
|
+
* The incoming webhook URL, when it is not `config.url`. Gets the config
|
|
635
|
+
* and `{ destinationId }`.
|
|
636
|
+
*/
|
|
637
|
+
url?: OptionResolver<string> | undefined;
|
|
638
|
+
/** Resolves the host before every request; the delivery is refused unless every address is public. */
|
|
639
|
+
resolveHost?: ResolveHost | undefined;
|
|
640
|
+
/** The message's options per delivery (heading, extra lines, a link back). */
|
|
641
|
+
message?: ((input: SlackMessageInput) => SlackMessageOptions | Promise<SlackMessageOptions>) | undefined;
|
|
642
|
+
}
|
|
643
|
+
declare function slackDestination(options?: SlackDestinationOptions): Destination;
|
|
644
|
+
|
|
645
|
+
/** How deep `all` / `any` / `not` may nest in a stored condition. */
|
|
646
|
+
declare const MAX_CONDITION_DEPTH = 10;
|
|
647
|
+
/** Whether a stored value has a `Condition`'s shape, nested at most `MAX_CONDITION_DEPTH` deep. */
|
|
648
|
+
declare function isCondition(v: unknown, depth?: number): v is Condition;
|
|
649
|
+
/**
|
|
650
|
+
* The destination's condition (`config.when`), or null when it has none. One
|
|
651
|
+
* that can't be read throws a permanent `DeliveryError` rather than meaning
|
|
652
|
+
* "send everything" or "send nothing".
|
|
653
|
+
*/
|
|
654
|
+
declare function conditionOf(config: DestinationConfig): Condition | null;
|
|
655
|
+
/**
|
|
656
|
+
* Whether a submission matches its destination's condition (true when there
|
|
657
|
+
* is none). Throws a `DeliveryError` when the condition can't be read.
|
|
658
|
+
*/
|
|
659
|
+
declare function passesCondition(submission: Pick<DeliverySubmission, "answers" | "hidden" | "context">, definition: FormDefinition, config: DestinationConfig): boolean;
|
|
660
|
+
|
|
661
|
+
/** Google's scope for files the person picked or created through the app. */
|
|
662
|
+
declare const DRIVE_FILE_SCOPE = "https://www.googleapis.com/auth/drive.file";
|
|
663
|
+
/** The app's OAuth client, from the Google Cloud console. Server-side only. */
|
|
664
|
+
interface GoogleOAuthClient {
|
|
665
|
+
clientId: string;
|
|
666
|
+
clientSecret: string;
|
|
667
|
+
}
|
|
668
|
+
interface GoogleOAuthTokenOptions extends HttpDeps {
|
|
669
|
+
now?: (() => number) | undefined;
|
|
670
|
+
signal?: AbortSignal | undefined;
|
|
671
|
+
}
|
|
672
|
+
interface RefreshedGoogleToken {
|
|
673
|
+
accessToken: string;
|
|
674
|
+
/** Seconds the access token is good for. */
|
|
675
|
+
expiresIn: number;
|
|
676
|
+
/** The scopes granted, space-separated. */
|
|
677
|
+
scope: string;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Trades a refresh token for an access token, uncached. `invalid_grant`
|
|
681
|
+
* (access revoked, or the account gone) is a permanent failure saying to
|
|
682
|
+
* connect the sheet again; 429 and 5xx are retryable. Never puts a token in
|
|
683
|
+
* an error message.
|
|
684
|
+
*/
|
|
685
|
+
declare function refreshGoogleAccessToken(client: GoogleOAuthClient, refreshToken: string, options?: GoogleOAuthTokenOptions): Promise<RefreshedGoogleToken>;
|
|
686
|
+
/** Forgets cached OAuth access tokens (tests, or after a sheet is disconnected). */
|
|
687
|
+
declare function clearGoogleOAuthTokenCache(): void;
|
|
688
|
+
/** A cached or fresh access token for a refresh token, cached until a minute before it expires. */
|
|
689
|
+
declare function googleOAuthAccessToken(client: GoogleOAuthClient, refreshToken: string | null | undefined, options?: GoogleOAuthTokenOptions): Promise<string>;
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Keeping a destination's secrets at rest: a webhook or Slack URL, a signing
|
|
693
|
+
* secret, a Google refresh token. AES-256-GCM (WebCrypto) under a key derived
|
|
694
|
+
* with HKDF-SHA-256 from one 32-byte app secret and a purpose string, so no
|
|
695
|
+
* two purposes share a key; each ciphertext is bound to associated data (the
|
|
696
|
+
* row it belongs to), so one moved onto another row does not decrypt.
|
|
697
|
+
*
|
|
698
|
+
* The sealed form is `v1.<iv>.<ciphertext and tag>`, base64url. Where it is
|
|
699
|
+
* stored is the app's; the key never is.
|
|
700
|
+
*/
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* The 32 key bytes from base64 or base64url text (as `openssl rand -base64 32`
|
|
704
|
+
* prints), or the bytes themselves. Throws a `DeliveryError` naming the
|
|
705
|
+
* problem, never the key.
|
|
706
|
+
*/
|
|
707
|
+
declare function secretKeyBytes(key: string | Uint8Array): Uint8Array<ArrayBuffer>;
|
|
708
|
+
/** Options for `deriveSecretKey`. */
|
|
709
|
+
interface DeriveSecretKeyOptions {
|
|
710
|
+
/**
|
|
711
|
+
* The HKDF salt: bytes, or text taken as its UTF-8 bytes. Default empty,
|
|
712
|
+
* which is what every key derived before this option existed used, so
|
|
713
|
+
* leave it out to open those. For new keys a fixed, app-wide random value
|
|
714
|
+
* (say 32 bytes from `openssl rand -base64 32`, kept as config) is
|
|
715
|
+
* recommended (RFC 5869). It need not be secret, but must never change
|
|
716
|
+
* once anything is sealed with it.
|
|
717
|
+
*/
|
|
718
|
+
salt?: string | Uint8Array | undefined;
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* An AES-GCM key for one purpose, derived from the app secret with HKDF-SHA-256
|
|
722
|
+
* (`info` naming the purpose; the salt empty unless `options.salt` is given).
|
|
723
|
+
* Use a different `info` for each kind of secret; keep it, and the salt,
|
|
724
|
+
* fixed once anything is sealed with them.
|
|
725
|
+
*/
|
|
726
|
+
declare function deriveSecretKey(key: string | Uint8Array, info: string, options?: DeriveSecretKeyOptions): Promise<WebCryptoKey>;
|
|
727
|
+
/** Encrypts `plaintext` bound to `aad` (the row it belongs to): `v1.<iv>.<ciphertext>`. */
|
|
728
|
+
declare function sealSecret(key: WebCryptoKey, plaintext: string, aad: string): Promise<string>;
|
|
729
|
+
/**
|
|
730
|
+
* Decrypts what `sealSecret` sealed with the same key and `aad`. Throws a
|
|
731
|
+
* permanent `DeliveryError` for an unknown format, a wrong key (rotated?) or
|
|
732
|
+
* a ciphertext from another row; never echoes the ciphertext.
|
|
733
|
+
*/
|
|
734
|
+
declare function openSecret(key: WebCryptoKey, sealed: string, aad: string): Promise<string>;
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* The outbox runner: claim a batch of due deliveries, send each to its
|
|
738
|
+
* destination, and record what happened. The store is the app's (a SQL
|
|
739
|
+
* outbox, a queue, a KV list): it hands over claimed deliveries and takes
|
|
740
|
+
* back outcomes, and decides how claims and leases work. The runner owns the
|
|
741
|
+
* rest: the destination's condition (`config.when`), batching deliveries to
|
|
742
|
+
* one destination into one request where the destination can, keeping its
|
|
743
|
+
* state, and turning each result into an outcome with a retry time.
|
|
744
|
+
*
|
|
745
|
+
* Outcomes:
|
|
746
|
+
* - `sent`: delivered (or already there, on a redelivery).
|
|
747
|
+
* - `filtered`: the submission doesn't match the destination's condition; nothing was sent, nothing to retry.
|
|
748
|
+
* - `skipped`: the destination deliberately sent nothing (`notSent`); never retried.
|
|
749
|
+
* - `retry`: a retryable failure. With the service's own Retry-After it is
|
|
750
|
+
* deferred without counting an attempt (`counted: false`); otherwise it backs
|
|
751
|
+
* off by attempt (`counted: true`), until `maxAttempts`.
|
|
752
|
+
* - `failed`: permanent, an unknown kind, or out of attempts. With `disable:
|
|
753
|
+
* true` the receiver asked to be switched off (a webhook's 410 Gone): the
|
|
754
|
+
* app should disable the destination rather than keep sending to it.
|
|
755
|
+
*
|
|
756
|
+
* It never throws for a delivery's failure, and never logs answers, contact
|
|
757
|
+
* details or secrets: only ids, kinds and the destinations' safe messages.
|
|
758
|
+
*/
|
|
759
|
+
|
|
760
|
+
/** One claimed delivery, as the app's store hands it over. */
|
|
761
|
+
interface ClaimedDelivery {
|
|
762
|
+
/** The delivery's id: stable across its retries (the email idempotency key, the webhook-id). */
|
|
763
|
+
id: string;
|
|
764
|
+
/** The destination's id. Deliveries to one destination are batched, and its state is kept by it. */
|
|
765
|
+
destinationId: string;
|
|
766
|
+
/** The destination's kind: a key of `destinations`. */
|
|
767
|
+
kind: string;
|
|
768
|
+
/** The destination's config (no secrets). */
|
|
769
|
+
config: DestinationConfig;
|
|
770
|
+
/** What the destination cached last time (`DeliveryResult.state`), as stored. */
|
|
771
|
+
state?: Record<string, Json> | undefined;
|
|
772
|
+
submission: DeliverySubmission;
|
|
773
|
+
definition: FormDefinition;
|
|
774
|
+
/** This delivery's attempt number, from 1 for its first. */
|
|
775
|
+
attempt: number;
|
|
776
|
+
/**
|
|
777
|
+
* How many times it was deferred so far (outcomes with `counted: false`).
|
|
778
|
+
* Keep and pass it back, so `maxDeferrals` can stop a service that answers
|
|
779
|
+
* "try later" forever. Default 0.
|
|
780
|
+
*/
|
|
781
|
+
deferrals?: number | undefined;
|
|
782
|
+
/**
|
|
783
|
+
* An earlier attempt may have gone through (it was claimed before and timed
|
|
784
|
+
* out, or its lease ran out), so destinations that can look first do.
|
|
785
|
+
*/
|
|
786
|
+
redelivery?: boolean | undefined;
|
|
787
|
+
/** An update of a submission delivered before (an edit). */
|
|
788
|
+
update?: boolean | undefined;
|
|
789
|
+
}
|
|
790
|
+
type DeliveryOutcome = {
|
|
791
|
+
status: "sent";
|
|
792
|
+
/** It was already there (a redelivery), so nothing was sent this time. */
|
|
793
|
+
alreadyThere?: boolean;
|
|
794
|
+
} | {
|
|
795
|
+
status: "filtered";
|
|
796
|
+
} | {
|
|
797
|
+
status: "skipped";
|
|
798
|
+
/** Why, safe to store. */
|
|
799
|
+
reason: string;
|
|
800
|
+
} | {
|
|
801
|
+
status: "retry";
|
|
802
|
+
error: string;
|
|
803
|
+
/** When to try again. */
|
|
804
|
+
retryAt: Date;
|
|
805
|
+
retryAfterSeconds: number;
|
|
806
|
+
/** Whether this attempt counts toward `maxAttempts`. False when the service said when to come back. */
|
|
807
|
+
counted: boolean;
|
|
808
|
+
httpStatus?: number;
|
|
809
|
+
} | {
|
|
810
|
+
status: "failed";
|
|
811
|
+
error: string;
|
|
812
|
+
httpStatus?: number;
|
|
813
|
+
/** The receiver asked to be switched off (a webhook's 410 Gone): disable the destination. */
|
|
814
|
+
disable?: true;
|
|
815
|
+
};
|
|
816
|
+
interface ClaimRequest<Filter> {
|
|
817
|
+
/** Most deliveries to claim. */
|
|
818
|
+
limit: number;
|
|
819
|
+
/** What `run` was given: the app's own narrowing (one response, one account). */
|
|
820
|
+
filter?: Filter | undefined;
|
|
821
|
+
}
|
|
822
|
+
interface DeliveryRunnerOptions<Filter = undefined> {
|
|
823
|
+
/** Claims up to `limit` due deliveries, each held (leased) so no other runner sends it meanwhile. */
|
|
824
|
+
claim(request: ClaimRequest<Filter>): Promise<readonly ClaimedDelivery[]>;
|
|
825
|
+
/**
|
|
826
|
+
* Records an outcome. Resolve `false` when the claim was no longer held
|
|
827
|
+
* (its lease ran out and another runner took it): it is counted as stale.
|
|
828
|
+
* A rejection is logged and counted as unrecorded; the lease then runs out
|
|
829
|
+
* and the delivery comes back on its own.
|
|
830
|
+
*/
|
|
831
|
+
record(delivery: ClaimedDelivery, outcome: DeliveryOutcome): Promise<void | boolean>;
|
|
832
|
+
/** Keeps a destination's new state (a sheet's header and last row). Failures are logged, never fatal. */
|
|
833
|
+
saveState?: ((destinationId: string, state: Record<string, Json>) => Promise<void>) | undefined;
|
|
834
|
+
/** The destinations by kind. */
|
|
835
|
+
destinations: Readonly<Record<string, Destination>>;
|
|
836
|
+
now?: (() => number) | undefined;
|
|
837
|
+
/** Safe lines (ids, kinds, short reasons). Default: nothing is logged. */
|
|
838
|
+
log?: ((message: string) => void) | undefined;
|
|
839
|
+
/** Deliveries claimed per run. Default 25. */
|
|
840
|
+
batchSize?: number | undefined;
|
|
841
|
+
/** Counted attempts before a retryable failure is given up. Default `DEFAULT_MAX_ATTEMPTS` (8). */
|
|
842
|
+
maxAttempts?: number | undefined;
|
|
843
|
+
/**
|
|
844
|
+
* Deferrals (Retry-After, no attempt counted) allowed before a further one
|
|
845
|
+
* counts as an attempt and backs off. Default `DEFAULT_MAX_DEFERRALS` (24).
|
|
846
|
+
*/
|
|
847
|
+
maxDeferrals?: number | undefined;
|
|
848
|
+
/** Seconds to wait after a counted failed attempt. Default `defaultBackoff`. */
|
|
849
|
+
backoff?: ((attempt: number) => number) | undefined;
|
|
850
|
+
}
|
|
851
|
+
interface DeliveryReport {
|
|
852
|
+
claimed: number;
|
|
853
|
+
sent: number;
|
|
854
|
+
filtered: number;
|
|
855
|
+
skipped: number;
|
|
856
|
+
/** Retryable failures, backed off by attempt. */
|
|
857
|
+
retrying: number;
|
|
858
|
+
/** Retryable failures the service said when to retry; no attempt used. */
|
|
859
|
+
deferred: number;
|
|
860
|
+
failed: number;
|
|
861
|
+
/** Recorded after the claim was lost (`record` resolved false). */
|
|
862
|
+
stale: number;
|
|
863
|
+
/** `record` threw. */
|
|
864
|
+
unrecorded: number;
|
|
865
|
+
}
|
|
866
|
+
interface RunOptions<Filter> {
|
|
867
|
+
filter?: Filter | undefined;
|
|
868
|
+
/** Overrides `batchSize` for this run. */
|
|
869
|
+
limit?: number | undefined;
|
|
870
|
+
/** Aborts the sends in flight; their outcomes are recorded as retries. */
|
|
871
|
+
signal?: AbortSignal | undefined;
|
|
872
|
+
}
|
|
873
|
+
interface DeliveryRunner<Filter = undefined> {
|
|
874
|
+
/** One claim, its sends and their records. Never rejects for a delivery's failure; a failing `claim` rejects. */
|
|
875
|
+
run(options?: RunOptions<Filter>): Promise<DeliveryReport>;
|
|
876
|
+
}
|
|
877
|
+
declare const DEFAULT_MAX_ATTEMPTS = 8;
|
|
878
|
+
declare const DEFAULT_MAX_DEFERRALS = 24;
|
|
879
|
+
declare const DEFAULT_BATCH_SIZE = 25;
|
|
880
|
+
/**
|
|
881
|
+
* 5 minutes, quadrupling each attempt, at most 24 hours: 5m, 20m, 80m, 5h20m,
|
|
882
|
+
* 21h20m, then 24h. Across the default 8 attempts that is about 3 days, as
|
|
883
|
+
* Standard Webhooks suggests. Each wait is jittered by up to ±20%, so
|
|
884
|
+
* deliveries that failed together (a receiver's outage) don't all come back
|
|
885
|
+
* at once. `random` (default `Math.random`) is for tests.
|
|
886
|
+
*/
|
|
887
|
+
declare function defaultBackoff(attempt: number, random?: () => number): number;
|
|
888
|
+
/** Turns a destination's result into an outcome for a delivery on its `attempt`. */
|
|
889
|
+
declare function deliveryOutcome(result: DeliveryResult, attempt: number, options?: {
|
|
890
|
+
now?: number | undefined;
|
|
891
|
+
maxAttempts?: number | undefined;
|
|
892
|
+
backoff?: ((attempt: number) => number) | undefined;
|
|
893
|
+
/** Deferrals so far (`ClaimedDelivery.deferrals`). */
|
|
894
|
+
deferrals?: number | undefined;
|
|
895
|
+
maxDeferrals?: number | undefined;
|
|
896
|
+
}): DeliveryOutcome;
|
|
897
|
+
declare function createDeliveryRunner<Filter = undefined>(options: DeliveryRunnerOptions<Filter>): DeliveryRunner<Filter>;
|
|
898
|
+
|
|
899
|
+
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 DeriveSecretKeyOptions, 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, MIN_WEBHOOK_SECRET_BYTES, 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 };
|