@cosmicdrift/kumiko-bundled-features 0.141.0 → 0.142.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/package.json +13 -7
- package/src/inbound-mail-foundation/__tests__/feature.test.ts +169 -0
- package/src/inbound-mail-foundation/__tests__/inbound-mail-foundation.integration.test.ts +437 -0
- package/src/inbound-mail-foundation/aggregate-id.ts +40 -0
- package/src/inbound-mail-foundation/connect-routes.ts +230 -0
- package/src/inbound-mail-foundation/constants.ts +80 -0
- package/src/inbound-mail-foundation/db/queries/inbound-projections.ts +38 -0
- package/src/inbound-mail-foundation/entities.ts +168 -0
- package/src/inbound-mail-foundation/events.ts +147 -0
- package/src/inbound-mail-foundation/feature.ts +185 -0
- package/src/inbound-mail-foundation/handlers/account-state.ts +23 -0
- package/src/inbound-mail-foundation/handlers/connect-account.write.ts +112 -0
- package/src/inbound-mail-foundation/handlers/disconnect-account.write.ts +69 -0
- package/src/inbound-mail-foundation/handlers/ingest-message.write.ts +279 -0
- package/src/inbound-mail-foundation/handlers/list-accounts.query.ts +44 -0
- package/src/inbound-mail-foundation/handlers/list-messages.query.ts +58 -0
- package/src/inbound-mail-foundation/handlers/scope-visibility.ts +14 -0
- package/src/inbound-mail-foundation/handlers/update-account.write.ts +78 -0
- package/src/inbound-mail-foundation/index.ts +97 -0
- package/src/inbound-mail-foundation/oauth-state.ts +103 -0
- package/src/inbound-mail-foundation/projection.ts +152 -0
- package/src/inbound-mail-foundation/provider-factory.ts +53 -0
- package/src/inbound-mail-foundation/tenant-destroy-hook.ts +93 -0
- package/src/inbound-mail-foundation/types.ts +250 -0
- package/src/inbound-mail-foundation/watch-supervisor.ts +482 -0
- package/src/inbound-provider-imap/__tests__/feature.test.ts +115 -0
- package/src/inbound-provider-imap/__tests__/imap-live.integration.test.ts +194 -0
- package/src/inbound-provider-imap/credential-document.ts +51 -0
- package/src/inbound-provider-imap/feature.ts +287 -0
- package/src/inbound-provider-imap/imap-client.ts +158 -0
- package/src/inbound-provider-imap/index.ts +17 -0
- package/src/inbound-provider-inmemory/feature.ts +154 -0
- package/src/inbound-provider-inmemory/index.ts +12 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Tenant-destroy hooks — räumen beim Tenant-Destroy alle Inbound-Mail-
|
|
2
|
+
// Spuren des Tenants ab. Ein Hook PRO tenant-subject-Entity (der
|
|
3
|
+
// gdpr-storage-Boot-Validator prüft die EXT_TENANT_DATA-Registrierung
|
|
4
|
+
// entity-genau), jeweils:
|
|
5
|
+
//
|
|
6
|
+
// 1. Streams archivieren — ein künftiger Projection-Rebuild kann
|
|
7
|
+
// Gelöschtes nicht resurrecten (Muster billing-foundation #800).
|
|
8
|
+
// Message-Streams sind viele → Loop über die Projection-Rows
|
|
9
|
+
// (Row-PK = Stream-ID); tenant-destroy ist ein seltener
|
|
10
|
+
// Batch-Vorgang, O(n) ist ok.
|
|
11
|
+
// 2. Projection-Rows löschen.
|
|
12
|
+
// 3. Nur mail-account: unmanaged Sync-State (Cursors, Seen-Anchors)
|
|
13
|
+
// löschen — gekeyt per accountId, deshalb aus den Account-Rows
|
|
14
|
+
// VOR dem Delete eingesammelt.
|
|
15
|
+
//
|
|
16
|
+
// Die eigentliche PII-Erasure macht crypto-shredding: eraseSubjectKeys
|
|
17
|
+
// löscht den Tenant-Subject-Key, damit werden Event-Log-Payloads UND
|
|
18
|
+
// etwaige Ciphertext-Kopien unlesbar. Diese Hooks entsorgen die Rows.
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
type DbRunner,
|
|
22
|
+
deleteMany,
|
|
23
|
+
type EntityTableMeta,
|
|
24
|
+
selectMany,
|
|
25
|
+
} from "@cosmicdrift/kumiko-framework/db";
|
|
26
|
+
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
27
|
+
import { archiveStream } from "@cosmicdrift/kumiko-framework/event-store";
|
|
28
|
+
import { seenMessageTable, syncCursorTable } from "./entities";
|
|
29
|
+
import {
|
|
30
|
+
INBOUND_MESSAGE_AGGREGATE_TYPE,
|
|
31
|
+
MAIL_ACCOUNT_AGGREGATE_TYPE,
|
|
32
|
+
MAIL_THREAD_AGGREGATE_TYPE,
|
|
33
|
+
} from "./events";
|
|
34
|
+
import {
|
|
35
|
+
inboundMessagesProjectionTable,
|
|
36
|
+
mailAccountsProjectionTable,
|
|
37
|
+
mailThreadsProjectionTable,
|
|
38
|
+
} from "./projection";
|
|
39
|
+
|
|
40
|
+
const ARCHIVED_BY = "tenant-lifecycle:destroy";
|
|
41
|
+
const REASON = "tenant_destroy";
|
|
42
|
+
|
|
43
|
+
type DestroyCtx = {
|
|
44
|
+
readonly db: DbRunner;
|
|
45
|
+
readonly tenantId: TenantId;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
async function archiveAndDeleteRows(
|
|
49
|
+
ctx: DestroyCtx,
|
|
50
|
+
table: EntityTableMeta,
|
|
51
|
+
aggregateType: string,
|
|
52
|
+
): Promise<readonly string[]> {
|
|
53
|
+
const rows = await selectMany<{ id: string }>(ctx.db, table, { tenantId: ctx.tenantId });
|
|
54
|
+
for (const row of rows) {
|
|
55
|
+
await archiveStream(ctx.db, {
|
|
56
|
+
tenantId: ctx.tenantId,
|
|
57
|
+
aggregateId: row.id,
|
|
58
|
+
aggregateType,
|
|
59
|
+
archivedBy: ARCHIVED_BY,
|
|
60
|
+
reason: REASON,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
await deleteMany(ctx.db, table, { tenantId: ctx.tenantId });
|
|
64
|
+
return rows.map((row) => row.id);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function mailAccountTenantDestroyHook(ctx: DestroyCtx): Promise<void> {
|
|
68
|
+
const accountIds = await archiveAndDeleteRows(
|
|
69
|
+
ctx,
|
|
70
|
+
mailAccountsProjectionTable as EntityTableMeta,
|
|
71
|
+
MAIL_ACCOUNT_AGGREGATE_TYPE,
|
|
72
|
+
);
|
|
73
|
+
for (const accountId of accountIds) {
|
|
74
|
+
await deleteMany(ctx.db, syncCursorTable as EntityTableMeta, { accountId });
|
|
75
|
+
await deleteMany(ctx.db, seenMessageTable as EntityTableMeta, { accountId });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function inboundMessageTenantDestroyHook(ctx: DestroyCtx): Promise<void> {
|
|
80
|
+
await archiveAndDeleteRows(
|
|
81
|
+
ctx,
|
|
82
|
+
inboundMessagesProjectionTable as EntityTableMeta,
|
|
83
|
+
INBOUND_MESSAGE_AGGREGATE_TYPE,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function mailThreadTenantDestroyHook(ctx: DestroyCtx): Promise<void> {
|
|
88
|
+
await archiveAndDeleteRows(
|
|
89
|
+
ctx,
|
|
90
|
+
mailThreadsProjectionTable as EntityTableMeta,
|
|
91
|
+
MAIL_THREAD_AGGREGATE_TYPE,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// Provider-Contract für inbound-mail-Provider-Plugins (Plan §2).
|
|
2
|
+
//
|
|
3
|
+
// Gespiegelt von mail-foundation's MailTransportContext/-Plugin —
|
|
4
|
+
// gleiche Trennung Foundation ↔ Provider, gleicher Slim-Context.
|
|
5
|
+
//
|
|
6
|
+
// **Warum kein voller HandlerContext?** fetch()/watch() laufen im
|
|
7
|
+
// Worker-Pfad (Poll-Cron, IDLE-Supervisor) — dort gibt es keinen
|
|
8
|
+
// per-request `tx`/`actor`. Ein Plugin das mehr liest würde den
|
|
9
|
+
// Worker-Pfad zur Runtime brechen, und das fiele nur in production
|
|
10
|
+
// auf. Plugin das mehr braucht: InboundMailContext explizit erweitern
|
|
11
|
+
// (sichtbarer breaking change) statt ctx-cast.
|
|
12
|
+
|
|
13
|
+
import type { ConfigAccessor, Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
14
|
+
import type { InboundMailAccountStatus } from "./constants";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Slim-Context für Provider-Plugins.
|
|
18
|
+
*
|
|
19
|
+
* **Felder:**
|
|
20
|
+
* config — tenant-config-reads (host/port/... der Plugins)
|
|
21
|
+
* registry — extension-Lookup in der Factory (nicht plugin-intern)
|
|
22
|
+
* secrets — tenant-secret-reads (IMAP-Passwort, OAuth-Refresh-Token)
|
|
23
|
+
* _userId — Audit-Identity für secret-reads. Handler-Pfad: dispatcher
|
|
24
|
+
* setzt Caller-User-ID; Worker-Pfad: System-Identity.
|
|
25
|
+
*/
|
|
26
|
+
export type InboundMailContext = {
|
|
27
|
+
readonly config?: ConfigAccessor;
|
|
28
|
+
readonly registry?: Registry;
|
|
29
|
+
readonly secrets?: import("@cosmicdrift/kumiko-framework/secrets").SecretsContext;
|
|
30
|
+
readonly _userId?: string | undefined;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Plaintext-Sicht auf ein verbundenes Postfach, wie Provider sie sehen.
|
|
35
|
+
* Die Foundation decryptet `address` VOR dem Provider-Call — Provider
|
|
36
|
+
* arbeiten nie mit `kumiko-pii:`-Ciphertext (Guard analog
|
|
37
|
+
* withPiiCiphertextGuard im Transport-Pfad).
|
|
38
|
+
*/
|
|
39
|
+
export type MailAccountRecord = {
|
|
40
|
+
/** = aggregateId des mail-account-Streams; zugleich Secret-Slot-Key. */
|
|
41
|
+
readonly id: string;
|
|
42
|
+
readonly tenantId: string;
|
|
43
|
+
/** Provider-Key wie bei der Extension registriert ("imap", "m365-graph",
|
|
44
|
+
* "gmail-rest"). */
|
|
45
|
+
readonly provider: string;
|
|
46
|
+
/** "password" | "xoauth2" | "oauth" — imap unterscheidet hierüber
|
|
47
|
+
* Formular-Connect vs. XOAUTH2-Fallback (Plan §2). */
|
|
48
|
+
readonly authMethod: string;
|
|
49
|
+
/** null = tenant-geteiltes Postfach, sonst persönliches Postfach
|
|
50
|
+
* dieses Users (Sichtbarkeits-Scope, Plan Entscheidung 2). */
|
|
51
|
+
readonly ownerUserId: string | null;
|
|
52
|
+
/** Postfach-Adresse, Plaintext (decrypted). */
|
|
53
|
+
readonly address: string;
|
|
54
|
+
readonly displayName: string;
|
|
55
|
+
readonly status: InboundMailAccountStatus;
|
|
56
|
+
readonly watchState: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Normalisierte eingehende Mail — Provider-Output, Ingest-Input.
|
|
61
|
+
* Provider-raw-Strukturen (IMAP-Flags, Graph-Objekte) bleiben im
|
|
62
|
+
* Provider; hier nur der domain-clean Schnitt.
|
|
63
|
+
*/
|
|
64
|
+
export type RawInboundMessage = {
|
|
65
|
+
/** Provider-native Message-ID (IMAP UID, Graph id, Gmail id) —
|
|
66
|
+
* Idempotency-Anchor zusammen mit providerName. */
|
|
67
|
+
readonly providerMessageId: string;
|
|
68
|
+
/** RFC-5322 Message-ID-Header ohne <>; null wenn der Header fehlt
|
|
69
|
+
* (Foundation fällt auf deterministischen Ersatz zurück). */
|
|
70
|
+
readonly messageIdHeader: string | null;
|
|
71
|
+
/** Provider-native Thread-/Conversation-ID falls vorhanden (Graph
|
|
72
|
+
* conversationId, Gmail threadId); Foundation bevorzugt sie vor der
|
|
73
|
+
* References-Chain beim threadKey-Bau. */
|
|
74
|
+
readonly providerThreadId: string | null;
|
|
75
|
+
/** References/In-Reply-To-Chain (Message-IDs ohne <>), älteste zuerst. */
|
|
76
|
+
readonly references: readonly string[];
|
|
77
|
+
readonly from: string;
|
|
78
|
+
readonly to: readonly string[];
|
|
79
|
+
readonly cc: readonly string[];
|
|
80
|
+
readonly subject: string;
|
|
81
|
+
/** Plaintext-Vorschau, provider-generiert oder aus dem Body geschnitten. */
|
|
82
|
+
readonly snippet: string;
|
|
83
|
+
/** ISO-Instant — INTERNALDATE bzw. receivedDateTime, nicht Ingest-Zeit. */
|
|
84
|
+
readonly receivedAtIso: string;
|
|
85
|
+
/** Raw MIME; null im snippet-only-Mode. Foundation persisted ihn nach
|
|
86
|
+
* file-foundation und schreibt bodyRef — der Event-Store bleibt
|
|
87
|
+
* blob-frei. */
|
|
88
|
+
readonly rawMime: Uint8Array | null;
|
|
89
|
+
/** Generischer Scope-Hint ("inbox", Folder-Name) — keine App-Semantik. */
|
|
90
|
+
readonly scope: string;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** Provider-opaker Cursor: IMAP {uidValidity,lastUid} · Graph {deltaLink}
|
|
94
|
+
* · Gmail {historyId}. Foundation persisted ihn JSON-stringified in
|
|
95
|
+
* read_mail_sync_cursors, interpretiert ihn nie. */
|
|
96
|
+
export type SyncCursorPayload = Readonly<Record<string, unknown>>;
|
|
97
|
+
|
|
98
|
+
export type InboundFetchResult = {
|
|
99
|
+
readonly messages: readonly RawInboundMessage[];
|
|
100
|
+
readonly nextCursor: SyncCursorPayload;
|
|
101
|
+
/** true ⇒ Foundation ruft fetch im selben Poll-Tick erneut auf
|
|
102
|
+
* (Pagination), bis false oder maxMessages-Budget erschöpft. */
|
|
103
|
+
readonly hasMore: boolean;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export type OAuthTokenSet = {
|
|
107
|
+
readonly accessToken: string;
|
|
108
|
+
/** ISO-Instant. TTL typisch ~1h ⇒ Foundation refresht before-poll. */
|
|
109
|
+
readonly expiresAt: string;
|
|
110
|
+
readonly refreshToken?: string;
|
|
111
|
+
readonly scopesGranted: readonly string[];
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* OAuth-Teil des Contracts — nur OAuth-Provider; imap (Passwort-Modus)
|
|
116
|
+
* lässt `oauth` weg. Die Routen (`/inbound-mail/connect`,
|
|
117
|
+
* `/inbound-mail/oauth/callback`) stellt die Foundation generisch —
|
|
118
|
+
* Provider registrieren KEINE eigenen Routen.
|
|
119
|
+
*/
|
|
120
|
+
export type InboundOAuthFlow = {
|
|
121
|
+
/** Scopes: Empfang UND Send in EINEM Consent — Re-Consent ist teuer. */
|
|
122
|
+
readonly scopes: {
|
|
123
|
+
readonly receive: readonly string[];
|
|
124
|
+
readonly send?: readonly string[];
|
|
125
|
+
};
|
|
126
|
+
readonly buildAuthorizeUrl: (
|
|
127
|
+
ctx: InboundMailContext,
|
|
128
|
+
p: { readonly state: string; readonly redirectUri: string },
|
|
129
|
+
) => Promise<string>;
|
|
130
|
+
readonly exchangeCode: (
|
|
131
|
+
ctx: InboundMailContext,
|
|
132
|
+
p: { readonly code: string; readonly redirectUri: string },
|
|
133
|
+
) => Promise<OAuthTokenSet>;
|
|
134
|
+
readonly refreshAccessToken: (
|
|
135
|
+
ctx: InboundMailContext,
|
|
136
|
+
account: MailAccountRecord,
|
|
137
|
+
refreshToken: string,
|
|
138
|
+
) => Promise<OAuthTokenSet>;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Inbound-Mail-Provider-Plugin contract. Jedes Provider-Feature
|
|
143
|
+
* (inbound-provider-imap, -m365-graph, -gmail-rest) registriert eine
|
|
144
|
+
* Implementation via `r.useExtension(INBOUND_MAIL_PROVIDER_EXTENSION,
|
|
145
|
+
* "<key>", plugin)`.
|
|
146
|
+
*/
|
|
147
|
+
export type InboundMailProviderPlugin = {
|
|
148
|
+
/** Credentials-/Erreichbarkeits-Check beim Connect-Flow — wirft
|
|
149
|
+
* typisiert (InboundAuthError etc.), kein Rückgabewert. */
|
|
150
|
+
readonly verify: (ctx: InboundMailContext, account: MailAccountRecord) => Promise<void>;
|
|
151
|
+
/** Pull seit cursor. `cursor === null` ⇒ Erst-Befüllung, begrenzt
|
|
152
|
+
* durch backfillWindowDays. */
|
|
153
|
+
readonly fetch: (
|
|
154
|
+
ctx: InboundMailContext,
|
|
155
|
+
account: MailAccountRecord,
|
|
156
|
+
cursor: SyncCursorPayload | null,
|
|
157
|
+
opts: { readonly backfillWindowDays: number; readonly maxMessages: number },
|
|
158
|
+
) => Promise<InboundFetchResult>;
|
|
159
|
+
/** Nur OAuth-Provider. */
|
|
160
|
+
readonly oauth?: InboundOAuthFlow;
|
|
161
|
+
/** Optional: Live-Push (IMAP IDLE, später Graph/Gmail-Webhooks).
|
|
162
|
+
* Liefert stop(). Poll bleibt Reconciliation — watch ist Latenz-
|
|
163
|
+
* Optimierung, nie Korrektheits-Anker. `onError` meldet den Tod der
|
|
164
|
+
* Live-Verbindung (Drop, Auth-Revoke) an den Supervisor, der mit
|
|
165
|
+
* Backoff neu startet — der Provider reconnected NICHT selbst. */
|
|
166
|
+
readonly watch?: (
|
|
167
|
+
ctx: InboundMailContext,
|
|
168
|
+
account: MailAccountRecord,
|
|
169
|
+
handlers: {
|
|
170
|
+
readonly onMessages: (msgs: readonly RawInboundMessage[]) => Promise<void>;
|
|
171
|
+
readonly onError: (err: unknown) => void;
|
|
172
|
+
},
|
|
173
|
+
) => Promise<() => Promise<void>>;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// extension-usage `options` ist engine-payload (unknown) — strukturell
|
|
177
|
+
// validieren statt blind casten (Muster isMailTransportPlugin).
|
|
178
|
+
export function isInboundMailProviderPlugin(o: unknown): o is InboundMailProviderPlugin {
|
|
179
|
+
return (
|
|
180
|
+
typeof o === "object" &&
|
|
181
|
+
o !== null &&
|
|
182
|
+
"verify" in o &&
|
|
183
|
+
typeof (o as { verify: unknown }).verify === "function" &&
|
|
184
|
+
"fetch" in o &&
|
|
185
|
+
typeof (o as { fetch: unknown }).fetch === "function"
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// =============================================================================
|
|
190
|
+
// Typisierte Fehlerklassen — Foundation reagiert einheitlich im Poll
|
|
191
|
+
// (Plan §2, Fehler-Tabelle):
|
|
192
|
+
// InboundAuthError → status=auth_error, kein Retry
|
|
193
|
+
// InboundRateLimitError → Poll-Abbruch, nächster Cron-Tick
|
|
194
|
+
// InboundCursorInvalidError → Cursor-Reset + Voll-Resync im Backfill-
|
|
195
|
+
// Fenster; Dedup fängt Dubletten
|
|
196
|
+
// InboundTransientError → Job-Retry (retries: 3, exponential)
|
|
197
|
+
// =============================================================================
|
|
198
|
+
//
|
|
199
|
+
// `kind`-Discriminant statt instanceof: Plugin und Foundation können in
|
|
200
|
+
// getrennten Bundles leben (dual-package-hazard), instanceof über
|
|
201
|
+
// Realm-/Kopie-Grenzen ist unzuverlässig.
|
|
202
|
+
|
|
203
|
+
export class InboundAuthError extends Error {
|
|
204
|
+
readonly kind = "inbound-auth-error" as const;
|
|
205
|
+
constructor(message: string) {
|
|
206
|
+
super(message);
|
|
207
|
+
this.name = "InboundAuthError";
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export class InboundRateLimitError extends Error {
|
|
212
|
+
readonly kind = "inbound-rate-limit-error" as const;
|
|
213
|
+
readonly retryAfterMs: number;
|
|
214
|
+
constructor(message: string, retryAfterMs: number) {
|
|
215
|
+
super(message);
|
|
216
|
+
this.name = "InboundRateLimitError";
|
|
217
|
+
this.retryAfterMs = retryAfterMs;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export class InboundCursorInvalidError extends Error {
|
|
222
|
+
readonly kind = "inbound-cursor-invalid-error" as const;
|
|
223
|
+
constructor(message: string) {
|
|
224
|
+
super(message);
|
|
225
|
+
this.name = "InboundCursorInvalidError";
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export class InboundTransientError extends Error {
|
|
230
|
+
readonly kind = "inbound-transient-error" as const;
|
|
231
|
+
constructor(message: string) {
|
|
232
|
+
super(message);
|
|
233
|
+
this.name = "InboundTransientError";
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
type KindCarrier = { readonly kind?: unknown };
|
|
238
|
+
|
|
239
|
+
export function isInboundAuthError(e: unknown): e is InboundAuthError {
|
|
240
|
+
return e instanceof Error && (e as KindCarrier).kind === "inbound-auth-error";
|
|
241
|
+
}
|
|
242
|
+
export function isInboundRateLimitError(e: unknown): e is InboundRateLimitError {
|
|
243
|
+
return e instanceof Error && (e as KindCarrier).kind === "inbound-rate-limit-error";
|
|
244
|
+
}
|
|
245
|
+
export function isInboundCursorInvalidError(e: unknown): e is InboundCursorInvalidError {
|
|
246
|
+
return e instanceof Error && (e as KindCarrier).kind === "inbound-cursor-invalid-error";
|
|
247
|
+
}
|
|
248
|
+
export function isInboundTransientError(e: unknown): e is InboundTransientError {
|
|
249
|
+
return e instanceof Error && (e as KindCarrier).kind === "inbound-transient-error";
|
|
250
|
+
}
|