@particle-academy/fancy-connector-core 0.6.1 → 0.7.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/CHANGELOG.md +48 -0
- package/README.md +6 -2
- package/dist/index.cjs +311 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -1
- package/dist/index.d.ts +114 -1
- package/dist/index.js +306 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1388,6 +1388,67 @@ declare function idempotencyKeyFor(ctx: unknown, nodeId: string, options?: Idemp
|
|
|
1388
1388
|
*/
|
|
1389
1389
|
declare const NO_IDEMPOTENCY_KEY_WARNING: string;
|
|
1390
1390
|
|
|
1391
|
+
/**
|
|
1392
|
+
* A subscription that EXPIRES, and when the host must act on it.
|
|
1393
|
+
*
|
|
1394
|
+
* `DeliveryMechanism` has said since 0.1.0 that a `subscription` trigger is a
|
|
1395
|
+
* webhook somebody has to renew, forever, and that if nobody does the workflow
|
|
1396
|
+
* stops firing with no error anywhere. This is the value for that duty — ONE
|
|
1397
|
+
* shape every expiring trigger declares, so a host runs one renewal scheduler
|
|
1398
|
+
* rather than learning a loop per connector, each with its own boundary bugs.
|
|
1399
|
+
*
|
|
1400
|
+
* Three facts and two verbs:
|
|
1401
|
+
*
|
|
1402
|
+
* - `expiresAt` — the provider's expiry, as an RFC 3339 instant. Google Calendar
|
|
1403
|
+
* hands back `expiration` as epoch MILLISECONDS in a string and Microsoft
|
|
1404
|
+
* Graph hands back `expirationDateTime` as ISO 8601; the CONNECTOR converts on
|
|
1405
|
+
* the way in, and this module refuses anything that is not an instant rather
|
|
1406
|
+
* than guessing units.
|
|
1407
|
+
* - `renewBeforeSeconds` — the connector's margin. Positive, because zero would
|
|
1408
|
+
* make `due` unreachable (the moment it applied, `expired` would win) — a
|
|
1409
|
+
* lease nobody ever renews, declared in a way nothing would report.
|
|
1410
|
+
* - `renewOperation` — the operation the host calls when the lease is due. For
|
|
1411
|
+
* Graph that is a renew; for a Google Calendar channel, which cannot be
|
|
1412
|
+
* renewed, it is the create again (the connector stops the old channel
|
|
1413
|
+
* itself). The lease says WHEN; the connector says WHAT.
|
|
1414
|
+
*
|
|
1415
|
+
* - `leaseState` — where the lease IS: `active`, `due` (inclusive at renewAt),
|
|
1416
|
+
* `expired` (inclusive at expiresAt, and it wins over `due`: a lease that has
|
|
1417
|
+
* just expired cannot be renewed).
|
|
1418
|
+
* - `leaseAction` — what the host DOES: `none`, `renew`, or `resync`. A missed
|
|
1419
|
+
* lease is never a quiet re-create: notifications during the gap are gone, so
|
|
1420
|
+
* the host must re-list (sync token or full) AND re-subscribe.
|
|
1421
|
+
*
|
|
1422
|
+
* The boundaries are DECIDED, not measured, and they are pinned in
|
|
1423
|
+
* `fixtures/subscription-lease/cases.json`, which the PHP twin reads too.
|
|
1424
|
+
*/
|
|
1425
|
+
|
|
1426
|
+
type SubscriptionLease = {
|
|
1427
|
+
/** The provider's expiry, an RFC 3339 instant. */
|
|
1428
|
+
expiresAt: string;
|
|
1429
|
+
/** How early the host renews, in seconds. Positive. */
|
|
1430
|
+
renewBeforeSeconds: number;
|
|
1431
|
+
/** The operation the host calls when the lease is due. */
|
|
1432
|
+
renewOperation: string;
|
|
1433
|
+
};
|
|
1434
|
+
type LeaseState = "active" | "due" | "expired";
|
|
1435
|
+
type LeaseAction = "none" | "renew" | "resync";
|
|
1436
|
+
/**
|
|
1437
|
+
* Validate a declaration and return the lease — a plain value a host stores
|
|
1438
|
+
* beside the trigger and hands back to `leaseState` / `leaseAction` later.
|
|
1439
|
+
*/
|
|
1440
|
+
declare function subscriptionLease(input: {
|
|
1441
|
+
expiresAt: string;
|
|
1442
|
+
renewBeforeSeconds: number;
|
|
1443
|
+
renewOperation: string;
|
|
1444
|
+
}, at?: ConnectorErrorContext): SubscriptionLease;
|
|
1445
|
+
/** The instant the lease becomes due: `expiresAt - renewBeforeSeconds`. */
|
|
1446
|
+
declare function leaseRenewAt(lease: SubscriptionLease): string;
|
|
1447
|
+
/** Where the lease is at `now`. Both boundaries are inclusive; `expired` wins. */
|
|
1448
|
+
declare function leaseState(lease: SubscriptionLease, now: string | Date): LeaseState;
|
|
1449
|
+
/** What the host does about the lease at `now`. */
|
|
1450
|
+
declare function leaseAction(lease: SubscriptionLease, now: string | Date): LeaseAction;
|
|
1451
|
+
|
|
1391
1452
|
/**
|
|
1392
1453
|
* Measuring and slicing text the way providers actually measure and slice it.
|
|
1393
1454
|
*
|
|
@@ -2058,6 +2119,58 @@ declare function providerProblems(provider: ProviderAdapter): string[];
|
|
|
2058
2119
|
/** True when a provider's declared sandbox can actually be selected at run time. */
|
|
2059
2120
|
declare function providerSandboxIsSelectable(provider: ProviderAdapter): boolean;
|
|
2060
2121
|
|
|
2122
|
+
/**
|
|
2123
|
+
* Raw MIME in, headers / parts / attachments out — the payload TRANSFORM an
|
|
2124
|
+
* inbound-email trigger declares.
|
|
2125
|
+
*
|
|
2126
|
+
* Written from scratch (the owner: no third-party code), as ONE implementation
|
|
2127
|
+
* in two runtimes — `php/src/Mime.php` is the twin — and held to the authored
|
|
2128
|
+
* corpus under `fixtures/mime/`, which both must reproduce byte for byte.
|
|
2129
|
+
* `fixtures/mime/README.md` lists every decision a case pins; the ones that
|
|
2130
|
+
* are easy to get differently in two languages are restated here:
|
|
2131
|
+
*
|
|
2132
|
+
* - Header lines are read as Latin-1 (one byte, one char), unfolded with a
|
|
2133
|
+
* single space, names lowercased, values RFC 2047-decoded (B and Q; adjacent
|
|
2134
|
+
* encoded-words join without the whitespace between them).
|
|
2135
|
+
* - The line break before a boundary delimiter belongs to the DELIMITER.
|
|
2136
|
+
* - `size` is the transfer-decoded byte length, before any charset conversion.
|
|
2137
|
+
* - A text part comes back as UTF-8 with the message's own line endings. A
|
|
2138
|
+
* charset this module does not decode (anything but utf-8, us-ascii,
|
|
2139
|
+
* iso-8859-1, iso-8859-15, windows-1252) leaves the part with no text.
|
|
2140
|
+
* - Every non-text leaf is an attachment, as is a text leaf declared
|
|
2141
|
+
* `attachment`; attachments carry their bytes as base64. `filename*` (RFC
|
|
2142
|
+
* 2231) wins over `filename`, which wins over Content-Type's `name`.
|
|
2143
|
+
* - `messageId` is the header verbatim; `contentId` has its angle brackets
|
|
2144
|
+
* stripped, because `cid:` references omit them.
|
|
2145
|
+
*/
|
|
2146
|
+
type MimeHeader = {
|
|
2147
|
+
name: string;
|
|
2148
|
+
value: string;
|
|
2149
|
+
};
|
|
2150
|
+
type MimePart = {
|
|
2151
|
+
index: number;
|
|
2152
|
+
contentType: string;
|
|
2153
|
+
charset: string | null;
|
|
2154
|
+
disposition: "inline" | "attachment" | null;
|
|
2155
|
+
filename: string | null;
|
|
2156
|
+
contentId: string | null;
|
|
2157
|
+
size: number;
|
|
2158
|
+
headers: MimeHeader[];
|
|
2159
|
+
};
|
|
2160
|
+
type MimeAttachment = MimePart & {
|
|
2161
|
+
content: string;
|
|
2162
|
+
};
|
|
2163
|
+
type MimeMessage = {
|
|
2164
|
+
headers: MimeHeader[];
|
|
2165
|
+
subject: string | null;
|
|
2166
|
+
messageId: string | null;
|
|
2167
|
+
text: string | null;
|
|
2168
|
+
html: string | null;
|
|
2169
|
+
parts: MimePart[];
|
|
2170
|
+
attachments: MimeAttachment[];
|
|
2171
|
+
};
|
|
2172
|
+
declare function parseMime(raw: Uint8Array | string): MimeMessage;
|
|
2173
|
+
|
|
2061
2174
|
/**
|
|
2062
2175
|
* Dry-verify — proving the transport with a credential that cannot work.
|
|
2063
2176
|
*
|
|
@@ -2323,4 +2436,4 @@ declare function verifyDelivery(trigger: TriggerDescriptor, delivery: InboundDel
|
|
|
2323
2436
|
*/
|
|
2324
2437
|
declare function triggerEvent(trigger: TriggerDescriptor, connection: ResolvedConnection, injected: unknown, config: Record<string, unknown>): unknown;
|
|
2325
2438
|
|
|
2326
|
-
export { AMBIGUOUS_REFUSAL, type ApiContract, type Attempt, type ByteRange, CANONICAL_METRICS, CONNECTOR_API_VERSION, CONNECTOR_MODES, CREDENTIAL_SCOPES, type CallOptions, type CallResult, type Capabilities, type Catalogue, type ChainLinks, type ChainOutcome, type ChainRef, type Citation, type Classified, type ConnectionCredentials, type ConnectionHost, type ConnectionSpec, type Connector, ConnectorAmbiguous, ConnectorApiMismatch, ConnectorAuthError, ConnectorConfigError, ConnectorError, type ConnectorErrorContext, type ConnectorFaker, ConnectorIdempotencyExpiredError, type ConnectorMode, ConnectorModeError, ConnectorRateLimited, type ConnectorRequest, ConnectorRequestError, type ConnectorResult, ConnectorTransient, ConnectorUnreachable, type ContractOperation, type CredentialField, type CredentialScope, DEFAULT_IDEMPOTENCY_WINDOW_SECONDS, DEFAULT_RETRY, type DeliveryDeclaration, type DeliveryMechanism, type DeliveryOutcome, type DriftFinding, type DriftReport, FAKE_EPOCH, type FailureKind, type FakeRequest, type FakeValues, type HmacScheme, type HostEnvironment, type IdempotencyOptions, type InboundDelivery, MAX_IDEMPOTENCY_KEY_LENGTH, type MetricDescriptor, type MetricSample, type ModeInputs, NO_IDEMPOTENCY_KEY_WARNING, PROBLEM_SEVERITIES, type PreparedRequest, type ProbeOutcome, type ProbeReport, type ProbeResult, type ProbeSpec, type Problem, type ProviderAdapter, RENDERER_VERSION, REVIEW_STALE_DAYS, type RecordedShape, type RenderRules, type RenderedPayload, type RequestedMode, type ResolveOptions, type ResolvedConnection, type RetryPolicy, type RunIdentity, SANDBOX_KINDS, SUPPORTED_CONNECTOR_API, type SandboxKind, type Segment, type ServiceDescriptor, type SetupStep, type ShapeMismatch, type SpecSource, type TextUnit, type Transport, type TransportResponse, type TriggerDescriptor, type VerifyResult, type WebhookVerification, type WebhookVerificationSpec, assertConnectorApi, byteRangeOf, callConnector, capabilityProblems, checkAgainstOpenApi, checkAgainstRecordedShape, classifyError, classifyHttp, classifyStatus, classifyThrown, compareShape, connectionsFor, constantTimeEquals, deliver, fakeRequest, fakeValues, getConnectionHost, graphemes, header, hmac, httpFailure, idempotencyKeyFor, isEmptyPayload, isRemote, isUnconditionallyRetryable, linkRanges, measure, payloadHash, postChain, probe, providerProblems, providerSandboxIsSelectable, registerConnectionHost, registerTransport, render, reported, resetRateState, resolveConnection, resolveConnectorMode, respectRate, runIdentityFrom, runKeyFrom, runProbes, sandboxIsSelectable, sandboxRefusal, satisfiesMinimum, seedFrom, shapeOf, shouldRetry, sliceByteRange, splitToFit, triggerEvent, unchecked, utf8Length, verifyDelivery, verifyHmac, withResolvedLimit };
|
|
2439
|
+
export { AMBIGUOUS_REFUSAL, type ApiContract, type Attempt, type ByteRange, CANONICAL_METRICS, CONNECTOR_API_VERSION, CONNECTOR_MODES, CREDENTIAL_SCOPES, type CallOptions, type CallResult, type Capabilities, type Catalogue, type ChainLinks, type ChainOutcome, type ChainRef, type Citation, type Classified, type ConnectionCredentials, type ConnectionHost, type ConnectionSpec, type Connector, ConnectorAmbiguous, ConnectorApiMismatch, ConnectorAuthError, ConnectorConfigError, ConnectorError, type ConnectorErrorContext, type ConnectorFaker, ConnectorIdempotencyExpiredError, type ConnectorMode, ConnectorModeError, ConnectorRateLimited, type ConnectorRequest, ConnectorRequestError, type ConnectorResult, ConnectorTransient, ConnectorUnreachable, type ContractOperation, type CredentialField, type CredentialScope, DEFAULT_IDEMPOTENCY_WINDOW_SECONDS, DEFAULT_RETRY, type DeliveryDeclaration, type DeliveryMechanism, type DeliveryOutcome, type DriftFinding, type DriftReport, FAKE_EPOCH, type FailureKind, type FakeRequest, type FakeValues, type HmacScheme, type HostEnvironment, type IdempotencyOptions, type InboundDelivery, type LeaseAction, type LeaseState, MAX_IDEMPOTENCY_KEY_LENGTH, type MetricDescriptor, type MetricSample, type MimeAttachment, type MimeHeader, type MimeMessage, type MimePart, type ModeInputs, NO_IDEMPOTENCY_KEY_WARNING, PROBLEM_SEVERITIES, type PreparedRequest, type ProbeOutcome, type ProbeReport, type ProbeResult, type ProbeSpec, type Problem, type ProviderAdapter, RENDERER_VERSION, REVIEW_STALE_DAYS, type RecordedShape, type RenderRules, type RenderedPayload, type RequestedMode, type ResolveOptions, type ResolvedConnection, type RetryPolicy, type RunIdentity, SANDBOX_KINDS, SUPPORTED_CONNECTOR_API, type SandboxKind, type Segment, type ServiceDescriptor, type SetupStep, type ShapeMismatch, type SpecSource, type SubscriptionLease, type TextUnit, type Transport, type TransportResponse, type TriggerDescriptor, type VerifyResult, type WebhookVerification, type WebhookVerificationSpec, assertConnectorApi, byteRangeOf, callConnector, capabilityProblems, checkAgainstOpenApi, checkAgainstRecordedShape, classifyError, classifyHttp, classifyStatus, classifyThrown, compareShape, connectionsFor, constantTimeEquals, deliver, fakeRequest, fakeValues, getConnectionHost, graphemes, header, hmac, httpFailure, idempotencyKeyFor, isEmptyPayload, isRemote, isUnconditionallyRetryable, leaseAction, leaseRenewAt, leaseState, linkRanges, measure, parseMime, payloadHash, postChain, probe, providerProblems, providerSandboxIsSelectable, registerConnectionHost, registerTransport, render, reported, resetRateState, resolveConnection, resolveConnectorMode, respectRate, runIdentityFrom, runKeyFrom, runProbes, sandboxIsSelectable, sandboxRefusal, satisfiesMinimum, seedFrom, shapeOf, shouldRetry, sliceByteRange, splitToFit, subscriptionLease, triggerEvent, unchecked, utf8Length, verifyDelivery, verifyHmac, withResolvedLimit };
|
package/dist/index.d.ts
CHANGED
|
@@ -1388,6 +1388,67 @@ declare function idempotencyKeyFor(ctx: unknown, nodeId: string, options?: Idemp
|
|
|
1388
1388
|
*/
|
|
1389
1389
|
declare const NO_IDEMPOTENCY_KEY_WARNING: string;
|
|
1390
1390
|
|
|
1391
|
+
/**
|
|
1392
|
+
* A subscription that EXPIRES, and when the host must act on it.
|
|
1393
|
+
*
|
|
1394
|
+
* `DeliveryMechanism` has said since 0.1.0 that a `subscription` trigger is a
|
|
1395
|
+
* webhook somebody has to renew, forever, and that if nobody does the workflow
|
|
1396
|
+
* stops firing with no error anywhere. This is the value for that duty — ONE
|
|
1397
|
+
* shape every expiring trigger declares, so a host runs one renewal scheduler
|
|
1398
|
+
* rather than learning a loop per connector, each with its own boundary bugs.
|
|
1399
|
+
*
|
|
1400
|
+
* Three facts and two verbs:
|
|
1401
|
+
*
|
|
1402
|
+
* - `expiresAt` — the provider's expiry, as an RFC 3339 instant. Google Calendar
|
|
1403
|
+
* hands back `expiration` as epoch MILLISECONDS in a string and Microsoft
|
|
1404
|
+
* Graph hands back `expirationDateTime` as ISO 8601; the CONNECTOR converts on
|
|
1405
|
+
* the way in, and this module refuses anything that is not an instant rather
|
|
1406
|
+
* than guessing units.
|
|
1407
|
+
* - `renewBeforeSeconds` — the connector's margin. Positive, because zero would
|
|
1408
|
+
* make `due` unreachable (the moment it applied, `expired` would win) — a
|
|
1409
|
+
* lease nobody ever renews, declared in a way nothing would report.
|
|
1410
|
+
* - `renewOperation` — the operation the host calls when the lease is due. For
|
|
1411
|
+
* Graph that is a renew; for a Google Calendar channel, which cannot be
|
|
1412
|
+
* renewed, it is the create again (the connector stops the old channel
|
|
1413
|
+
* itself). The lease says WHEN; the connector says WHAT.
|
|
1414
|
+
*
|
|
1415
|
+
* - `leaseState` — where the lease IS: `active`, `due` (inclusive at renewAt),
|
|
1416
|
+
* `expired` (inclusive at expiresAt, and it wins over `due`: a lease that has
|
|
1417
|
+
* just expired cannot be renewed).
|
|
1418
|
+
* - `leaseAction` — what the host DOES: `none`, `renew`, or `resync`. A missed
|
|
1419
|
+
* lease is never a quiet re-create: notifications during the gap are gone, so
|
|
1420
|
+
* the host must re-list (sync token or full) AND re-subscribe.
|
|
1421
|
+
*
|
|
1422
|
+
* The boundaries are DECIDED, not measured, and they are pinned in
|
|
1423
|
+
* `fixtures/subscription-lease/cases.json`, which the PHP twin reads too.
|
|
1424
|
+
*/
|
|
1425
|
+
|
|
1426
|
+
type SubscriptionLease = {
|
|
1427
|
+
/** The provider's expiry, an RFC 3339 instant. */
|
|
1428
|
+
expiresAt: string;
|
|
1429
|
+
/** How early the host renews, in seconds. Positive. */
|
|
1430
|
+
renewBeforeSeconds: number;
|
|
1431
|
+
/** The operation the host calls when the lease is due. */
|
|
1432
|
+
renewOperation: string;
|
|
1433
|
+
};
|
|
1434
|
+
type LeaseState = "active" | "due" | "expired";
|
|
1435
|
+
type LeaseAction = "none" | "renew" | "resync";
|
|
1436
|
+
/**
|
|
1437
|
+
* Validate a declaration and return the lease — a plain value a host stores
|
|
1438
|
+
* beside the trigger and hands back to `leaseState` / `leaseAction` later.
|
|
1439
|
+
*/
|
|
1440
|
+
declare function subscriptionLease(input: {
|
|
1441
|
+
expiresAt: string;
|
|
1442
|
+
renewBeforeSeconds: number;
|
|
1443
|
+
renewOperation: string;
|
|
1444
|
+
}, at?: ConnectorErrorContext): SubscriptionLease;
|
|
1445
|
+
/** The instant the lease becomes due: `expiresAt - renewBeforeSeconds`. */
|
|
1446
|
+
declare function leaseRenewAt(lease: SubscriptionLease): string;
|
|
1447
|
+
/** Where the lease is at `now`. Both boundaries are inclusive; `expired` wins. */
|
|
1448
|
+
declare function leaseState(lease: SubscriptionLease, now: string | Date): LeaseState;
|
|
1449
|
+
/** What the host does about the lease at `now`. */
|
|
1450
|
+
declare function leaseAction(lease: SubscriptionLease, now: string | Date): LeaseAction;
|
|
1451
|
+
|
|
1391
1452
|
/**
|
|
1392
1453
|
* Measuring and slicing text the way providers actually measure and slice it.
|
|
1393
1454
|
*
|
|
@@ -2058,6 +2119,58 @@ declare function providerProblems(provider: ProviderAdapter): string[];
|
|
|
2058
2119
|
/** True when a provider's declared sandbox can actually be selected at run time. */
|
|
2059
2120
|
declare function providerSandboxIsSelectable(provider: ProviderAdapter): boolean;
|
|
2060
2121
|
|
|
2122
|
+
/**
|
|
2123
|
+
* Raw MIME in, headers / parts / attachments out — the payload TRANSFORM an
|
|
2124
|
+
* inbound-email trigger declares.
|
|
2125
|
+
*
|
|
2126
|
+
* Written from scratch (the owner: no third-party code), as ONE implementation
|
|
2127
|
+
* in two runtimes — `php/src/Mime.php` is the twin — and held to the authored
|
|
2128
|
+
* corpus under `fixtures/mime/`, which both must reproduce byte for byte.
|
|
2129
|
+
* `fixtures/mime/README.md` lists every decision a case pins; the ones that
|
|
2130
|
+
* are easy to get differently in two languages are restated here:
|
|
2131
|
+
*
|
|
2132
|
+
* - Header lines are read as Latin-1 (one byte, one char), unfolded with a
|
|
2133
|
+
* single space, names lowercased, values RFC 2047-decoded (B and Q; adjacent
|
|
2134
|
+
* encoded-words join without the whitespace between them).
|
|
2135
|
+
* - The line break before a boundary delimiter belongs to the DELIMITER.
|
|
2136
|
+
* - `size` is the transfer-decoded byte length, before any charset conversion.
|
|
2137
|
+
* - A text part comes back as UTF-8 with the message's own line endings. A
|
|
2138
|
+
* charset this module does not decode (anything but utf-8, us-ascii,
|
|
2139
|
+
* iso-8859-1, iso-8859-15, windows-1252) leaves the part with no text.
|
|
2140
|
+
* - Every non-text leaf is an attachment, as is a text leaf declared
|
|
2141
|
+
* `attachment`; attachments carry their bytes as base64. `filename*` (RFC
|
|
2142
|
+
* 2231) wins over `filename`, which wins over Content-Type's `name`.
|
|
2143
|
+
* - `messageId` is the header verbatim; `contentId` has its angle brackets
|
|
2144
|
+
* stripped, because `cid:` references omit them.
|
|
2145
|
+
*/
|
|
2146
|
+
type MimeHeader = {
|
|
2147
|
+
name: string;
|
|
2148
|
+
value: string;
|
|
2149
|
+
};
|
|
2150
|
+
type MimePart = {
|
|
2151
|
+
index: number;
|
|
2152
|
+
contentType: string;
|
|
2153
|
+
charset: string | null;
|
|
2154
|
+
disposition: "inline" | "attachment" | null;
|
|
2155
|
+
filename: string | null;
|
|
2156
|
+
contentId: string | null;
|
|
2157
|
+
size: number;
|
|
2158
|
+
headers: MimeHeader[];
|
|
2159
|
+
};
|
|
2160
|
+
type MimeAttachment = MimePart & {
|
|
2161
|
+
content: string;
|
|
2162
|
+
};
|
|
2163
|
+
type MimeMessage = {
|
|
2164
|
+
headers: MimeHeader[];
|
|
2165
|
+
subject: string | null;
|
|
2166
|
+
messageId: string | null;
|
|
2167
|
+
text: string | null;
|
|
2168
|
+
html: string | null;
|
|
2169
|
+
parts: MimePart[];
|
|
2170
|
+
attachments: MimeAttachment[];
|
|
2171
|
+
};
|
|
2172
|
+
declare function parseMime(raw: Uint8Array | string): MimeMessage;
|
|
2173
|
+
|
|
2061
2174
|
/**
|
|
2062
2175
|
* Dry-verify — proving the transport with a credential that cannot work.
|
|
2063
2176
|
*
|
|
@@ -2323,4 +2436,4 @@ declare function verifyDelivery(trigger: TriggerDescriptor, delivery: InboundDel
|
|
|
2323
2436
|
*/
|
|
2324
2437
|
declare function triggerEvent(trigger: TriggerDescriptor, connection: ResolvedConnection, injected: unknown, config: Record<string, unknown>): unknown;
|
|
2325
2438
|
|
|
2326
|
-
export { AMBIGUOUS_REFUSAL, type ApiContract, type Attempt, type ByteRange, CANONICAL_METRICS, CONNECTOR_API_VERSION, CONNECTOR_MODES, CREDENTIAL_SCOPES, type CallOptions, type CallResult, type Capabilities, type Catalogue, type ChainLinks, type ChainOutcome, type ChainRef, type Citation, type Classified, type ConnectionCredentials, type ConnectionHost, type ConnectionSpec, type Connector, ConnectorAmbiguous, ConnectorApiMismatch, ConnectorAuthError, ConnectorConfigError, ConnectorError, type ConnectorErrorContext, type ConnectorFaker, ConnectorIdempotencyExpiredError, type ConnectorMode, ConnectorModeError, ConnectorRateLimited, type ConnectorRequest, ConnectorRequestError, type ConnectorResult, ConnectorTransient, ConnectorUnreachable, type ContractOperation, type CredentialField, type CredentialScope, DEFAULT_IDEMPOTENCY_WINDOW_SECONDS, DEFAULT_RETRY, type DeliveryDeclaration, type DeliveryMechanism, type DeliveryOutcome, type DriftFinding, type DriftReport, FAKE_EPOCH, type FailureKind, type FakeRequest, type FakeValues, type HmacScheme, type HostEnvironment, type IdempotencyOptions, type InboundDelivery, MAX_IDEMPOTENCY_KEY_LENGTH, type MetricDescriptor, type MetricSample, type ModeInputs, NO_IDEMPOTENCY_KEY_WARNING, PROBLEM_SEVERITIES, type PreparedRequest, type ProbeOutcome, type ProbeReport, type ProbeResult, type ProbeSpec, type Problem, type ProviderAdapter, RENDERER_VERSION, REVIEW_STALE_DAYS, type RecordedShape, type RenderRules, type RenderedPayload, type RequestedMode, type ResolveOptions, type ResolvedConnection, type RetryPolicy, type RunIdentity, SANDBOX_KINDS, SUPPORTED_CONNECTOR_API, type SandboxKind, type Segment, type ServiceDescriptor, type SetupStep, type ShapeMismatch, type SpecSource, type TextUnit, type Transport, type TransportResponse, type TriggerDescriptor, type VerifyResult, type WebhookVerification, type WebhookVerificationSpec, assertConnectorApi, byteRangeOf, callConnector, capabilityProblems, checkAgainstOpenApi, checkAgainstRecordedShape, classifyError, classifyHttp, classifyStatus, classifyThrown, compareShape, connectionsFor, constantTimeEquals, deliver, fakeRequest, fakeValues, getConnectionHost, graphemes, header, hmac, httpFailure, idempotencyKeyFor, isEmptyPayload, isRemote, isUnconditionallyRetryable, linkRanges, measure, payloadHash, postChain, probe, providerProblems, providerSandboxIsSelectable, registerConnectionHost, registerTransport, render, reported, resetRateState, resolveConnection, resolveConnectorMode, respectRate, runIdentityFrom, runKeyFrom, runProbes, sandboxIsSelectable, sandboxRefusal, satisfiesMinimum, seedFrom, shapeOf, shouldRetry, sliceByteRange, splitToFit, triggerEvent, unchecked, utf8Length, verifyDelivery, verifyHmac, withResolvedLimit };
|
|
2439
|
+
export { AMBIGUOUS_REFUSAL, type ApiContract, type Attempt, type ByteRange, CANONICAL_METRICS, CONNECTOR_API_VERSION, CONNECTOR_MODES, CREDENTIAL_SCOPES, type CallOptions, type CallResult, type Capabilities, type Catalogue, type ChainLinks, type ChainOutcome, type ChainRef, type Citation, type Classified, type ConnectionCredentials, type ConnectionHost, type ConnectionSpec, type Connector, ConnectorAmbiguous, ConnectorApiMismatch, ConnectorAuthError, ConnectorConfigError, ConnectorError, type ConnectorErrorContext, type ConnectorFaker, ConnectorIdempotencyExpiredError, type ConnectorMode, ConnectorModeError, ConnectorRateLimited, type ConnectorRequest, ConnectorRequestError, type ConnectorResult, ConnectorTransient, ConnectorUnreachable, type ContractOperation, type CredentialField, type CredentialScope, DEFAULT_IDEMPOTENCY_WINDOW_SECONDS, DEFAULT_RETRY, type DeliveryDeclaration, type DeliveryMechanism, type DeliveryOutcome, type DriftFinding, type DriftReport, FAKE_EPOCH, type FailureKind, type FakeRequest, type FakeValues, type HmacScheme, type HostEnvironment, type IdempotencyOptions, type InboundDelivery, type LeaseAction, type LeaseState, MAX_IDEMPOTENCY_KEY_LENGTH, type MetricDescriptor, type MetricSample, type MimeAttachment, type MimeHeader, type MimeMessage, type MimePart, type ModeInputs, NO_IDEMPOTENCY_KEY_WARNING, PROBLEM_SEVERITIES, type PreparedRequest, type ProbeOutcome, type ProbeReport, type ProbeResult, type ProbeSpec, type Problem, type ProviderAdapter, RENDERER_VERSION, REVIEW_STALE_DAYS, type RecordedShape, type RenderRules, type RenderedPayload, type RequestedMode, type ResolveOptions, type ResolvedConnection, type RetryPolicy, type RunIdentity, SANDBOX_KINDS, SUPPORTED_CONNECTOR_API, type SandboxKind, type Segment, type ServiceDescriptor, type SetupStep, type ShapeMismatch, type SpecSource, type SubscriptionLease, type TextUnit, type Transport, type TransportResponse, type TriggerDescriptor, type VerifyResult, type WebhookVerification, type WebhookVerificationSpec, assertConnectorApi, byteRangeOf, callConnector, capabilityProblems, checkAgainstOpenApi, checkAgainstRecordedShape, classifyError, classifyHttp, classifyStatus, classifyThrown, compareShape, connectionsFor, constantTimeEquals, deliver, fakeRequest, fakeValues, getConnectionHost, graphemes, header, hmac, httpFailure, idempotencyKeyFor, isEmptyPayload, isRemote, isUnconditionallyRetryable, leaseAction, leaseRenewAt, leaseState, linkRanges, measure, parseMime, payloadHash, postChain, probe, providerProblems, providerSandboxIsSelectable, registerConnectionHost, registerTransport, render, reported, resetRateState, resolveConnection, resolveConnectorMode, respectRate, runIdentityFrom, runKeyFrom, runProbes, sandboxIsSelectable, sandboxRefusal, satisfiesMinimum, seedFrom, shapeOf, shouldRetry, sliceByteRange, splitToFit, subscriptionLease, triggerEvent, unchecked, utf8Length, verifyDelivery, verifyHmac, withResolvedLimit };
|
package/dist/index.js
CHANGED
|
@@ -34,7 +34,7 @@ function assertConnectorApi(connectorId, declared) {
|
|
|
34
34
|
if (SUPPORTED_CONNECTOR_API.includes(declared)) return;
|
|
35
35
|
const supported = SUPPORTED_CONNECTOR_API.join(", ");
|
|
36
36
|
throw new ConnectorApiMismatch(
|
|
37
|
-
declared > CONNECTOR_API_VERSION ? `"${connectorId}" was written against connector API ${declared}, and this core implements ${supported}. The CONNECTOR is newer: upgrade @particle-academy/fancy-connector-core. Vendoring a connector ahead of the core it needs is the one direction that cannot be made to work by trying.` : `"${connectorId}" was written against connector API ${declared}, which is older than anything this core still runs (${supported}). The CONNECTOR is behind:
|
|
37
|
+
declared > CONNECTOR_API_VERSION ? `"${connectorId}" was written against connector API ${declared}, and this core implements ${supported}. The CONNECTOR is newer: upgrade @particle-academy/fancy-connector-core. Vendoring a connector ahead of the core it needs is the one direction that cannot be made to work by trying.` : `"${connectorId}" was written against connector API ${declared}, which is older than anything this core still runs (${supported}). The CONNECTOR is behind: move it to a release written for this core \u2014 upgrade its package (@particle-academy/${connectorId}-js, particle-academy/${connectorId}-php), or regenerate a vendored copy from that release; its CHANGELOG names the connector API each release targets. Nothing is adapted automatically, because guessing what a two-version-old connector meant is how a connector quietly starts doing something else.`,
|
|
38
38
|
connectorId,
|
|
39
39
|
declared
|
|
40
40
|
);
|
|
@@ -595,21 +595,21 @@ async function prepare(service, connection, options) {
|
|
|
595
595
|
}
|
|
596
596
|
function encodeForm(form) {
|
|
597
597
|
const pairs = [];
|
|
598
|
-
const
|
|
598
|
+
const walk2 = (prefix, value) => {
|
|
599
599
|
if (value === void 0 || value === null) return;
|
|
600
600
|
if (Array.isArray(value)) {
|
|
601
|
-
value.forEach((item, index) =>
|
|
601
|
+
value.forEach((item, index) => walk2(`${prefix}[${index}]`, item));
|
|
602
602
|
return;
|
|
603
603
|
}
|
|
604
604
|
if (typeof value === "object") {
|
|
605
605
|
for (const [key, nested] of Object.entries(value)) {
|
|
606
|
-
|
|
606
|
+
walk2(prefix === "" ? key : `${prefix}[${key}]`, nested);
|
|
607
607
|
}
|
|
608
608
|
return;
|
|
609
609
|
}
|
|
610
610
|
pairs.push(`${encodeURIComponent(prefix)}=${encodeURIComponent(String(value))}`);
|
|
611
611
|
};
|
|
612
|
-
|
|
612
|
+
walk2("", form);
|
|
613
613
|
return pairs.join("&");
|
|
614
614
|
}
|
|
615
615
|
var fetchTransport = async (request) => {
|
|
@@ -913,6 +913,59 @@ function fnv1a(value) {
|
|
|
913
913
|
return hash.toString(16).padStart(8, "0");
|
|
914
914
|
}
|
|
915
915
|
|
|
916
|
+
// src/lease.ts
|
|
917
|
+
var INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
918
|
+
var DEFAULT_CONTEXT = { service: "", operation: "subscription" };
|
|
919
|
+
function instantMs(value, field, at) {
|
|
920
|
+
if (typeof value !== "string" || !INSTANT.test(value)) {
|
|
921
|
+
throw new ConnectorConfigError(
|
|
922
|
+
`${field} must be an RFC 3339 instant such as "2026-09-22T00:00:00Z", got ${JSON.stringify(value)}. A provider's own shape \u2014 Google's epoch milliseconds, say \u2014 is converted by the connector, never guessed at here.`,
|
|
923
|
+
at
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
const ms = Date.parse(value);
|
|
927
|
+
if (Number.isNaN(ms)) {
|
|
928
|
+
throw new ConnectorConfigError(`${field} is not a real instant: ${JSON.stringify(value)}`, at);
|
|
929
|
+
}
|
|
930
|
+
return ms;
|
|
931
|
+
}
|
|
932
|
+
function subscriptionLease(input, at = DEFAULT_CONTEXT) {
|
|
933
|
+
instantMs(input.expiresAt, "expiresAt", at);
|
|
934
|
+
if (!Number.isInteger(input.renewBeforeSeconds) || input.renewBeforeSeconds <= 0) {
|
|
935
|
+
throw new ConnectorConfigError(
|
|
936
|
+
`renewBeforeSeconds must be a positive whole number of seconds, got ${JSON.stringify(input.renewBeforeSeconds)}. Zero would make the lease due only at the instant it expires, which is a lease nobody renews.`,
|
|
937
|
+
at
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
if (typeof input.renewOperation !== "string" || input.renewOperation.trim() === "") {
|
|
941
|
+
throw new ConnectorConfigError(
|
|
942
|
+
"renewOperation must name the operation the host calls when the lease is due; a due lease with nothing to call is one nobody renews.",
|
|
943
|
+
at
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
return { expiresAt: input.expiresAt, renewBeforeSeconds: input.renewBeforeSeconds, renewOperation: input.renewOperation };
|
|
947
|
+
}
|
|
948
|
+
function leaseRenewAt(lease) {
|
|
949
|
+
return new Date(instantMs(lease.expiresAt, "expiresAt", DEFAULT_CONTEXT) - lease.renewBeforeSeconds * 1e3).toISOString();
|
|
950
|
+
}
|
|
951
|
+
function leaseState(lease, now) {
|
|
952
|
+
const at = now instanceof Date ? now.getTime() : instantMs(now, "now", DEFAULT_CONTEXT);
|
|
953
|
+
const expires = instantMs(lease.expiresAt, "expiresAt", DEFAULT_CONTEXT);
|
|
954
|
+
if (at >= expires) return "expired";
|
|
955
|
+
if (at >= expires - lease.renewBeforeSeconds * 1e3) return "due";
|
|
956
|
+
return "active";
|
|
957
|
+
}
|
|
958
|
+
function leaseAction(lease, now) {
|
|
959
|
+
switch (leaseState(lease, now)) {
|
|
960
|
+
case "active":
|
|
961
|
+
return "none";
|
|
962
|
+
case "due":
|
|
963
|
+
return "renew";
|
|
964
|
+
case "expired":
|
|
965
|
+
return "resync";
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
916
969
|
// src/metrics.ts
|
|
917
970
|
function compareShape(connectorId, declared, produced) {
|
|
918
971
|
const declaredKeys = new Set((declared ?? []).map((metric) => metric.key));
|
|
@@ -1007,6 +1060,245 @@ function providerSandboxIsSelectable(provider) {
|
|
|
1007
1060
|
return sandboxIsSelectable(provider.sandbox);
|
|
1008
1061
|
}
|
|
1009
1062
|
|
|
1063
|
+
// src/mime.ts
|
|
1064
|
+
var CR = 13;
|
|
1065
|
+
var LF = 10;
|
|
1066
|
+
function latin1(bytes) {
|
|
1067
|
+
let s = "";
|
|
1068
|
+
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
|
|
1069
|
+
return s;
|
|
1070
|
+
}
|
|
1071
|
+
function bytesOfLatin1(s) {
|
|
1072
|
+
const out = new Uint8Array(s.length);
|
|
1073
|
+
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i) & 255;
|
|
1074
|
+
return out;
|
|
1075
|
+
}
|
|
1076
|
+
function splitEntity(bytes) {
|
|
1077
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1078
|
+
if (bytes[i] === LF) {
|
|
1079
|
+
if (bytes[i + 1] === LF) return [i, i + 2];
|
|
1080
|
+
if (bytes[i + 1] === CR && bytes[i + 2] === LF) return [i, i + 3];
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
return [bytes.length, bytes.length];
|
|
1084
|
+
}
|
|
1085
|
+
function parseHeaders(block) {
|
|
1086
|
+
const lines = block.split(/\r?\n/);
|
|
1087
|
+
const unfolded = [];
|
|
1088
|
+
for (const line of lines) {
|
|
1089
|
+
if (line === "") continue;
|
|
1090
|
+
if ((line.startsWith(" ") || line.startsWith(" ")) && unfolded.length > 0) {
|
|
1091
|
+
unfolded[unfolded.length - 1] += " " + line.trim();
|
|
1092
|
+
} else {
|
|
1093
|
+
unfolded.push(line);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
const headers = [];
|
|
1097
|
+
for (const line of unfolded) {
|
|
1098
|
+
const colon = line.indexOf(":");
|
|
1099
|
+
if (colon <= 0) continue;
|
|
1100
|
+
headers.push({ name: line.slice(0, colon).trim().toLowerCase(), value: decodeWords(line.slice(colon + 1).trim()) });
|
|
1101
|
+
}
|
|
1102
|
+
return headers;
|
|
1103
|
+
}
|
|
1104
|
+
function header(headers, name) {
|
|
1105
|
+
return headers.find((h) => h.name === name)?.value ?? null;
|
|
1106
|
+
}
|
|
1107
|
+
var ENCODED_WORD = /=\?([^?\s]+)\?([BbQq])\?([^?\s]*)\?=/g;
|
|
1108
|
+
function decodeWords(value) {
|
|
1109
|
+
const joined = value.replace(/(\?=)[ \t\r\n]+(=\?)/g, "$1$2");
|
|
1110
|
+
return joined.replace(ENCODED_WORD, (whole, charset, encoding, text) => {
|
|
1111
|
+
const bytes = encoding.toUpperCase() === "B" ? base64Bytes(text) : bytesOfLatin1(text.replace(/_/g, " ").replace(/=([0-9A-Fa-f]{2})/g, (_m, h) => String.fromCharCode(parseInt(h, 16))));
|
|
1112
|
+
const decoded = decodeCharset(bytes, charset);
|
|
1113
|
+
return decoded ?? whole;
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
function parseParams(value) {
|
|
1117
|
+
const [head, ...rest] = value.split(";");
|
|
1118
|
+
const params = {};
|
|
1119
|
+
const continued = {};
|
|
1120
|
+
for (const piece of rest) {
|
|
1121
|
+
const eq = piece.indexOf("=");
|
|
1122
|
+
if (eq < 0) continue;
|
|
1123
|
+
let key = piece.slice(0, eq).trim().toLowerCase();
|
|
1124
|
+
let raw = piece.slice(eq + 1).trim();
|
|
1125
|
+
if (raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2) raw = raw.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
1126
|
+
const segment = /^(.+?)\*(\d+)(\*)?$/.exec(key);
|
|
1127
|
+
if (segment) {
|
|
1128
|
+
const [, base, n, star] = segment;
|
|
1129
|
+
(continued[base] ??= [])[Number(n)] = star ? decode2231(raw, Number(n) === 0) : raw;
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
if (key.endsWith("*")) {
|
|
1133
|
+
key = key.slice(0, -1);
|
|
1134
|
+
params[key] = decode2231(raw, true);
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
params[key] ??= raw;
|
|
1138
|
+
}
|
|
1139
|
+
for (const [base, parts] of Object.entries(continued)) params[base] = parts.join("");
|
|
1140
|
+
return { main: (head ?? "").trim().toLowerCase(), params };
|
|
1141
|
+
}
|
|
1142
|
+
var pendingCharset2231 = "utf-8";
|
|
1143
|
+
function decode2231(raw, first) {
|
|
1144
|
+
let text = raw;
|
|
1145
|
+
if (first) {
|
|
1146
|
+
const m = /^([^']*)'[^']*'(.*)$/s.exec(raw);
|
|
1147
|
+
if (m) {
|
|
1148
|
+
pendingCharset2231 = m[1] || "utf-8";
|
|
1149
|
+
text = m[2];
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
const bytes = bytesOfLatin1(text.replace(/%([0-9A-Fa-f]{2})/g, (_m, h) => String.fromCharCode(parseInt(h, 16))));
|
|
1153
|
+
return decodeCharset(bytes, pendingCharset2231) ?? text;
|
|
1154
|
+
}
|
|
1155
|
+
function base64Bytes(text) {
|
|
1156
|
+
return new Uint8Array(Buffer.from(text.replace(/[^A-Za-z0-9+/=]/g, ""), "base64"));
|
|
1157
|
+
}
|
|
1158
|
+
function quotedPrintableBytes(bytes) {
|
|
1159
|
+
const out = [];
|
|
1160
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1161
|
+
const b = bytes[i];
|
|
1162
|
+
if (b !== 61) {
|
|
1163
|
+
out.push(b);
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
const a = bytes[i + 1];
|
|
1167
|
+
const c = bytes[i + 2];
|
|
1168
|
+
if (a === LF) {
|
|
1169
|
+
i += 1;
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
if (a === CR && c === LF) {
|
|
1173
|
+
i += 2;
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
if (a !== void 0 && c !== void 0 && isHex(a) && isHex(c)) {
|
|
1177
|
+
out.push(parseInt(String.fromCharCode(a, c), 16));
|
|
1178
|
+
i += 2;
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
out.push(b);
|
|
1182
|
+
}
|
|
1183
|
+
return new Uint8Array(out);
|
|
1184
|
+
}
|
|
1185
|
+
function isHex(b) {
|
|
1186
|
+
return b >= 48 && b <= 57 || b >= 65 && b <= 70 || b >= 97 && b <= 102;
|
|
1187
|
+
}
|
|
1188
|
+
function transferDecode(body, encoding) {
|
|
1189
|
+
switch ((encoding ?? "7bit").trim().toLowerCase()) {
|
|
1190
|
+
case "base64":
|
|
1191
|
+
return base64Bytes(latin1(body));
|
|
1192
|
+
case "quoted-printable":
|
|
1193
|
+
return quotedPrintableBytes(body);
|
|
1194
|
+
default:
|
|
1195
|
+
return body;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
function decodeCharset(bytes, charset) {
|
|
1199
|
+
switch (charset.trim().toLowerCase()) {
|
|
1200
|
+
case "utf-8":
|
|
1201
|
+
case "utf8":
|
|
1202
|
+
return new TextDecoder("utf-8").decode(bytes);
|
|
1203
|
+
case "us-ascii":
|
|
1204
|
+
case "ascii":
|
|
1205
|
+
case "iso-8859-1":
|
|
1206
|
+
case "latin1":
|
|
1207
|
+
return Buffer.from(bytes).toString("latin1");
|
|
1208
|
+
case "windows-1252":
|
|
1209
|
+
case "cp1252":
|
|
1210
|
+
return new TextDecoder("windows-1252").decode(bytes);
|
|
1211
|
+
case "iso-8859-15":
|
|
1212
|
+
case "latin9":
|
|
1213
|
+
return new TextDecoder("iso-8859-15").decode(bytes);
|
|
1214
|
+
default:
|
|
1215
|
+
return null;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
function walk(bytes, entityHeaders, acc) {
|
|
1219
|
+
const [headerEnd, bodyStart] = splitEntity(bytes);
|
|
1220
|
+
const headers = entityHeaders ?? parseHeaders(latin1(bytes.subarray(0, headerEnd)));
|
|
1221
|
+
const body = bytes.subarray(entityHeaders ? 0 : bodyStart);
|
|
1222
|
+
const type = parseParams(header(headers, "content-type") ?? "text/plain");
|
|
1223
|
+
if (type.main.startsWith("multipart/") && type.params.boundary) {
|
|
1224
|
+
for (const child of splitMultipart(body, type.params.boundary)) walk(child, null, acc);
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
const disposition = parseParams(header(headers, "content-disposition") ?? "");
|
|
1228
|
+
const decoded = transferDecode(body, header(headers, "content-transfer-encoding"));
|
|
1229
|
+
const declaredDisposition = disposition.main === "attachment" || disposition.main === "inline" ? disposition.main : null;
|
|
1230
|
+
const filename = disposition.params.filename ?? type.params.name ?? null;
|
|
1231
|
+
const contentId = header(headers, "content-id")?.replace(/^<|>$/g, "") ?? null;
|
|
1232
|
+
const isText = type.main.startsWith("text/");
|
|
1233
|
+
const isAttachment = declaredDisposition === "attachment" || !isText;
|
|
1234
|
+
const part = {
|
|
1235
|
+
index: acc.parts.length,
|
|
1236
|
+
contentType: type.main,
|
|
1237
|
+
charset: type.params.charset?.toLowerCase() ?? null,
|
|
1238
|
+
disposition: declaredDisposition,
|
|
1239
|
+
filename,
|
|
1240
|
+
contentId,
|
|
1241
|
+
size: decoded.length,
|
|
1242
|
+
headers
|
|
1243
|
+
};
|
|
1244
|
+
acc.parts.push(part);
|
|
1245
|
+
if (isAttachment) {
|
|
1246
|
+
acc.attachments.push({ ...part, content: Buffer.from(decoded).toString("base64") });
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
const text = decodeCharset(decoded, type.params.charset ?? "utf-8");
|
|
1250
|
+
if (text === null) return;
|
|
1251
|
+
if (type.main === "text/plain" && acc.text === null) acc.text = text;
|
|
1252
|
+
if (type.main === "text/html" && acc.html === null) acc.html = text;
|
|
1253
|
+
}
|
|
1254
|
+
function splitMultipart(body, boundary) {
|
|
1255
|
+
const delimiter = bytesOfLatin1(`--${boundary}`);
|
|
1256
|
+
const children = [];
|
|
1257
|
+
let partStart = -1;
|
|
1258
|
+
for (let i = 0; i <= body.length - delimiter.length; i++) {
|
|
1259
|
+
if (i !== 0 && body[i - 1] !== LF) continue;
|
|
1260
|
+
if (!startsWith(body, i, delimiter)) continue;
|
|
1261
|
+
let partEnd = i;
|
|
1262
|
+
if (partEnd > 0 && body[partEnd - 1] === LF) partEnd -= 1;
|
|
1263
|
+
if (partEnd > 0 && body[partEnd - 1] === CR) partEnd -= 1;
|
|
1264
|
+
if (partStart >= 0) children.push(body.subarray(partStart, partEnd));
|
|
1265
|
+
let j = i + delimiter.length;
|
|
1266
|
+
const closing = body[j] === 45 && body[j + 1] === 45;
|
|
1267
|
+
if (closing) return children;
|
|
1268
|
+
while (j < body.length && body[j] !== LF) j++;
|
|
1269
|
+
partStart = j + 1;
|
|
1270
|
+
i = j;
|
|
1271
|
+
}
|
|
1272
|
+
if (partStart >= 0 && partStart <= body.length) children.push(body.subarray(partStart));
|
|
1273
|
+
return children;
|
|
1274
|
+
}
|
|
1275
|
+
function startsWith(haystack, at, needle) {
|
|
1276
|
+
for (let k = 0; k < needle.length; k++) if (haystack[at + k] !== needle[k]) return false;
|
|
1277
|
+
return true;
|
|
1278
|
+
}
|
|
1279
|
+
function parseMime(raw) {
|
|
1280
|
+
const bytes = typeof raw === "string" ? new Uint8Array(Buffer.from(raw, "utf8")) : raw;
|
|
1281
|
+
const [headerEnd, bodyStart] = splitEntity(bytes);
|
|
1282
|
+
const headers = parseHeaders(latin1(bytes.subarray(0, headerEnd)));
|
|
1283
|
+
const body = bytes.subarray(bodyStart);
|
|
1284
|
+
const acc = { parts: [], attachments: [], text: null, html: null };
|
|
1285
|
+
const type = parseParams(header(headers, "content-type") ?? "text/plain");
|
|
1286
|
+
if (type.main.startsWith("multipart/") && type.params.boundary) {
|
|
1287
|
+
for (const child of splitMultipart(body, type.params.boundary)) walk(child, null, acc);
|
|
1288
|
+
} else {
|
|
1289
|
+
walk(body, headers.filter((h) => h.name.startsWith("content-")), acc);
|
|
1290
|
+
}
|
|
1291
|
+
return {
|
|
1292
|
+
headers,
|
|
1293
|
+
subject: header(headers, "subject"),
|
|
1294
|
+
messageId: header(headers, "message-id"),
|
|
1295
|
+
text: acc.text,
|
|
1296
|
+
html: acc.html,
|
|
1297
|
+
parts: acc.parts,
|
|
1298
|
+
attachments: acc.attachments
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1010
1302
|
// src/probe.ts
|
|
1011
1303
|
async function probe(spec) {
|
|
1012
1304
|
let response;
|
|
@@ -1266,7 +1558,7 @@ function constantTimeEquals(a, b) {
|
|
|
1266
1558
|
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
1267
1559
|
return diff === 0;
|
|
1268
1560
|
}
|
|
1269
|
-
function
|
|
1561
|
+
function header2(headers, name) {
|
|
1270
1562
|
const wanted = name.toLowerCase();
|
|
1271
1563
|
for (const [key, value] of Object.entries(headers)) {
|
|
1272
1564
|
if (key.toLowerCase() !== wanted) continue;
|
|
@@ -1284,9 +1576,9 @@ async function verifyDelivery(trigger, delivery, secret, now) {
|
|
|
1284
1576
|
reason: `${trigger.service}.${trigger.operation} declares no signature scheme, so a delivery cannot be trusted.`
|
|
1285
1577
|
};
|
|
1286
1578
|
}
|
|
1287
|
-
const rawHeader =
|
|
1579
|
+
const rawHeader = header2(delivery.headers, spec.signatureHeader);
|
|
1288
1580
|
const parsed = spec.parse && rawHeader ? spec.parse(rawHeader) : { signature: rawHeader };
|
|
1289
|
-
const timestamp = parsed.timestamp ?? (spec.timestampHeader ?
|
|
1581
|
+
const timestamp = parsed.timestamp ?? (spec.timestampHeader ? header2(delivery.headers, spec.timestampHeader) : void 0);
|
|
1290
1582
|
return verifyHmac({
|
|
1291
1583
|
raw: delivery.raw,
|
|
1292
1584
|
signature: parsed.signature,
|
|
@@ -1359,15 +1651,19 @@ export {
|
|
|
1359
1651
|
fakeValues,
|
|
1360
1652
|
getConnectionHost,
|
|
1361
1653
|
graphemes,
|
|
1362
|
-
header,
|
|
1654
|
+
header2 as header,
|
|
1363
1655
|
hmac,
|
|
1364
1656
|
httpFailure,
|
|
1365
1657
|
idempotencyKeyFor,
|
|
1366
1658
|
isEmptyPayload,
|
|
1367
1659
|
isRemote,
|
|
1368
1660
|
isUnconditionallyRetryable,
|
|
1661
|
+
leaseAction,
|
|
1662
|
+
leaseRenewAt,
|
|
1663
|
+
leaseState,
|
|
1369
1664
|
linkRanges,
|
|
1370
1665
|
measure,
|
|
1666
|
+
parseMime,
|
|
1371
1667
|
payloadHash,
|
|
1372
1668
|
postChain,
|
|
1373
1669
|
probe,
|
|
@@ -1392,6 +1688,7 @@ export {
|
|
|
1392
1688
|
shouldRetry,
|
|
1393
1689
|
sliceByteRange,
|
|
1394
1690
|
splitToFit,
|
|
1691
|
+
subscriptionLease,
|
|
1395
1692
|
triggerEvent,
|
|
1396
1693
|
unchecked,
|
|
1397
1694
|
utf8Length,
|