@cosmicdrift/kumiko-bundled-features 0.337.1 → 0.339.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/dist/agent-tools/tool-catalog.js +37 -17
- package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +3 -2
- package/dist/auth-email-password/handlers/invite-accept.write.js +3 -4
- package/dist/auth-email-password/handlers/invite-info.query.js +4 -1
- package/dist/auth-email-password/handlers/invite-signup-complete.write.js +3 -4
- package/dist/auth-email-password/handlers/signup-confirm.write.js +2 -1
- package/dist/auth-email-password/handlers/signup-request.write.js +2 -2
- package/dist/auth-email-password/web/auth-gate.d.ts +12 -2
- package/dist/auth-email-password/web/auth-gate.js +25 -1
- package/dist/auth-email-password/web/auth-redirect.d.ts +5 -0
- package/dist/auth-email-password/web/auth-redirect.js +58 -0
- package/dist/auth-email-password/web/client-plugin.d.ts +8 -1
- package/dist/auth-email-password/web/client-plugin.js +2 -0
- package/dist/auth-email-password/web/index.d.ts +2 -1
- package/dist/auth-email-password/web/index.js +1 -0
- package/dist/auth-email-password/web/session.d.ts +3 -1
- package/dist/auth-email-password/web/session.js +8 -2
- package/dist/auth-mfa/feature.js +1 -1
- package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +1 -1
- package/dist/auth-mfa/handlers/enable-start-preauth.write.js +2 -1
- package/dist/auth-mfa/handlers/verify.write.js +1 -1
- package/dist/auth-mfa/mfa-code-verifier.js +1 -1
- package/dist/auth-mfa/mfa-status-checker.js +1 -1
- package/dist/billing-foundation/aggregate-id.d.ts +2 -0
- package/dist/billing-foundation/aggregate-id.js +8 -0
- package/dist/billing-foundation/checkout-core.d.ts +3 -0
- package/dist/billing-foundation/checkout-core.js +3 -0
- package/dist/billing-foundation/constants.d.ts +38 -0
- package/dist/billing-foundation/constants.js +43 -0
- package/dist/billing-foundation/consumer-protection/confirmation-mail.d.ts +21 -0
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +125 -0
- package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +32 -0
- package/dist/billing-foundation/consumer-protection/consent-gate.js +96 -0
- package/dist/billing-foundation/consumer-protection/consent-locale.d.ts +5 -0
- package/dist/billing-foundation/consumer-protection/consent-locale.js +17 -0
- package/dist/billing-foundation/consumer-protection/consent-text.d.ts +11 -0
- package/dist/billing-foundation/consumer-protection/consent-text.js +28 -0
- package/dist/billing-foundation/consumer-protection/issue-confirmation.d.ts +15 -0
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +149 -0
- package/dist/billing-foundation/consumer-protection/register-consent.d.ts +3 -0
- package/dist/billing-foundation/consumer-protection/register-consent.js +63 -0
- package/dist/billing-foundation/consumer-protection/register-termination.d.ts +3 -0
- package/dist/billing-foundation/consumer-protection/register-termination.js +40 -0
- package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +45 -0
- package/dist/billing-foundation/consumer-protection/termination-mail.js +76 -0
- package/dist/billing-foundation/consumer-protection/termination-notify.d.ts +18 -0
- package/dist/billing-foundation/consumer-protection/termination-notify.js +21 -0
- package/dist/billing-foundation/consumer-protection/termination-pages.d.ts +8 -0
- package/dist/billing-foundation/consumer-protection/termination-pages.js +268 -0
- package/dist/billing-foundation/consumer-protection/termination-record.d.ts +73 -0
- package/dist/billing-foundation/consumer-protection/termination-record.js +230 -0
- package/dist/billing-foundation/consumer-protection/termination-request.d.ts +45 -0
- package/dist/billing-foundation/consumer-protection/termination-request.js +231 -0
- package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +62 -0
- package/dist/billing-foundation/consumer-protection/termination-texts.js +168 -0
- package/dist/billing-foundation/events.d.ts +105 -2
- package/dist/billing-foundation/events.js +88 -2
- package/dist/billing-foundation/feature.js +6 -2
- package/dist/billing-foundation/handlers/billing-plans.query.js +19 -1
- package/dist/billing-foundation/handlers/create-checkout-session.write.js +33 -2
- package/dist/billing-foundation/handlers/process-event.write.d.ts +2 -2
- package/dist/billing-foundation/handlers/process-event.write.js +43 -15
- package/dist/billing-foundation/handlers/process-payment-event.write.d.ts +2 -1
- package/dist/billing-foundation/handlers/process-payment-event.write.js +6 -2
- package/dist/billing-foundation/handlers/start-plan-checkout.write.js +17 -1
- package/dist/billing-foundation/handlers/sync-subscription.write.js +0 -1
- package/dist/billing-foundation/i18n.js +61 -0
- package/dist/billing-foundation/index.d.ts +3 -2
- package/dist/billing-foundation/index.js +2 -1
- package/dist/billing-foundation/tenant-destroy-hook.js +4 -4
- package/dist/billing-foundation/types.d.ts +54 -9
- package/dist/billing-foundation/validate-options.js +51 -10
- package/dist/billing-foundation/web/billing-plans-panel.js +40 -3
- package/dist/billing-foundation/web/cancel-contract-dialog.d.ts +7 -0
- package/dist/billing-foundation/web/cancel-contract-dialog.js +57 -0
- package/dist/billing-foundation/web/checkout-consent-dialog.d.ts +28 -0
- package/dist/billing-foundation/web/checkout-consent-dialog.js +38 -0
- package/dist/billing-foundation/webhook-handler.js +5 -2
- package/dist/cap-counter/with-cap-enforcement.js +7 -3
- package/dist/cap-overview/access.d.ts +2 -1
- package/dist/cap-overview/access.js +26 -5
- package/dist/cap-overview/feature.d.ts +4 -0
- package/dist/cap-overview/feature.js +8 -6
- package/dist/cap-overview/handlers/caps-usage.query.d.ts +1 -1
- package/dist/cap-overview/handlers/caps-usage.query.js +6 -4
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +12 -7
- package/dist/cap-overview/index.d.ts +2 -2
- package/dist/cap-overview/index.js +2 -2
- package/dist/cap-overview/screens.d.ts +1 -1
- package/dist/cap-overview/screens.js +17 -16
- package/dist/channel-discord/discord-channel.d.ts +6 -0
- package/dist/channel-discord/discord-channel.js +19 -0
- package/dist/channel-discord/feature.d.ts +3 -0
- package/dist/channel-discord/feature.js +30 -0
- package/dist/channel-discord/index.d.ts +2 -0
- package/dist/channel-discord/index.js +2 -0
- package/dist/channel-slack/feature.d.ts +3 -0
- package/dist/channel-slack/feature.js +30 -0
- package/dist/channel-slack/index.d.ts +2 -0
- package/dist/channel-slack/index.js +2 -0
- package/dist/channel-slack/slack-channel.d.ts +5 -0
- package/dist/channel-slack/slack-channel.js +16 -0
- package/dist/channel-teams/feature.d.ts +3 -0
- package/dist/channel-teams/feature.js +34 -0
- package/dist/channel-teams/index.d.ts +2 -0
- package/dist/channel-teams/index.js +2 -0
- package/dist/channel-teams/teams-channel.d.ts +5 -0
- package/dist/channel-teams/teams-channel.js +38 -0
- package/dist/channel-telegram/feature.d.ts +3 -0
- package/dist/channel-telegram/feature.js +28 -0
- package/dist/channel-telegram/index.d.ts +2 -0
- package/dist/channel-telegram/index.js +2 -0
- package/dist/channel-telegram/telegram-channel.d.ts +8 -0
- package/dist/channel-telegram/telegram-channel.js +54 -0
- package/dist/config/handlers/set.write.js +18 -2
- package/dist/crypto-shredding/feature.js +3 -2
- package/dist/crypto-shredding/handlers/forget-subject.write.d.ts +1 -1
- package/dist/crypto-shredding/handlers/forget-subject.write.js +30 -19
- package/dist/custom-fields/feature.js +9 -6
- package/dist/custom-fields/handlers/clear-custom-field.write.js +2 -1
- package/dist/custom-fields/handlers/define-tenant-field.write.js +2 -1
- package/dist/custom-fields/handlers/set-custom-field.write.js +2 -1
- package/dist/custom-fields/wire-user-data-rights.js +1 -1
- package/dist/data-retention/feature.js +1 -1
- package/dist/delivery/channel-context.d.ts +2 -1
- package/dist/delivery/channel-context.js +2 -2
- package/dist/delivery/chat-webhook-channel.d.ts +19 -0
- package/dist/delivery/chat-webhook-channel.js +48 -0
- package/dist/delivery/chat-webhook-sender.d.ts +21 -0
- package/dist/delivery/chat-webhook-sender.js +102 -0
- package/dist/delivery/delivery-service.js +12 -7
- package/dist/delivery/index.d.ts +2 -0
- package/dist/delivery/index.js +2 -0
- package/dist/delivery/jobs.js +1 -1
- package/dist/delivery/testing.d.ts +2 -0
- package/dist/delivery/types.d.ts +3 -1
- package/dist/delivery/types.js +1 -2
- package/dist/document-ingest-foundation/feature.js +3 -2
- package/dist/file-derivatives/handlers/public-variant-by-file-ref.query.js +1 -1
- package/dist/files-tenant-data/index.js +1 -1
- package/dist/form-draft/feature.js +1 -1
- package/dist/form-draft/handlers/discard.write.js +2 -1
- package/dist/form-draft/handlers/save.write.js +2 -1
- package/dist/inbound-mail-foundation/connect-routes.d.ts +7 -0
- package/dist/inbound-mail-foundation/connect-routes.js +13 -8
- package/dist/inbound-mail-foundation/feature.js +1 -1
- package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +2 -1
- package/dist/inbound-mail-foundation/index.d.ts +1 -0
- package/dist/inbound-mail-foundation/index.js +1 -0
- package/dist/inbound-mail-foundation/oauth-access-token.d.ts +28 -0
- package/dist/inbound-mail-foundation/oauth-access-token.js +131 -0
- package/dist/inbound-mail-foundation/tenant-destroy-hook.js +1 -1
- package/dist/inbound-mail-foundation/types.d.ts +18 -1
- package/dist/inbound-mail-foundation/watch-supervisor.d.ts +5 -0
- package/dist/inbound-mail-foundation/watch-supervisor.js +42 -14
- package/dist/managed-pages/handlers/set.write.js +2 -1
- package/dist/notes-history/executor.d.ts +2 -0
- package/dist/notes-history/executor.js +9 -1
- package/dist/notes-history/feature.d.ts +2 -3
- package/dist/notes-history/feature.js +2 -1
- package/dist/notes-history/handlers/add-note.write.d.ts +2 -1
- package/dist/notes-history/handlers/add-note.write.js +21 -4
- package/dist/notes-history-user-data/hooks.js +26 -8
- package/dist/renderer-simple/feature.d.ts +8 -2
- package/dist/renderer-simple/feature.js +8 -5
- package/dist/renderer-simple/index.d.ts +2 -2
- package/dist/renderer-simple/index.js +1 -1
- package/dist/renderer-simple/simple-renderer.d.ts +15 -0
- package/dist/renderer-simple/simple-renderer.js +74 -27
- package/dist/secrets/feature.js +1 -1
- package/dist/sessions/feature.js +1 -1
- package/dist/sessions/handlers/revoke-all-for-user.write.js +2 -1
- package/dist/subscription-mollie/plugin-methods.js +1 -0
- package/dist/subscription-mollie/verify-webhook.js +5 -3
- package/dist/subscription-stripe/plugin-methods.d.ts +3 -1
- package/dist/subscription-stripe/plugin-methods.js +78 -13
- package/dist/subscription-stripe/verify-webhook.js +7 -3
- package/dist/tags/executor.d.ts +2 -0
- package/dist/tags/executor.js +9 -1
- package/dist/tags/feature.d.ts +4 -3
- package/dist/tags/feature.js +4 -2
- package/dist/tags/handlers/assign-tag.write.d.ts +2 -1
- package/dist/tags/handlers/assign-tag.write.js +4 -4
- package/dist/tags/handlers/remove-tag.write.d.ts +2 -1
- package/dist/tags/handlers/remove-tag.write.js +3 -3
- package/dist/tenant-handover/handlers/claim.write.js +2 -2
- package/dist/tenant-lifecycle/feature.js +1 -1
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +4 -1
- package/dist/tenant-lifecycle/handlers/request-destruction.write.js +2 -1
- package/dist/tier-engine/feature.js +1 -1
- package/dist/tier-engine/handlers/get-tenant-tier.query.js +2 -1
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +2 -1
- package/dist/user/handlers/detail.query.js +1 -0
- package/dist/user/handlers/list.query.js +1 -0
- package/dist/user-data-rights/feature.js +2 -2
- package/dist/user-data-rights/handlers/cancel-deletion.write.js +2 -1
- package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +1 -1
- package/dist/user-data-rights/handlers/download-attempt-list.query.js +1 -0
- package/dist/user-data-rights/handlers/download-by-job.query.js +2 -1
- package/dist/user-data-rights/handlers/download-by-token.query.js +2 -1
- package/dist/user-data-rights/handlers/export-status.query.js +2 -1
- package/dist/user-data-rights/handlers/lift-restriction.write.js +3 -4
- package/dist/user-data-rights/handlers/my-audit-log.query.js +2 -1
- package/dist/user-data-rights/handlers/request-deletion-by-email.write.js +2 -1
- package/dist/user-data-rights/handlers/request-deletion.write.js +1 -1
- package/dist/user-data-rights/handlers/request-export.write.js +2 -1
- package/dist/user-data-rights/handlers/restrict-account.write.js +2 -2
- package/dist/user-data-rights/handlers/run-forget-cleanup.write.js +2 -1
- package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
- package/dist/user-data-rights/run-forget-cleanup.js +30 -15
- package/dist/workflow-runner/event-subscriber.js +6 -2
- package/dist/workflow-runner/event-trigger.js +5 -0
- package/dist/workflow-runner/handlers/resume-run.write.js +147 -59
- package/dist/workflow-runner/pending-projection.js +1 -0
- package/dist/workflow-runner/runner.d.ts +7 -1
- package/dist/workflow-runner/runner.js +1 -1
- package/dist/workflow-runner/tables.js +8 -1
- package/package.json +41 -9
- package/src/agent-tools/changes.json +14 -0
- package/src/auth-email-password/changes.json +7 -0
- package/src/billing-foundation/changes.json +83 -0
- package/src/cap-counter/changes.json +7 -0
- package/src/cap-overview/changes.json +14 -0
- package/src/config/changes.json +7 -0
- package/src/crypto-shredding/changes.json +13 -0
- package/src/custom-fields/changes.json +7 -0
- package/src/delivery/changes.json +14 -0
- package/src/inbound-mail-foundation/changes.json +7 -0
- package/src/notes-history/changes.json +7 -0
- package/src/renderer-simple/changes.json +9 -1
- package/src/subscription-mollie/changes.json +13 -0
- package/src/subscription-stripe/changes.json +13 -0
- package/src/tags/changes.json +7 -0
- package/src/user-data-rights/changes.json +6 -0
- package/src/workflow-runner/changes.json +7 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtraRouteDefinition } from "@cosmicdrift/kumiko-framework/api";
|
|
2
|
+
import { type OAuthAccessTokenManager } from "./oauth-access-token.js";
|
|
2
3
|
export type InboundMailConnectRoutesOptions = {
|
|
3
4
|
/** HMAC-secret for the state param (deploy-time env — `scope:"system"`
|
|
4
5
|
* secrets don't exist in secrets-v1). */
|
|
@@ -14,6 +15,12 @@ export type InboundMailConnectRoutesOptions = {
|
|
|
14
15
|
readonly connectPath?: string;
|
|
15
16
|
/** Default "/inbound-mail/oauth/callback". */
|
|
16
17
|
readonly callbackPath?: string;
|
|
18
|
+
/** Apps wire the supervisor and these routes separately, so build ONE
|
|
19
|
+
* manager (`createOAuthAccessTokenManager`) and share it with `createInboundMailSupervisor({ oauthTokens })`:
|
|
20
|
+
* the callback then writes the refresh token under the account's refresh
|
|
21
|
+
* lock and primes the access-token cache from the code exchange. Omitted
|
|
22
|
+
* → plain secret write, no cache priming. */
|
|
23
|
+
readonly oauthTokens?: OAuthAccessTokenManager;
|
|
17
24
|
};
|
|
18
25
|
/** Builds the `entry:"user"`/`entry:"signature"` extraRoute pair. Mount via
|
|
19
26
|
* `extraRoutes: createInboundMailConnectRoutes(options)`. */
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// HMAC-verified state (CSRF-/foreign-claiming lock, see oauth-state.ts).
|
|
12
12
|
import { ExtraRouteRejection, signatureRoute } from "@cosmicdrift/kumiko-framework/api";
|
|
13
13
|
import { INBOUND_MAIL_FOUNDATION_FEATURE, InboundMailAuthMethods, InboundMailFoundationHandlers, inboundCredentialSecretKey, } from "./constants.js";
|
|
14
|
+
import { inboundRefreshTokenSecretOptions, } from "./oauth-access-token.js";
|
|
14
15
|
import { signOAuthState, verifyOAuthState } from "./oauth-state.js";
|
|
15
16
|
import { resolveInboundProviderForKey } from "./provider-factory.js";
|
|
16
17
|
const DEFAULT_STATE_TTL_MINUTES = 15;
|
|
@@ -97,8 +98,9 @@ export function createInboundMailConnectRoutes(options) {
|
|
|
97
98
|
redirectUri: options.callbackUrl,
|
|
98
99
|
});
|
|
99
100
|
}
|
|
100
|
-
catch
|
|
101
|
-
|
|
101
|
+
catch {
|
|
102
|
+
// Provider error messages can carry token/response data — code only.
|
|
103
|
+
return errorJson(c, 502, "token_exchange_failed", "token exchange with the provider failed");
|
|
102
104
|
}
|
|
103
105
|
if (!tokens.refreshToken) {
|
|
104
106
|
// No refresh token → no server-side sync possible. Consent must
|
|
@@ -128,12 +130,15 @@ export function createInboundMailConnectRoutes(options) {
|
|
|
128
130
|
return errorJson(c, 502, "account_create_failed", "connect-account returned no accountId");
|
|
129
131
|
}
|
|
130
132
|
// Refresh-token into the per-account secret slot (Slot = accountId).
|
|
131
|
-
// Access tokens (~1h) are
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
133
|
+
// Access tokens (~1h) are never persisted — the supervisor re-mints
|
|
134
|
+
// them from the refresh token (oauth-access-token.ts).
|
|
135
|
+
if (options.oauthTokens) {
|
|
136
|
+
await options.oauthTokens.storeRefreshToken({ id: accountId, tenantId: state.tenantId }, tokens.refreshToken);
|
|
137
|
+
options.oauthTokens.primeFromExchange({ id: accountId }, tokens);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
await deps.secrets.set(state.tenantId, inboundCredentialSecretKey(accountId), tokens.refreshToken, inboundRefreshTokenSecretOptions(accountId));
|
|
141
|
+
}
|
|
137
142
|
if (options.successRedirectUrl) {
|
|
138
143
|
const target = new URL(options.successRedirectUrl);
|
|
139
144
|
target.searchParams.set("accountId", accountId);
|
|
@@ -176,7 +176,7 @@ export const inboundMailFoundationFeature = defineFeature(INBOUND_MAIL_FOUNDATIO
|
|
|
176
176
|
return;
|
|
177
177
|
}
|
|
178
178
|
const T = (await import("@cosmicdrift/kumiko-framework/time")).getTemporal();
|
|
179
|
-
const retentionDb = ctx.db.unsafeRaw(
|
|
179
|
+
const retentionDb = ctx.db.unsafeRaw(); // @cast-boundary db-operator — jobs never run inside a DbTx
|
|
180
180
|
// Body-Objekte liegen in file-foundation. Provider lazy + memoized —
|
|
181
181
|
// heute schreibt der Ingest kein bodyRef (storeBody-Hook ungebunden),
|
|
182
182
|
// der Provider wird also nie gebaut. Sobald storeBody wired ist, raeumt
|
|
@@ -91,6 +91,7 @@ export const ingestMessageHandler = {
|
|
|
91
91
|
access: { roles: ["SystemAdmin"] },
|
|
92
92
|
agent: { expose: false },
|
|
93
93
|
escapeHatch: {
|
|
94
|
+
grants: ["unsafeRaw"],
|
|
94
95
|
reason: THREAD_ROLLUP_ADVISORY_LOCK_REASON,
|
|
95
96
|
},
|
|
96
97
|
handler: async (event, ctx) => {
|
|
@@ -229,7 +230,7 @@ export const ingestMessageHandler = {
|
|
|
229
230
|
tenantId,
|
|
230
231
|
}, { tenantId, entityName: MAIL_THREAD_AGGREGATE_TYPE })
|
|
231
232
|
: threadPlainPii;
|
|
232
|
-
const threadRollupRunner = ctx.db.unsafeRaw(
|
|
233
|
+
const threadRollupRunner = ctx.db.unsafeRaw();
|
|
233
234
|
await acquireNamespacedAdvisoryLock(threadRollupRunner, THREAD_ROLLUP_LOCK_NAMESPACE, threadAggId);
|
|
234
235
|
let threadAppendOk = false;
|
|
235
236
|
for (let attempt = 1; attempt <= THREAD_ROLLUP_MAX_ATTEMPTS && !threadAppendOk; attempt++) {
|
|
@@ -4,6 +4,7 @@ export { INBOUND_MAIL_FOUNDATION_FEATURE, INBOUND_MAIL_PROVIDER_EXTENSION, type
|
|
|
4
4
|
export { INBOUND_MESSAGE_PII_FIELDS, inboundMessageEntity, MAIL_ACCOUNT_PII_FIELDS, MAIL_THREAD_PII_FIELDS, mailAccountEntity, mailThreadEntity, seenMessageEntity, seenMessageTable, syncCursorEntity, syncCursorTable, } from "./entities.js";
|
|
5
5
|
export { INBOUND_MESSAGE_AGGREGATE_TYPE, INBOUND_MESSAGE_RECEIVED_EVENT_QN, INBOUND_MESSAGE_RECEIVED_EVENT_SHORT, type InboundMessageEventHeaders, type InboundMessageEventPayload, inboundMessageEventPayloadSchema, MAIL_ACCOUNT_AGGREGATE_TYPE, MAIL_ACCOUNT_CONNECTED_EVENT_QN, MAIL_ACCOUNT_CONNECTED_EVENT_SHORT, MAIL_ACCOUNT_DISCONNECTED_EVENT_QN, MAIL_ACCOUNT_DISCONNECTED_EVENT_SHORT, MAIL_ACCOUNT_UPDATED_EVENT_QN, MAIL_ACCOUNT_UPDATED_EVENT_SHORT, MAIL_THREAD_AGGREGATE_TYPE, MAIL_THREAD_UPDATED_EVENT_QN, MAIL_THREAD_UPDATED_EVENT_SHORT, type MailAccountEventHeaders, type MailAccountEventPayload, type MailThreadEventPayload, mailAccountEventPayloadSchema, mailThreadEventPayloadSchema, } from "./events.js";
|
|
6
6
|
export { inboundMailFoundationFeature } from "./feature.js";
|
|
7
|
+
export { createOAuthAccessTokenManager, inboundRefreshTokenSecretOptions, type OAuthAccessTokenManager, type OAuthAccessTokenManagerDeps, usesFoundationManagedOAuth, } from "./oauth-access-token.js";
|
|
7
8
|
export { type OAuthStatePayload, signOAuthState, type VerifyOAuthStateResult, verifyOAuthState, } from "./oauth-state.js";
|
|
8
9
|
export { inboundMessagesProjectionTable, mailAccountsProjectionTable, mailThreadsProjectionTable, } from "./projection.js";
|
|
9
10
|
export { resolveInboundProviderForAccount, resolveInboundProviderForKey, } from "./provider-factory.js";
|
|
@@ -5,6 +5,7 @@ export { INBOUND_MAIL_FOUNDATION_FEATURE, INBOUND_MAIL_PROVIDER_EXTENSION, Inbou
|
|
|
5
5
|
export { INBOUND_MESSAGE_PII_FIELDS, inboundMessageEntity, MAIL_ACCOUNT_PII_FIELDS, MAIL_THREAD_PII_FIELDS, mailAccountEntity, mailThreadEntity, seenMessageEntity, seenMessageTable, syncCursorEntity, syncCursorTable, } from "./entities.js";
|
|
6
6
|
export { INBOUND_MESSAGE_AGGREGATE_TYPE, INBOUND_MESSAGE_RECEIVED_EVENT_QN, INBOUND_MESSAGE_RECEIVED_EVENT_SHORT, inboundMessageEventPayloadSchema, MAIL_ACCOUNT_AGGREGATE_TYPE, MAIL_ACCOUNT_CONNECTED_EVENT_QN, MAIL_ACCOUNT_CONNECTED_EVENT_SHORT, MAIL_ACCOUNT_DISCONNECTED_EVENT_QN, MAIL_ACCOUNT_DISCONNECTED_EVENT_SHORT, MAIL_ACCOUNT_UPDATED_EVENT_QN, MAIL_ACCOUNT_UPDATED_EVENT_SHORT, MAIL_THREAD_AGGREGATE_TYPE, MAIL_THREAD_UPDATED_EVENT_QN, MAIL_THREAD_UPDATED_EVENT_SHORT, mailAccountEventPayloadSchema, mailThreadEventPayloadSchema, } from "./events.js";
|
|
7
7
|
export { inboundMailFoundationFeature } from "./feature.js";
|
|
8
|
+
export { createOAuthAccessTokenManager, inboundRefreshTokenSecretOptions, usesFoundationManagedOAuth, } from "./oauth-access-token.js";
|
|
8
9
|
export { signOAuthState, verifyOAuthState, } from "./oauth-state.js";
|
|
9
10
|
export { inboundMessagesProjectionTable, mailAccountsProjectionTable, mailThreadsProjectionTable, } from "./projection.js";
|
|
10
11
|
export { resolveInboundProviderForAccount, resolveInboundProviderForKey, } from "./provider-factory.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
3
|
+
import { Temporal } from "temporal-polyfill";
|
|
4
|
+
import { type InboundMailContext, type InboundMailProviderPlugin, type MailAccountRecord, type OAuthTokenSet } from "./types.js";
|
|
5
|
+
/** Shared by the OAuth callback and the refresh path so both write the slot identically. */
|
|
6
|
+
export declare function inboundRefreshTokenSecretOptions(accountId: string): {
|
|
7
|
+
redact: (plaintext: string) => string;
|
|
8
|
+
hint: string;
|
|
9
|
+
};
|
|
10
|
+
/** True for accounts whose access token the foundation must supply. */
|
|
11
|
+
export declare function usesFoundationManagedOAuth(account: MailAccountRecord, plugin: InboundMailProviderPlugin): boolean;
|
|
12
|
+
export type OAuthAccessTokenManagerDeps = {
|
|
13
|
+
readonly db: DbConnection;
|
|
14
|
+
readonly secrets: SecretsContext;
|
|
15
|
+
readonly providerCtx?: InboundMailContext;
|
|
16
|
+
readonly now?: () => Temporal.Instant;
|
|
17
|
+
readonly refreshTimeoutMs?: number;
|
|
18
|
+
};
|
|
19
|
+
export type OAuthAccessTokenManager = {
|
|
20
|
+
/** Valid access token for the account; refreshes when missing or near expiry. */
|
|
21
|
+
readonly getAccessToken: (account: MailAccountRecord, plugin: InboundMailProviderPlugin) => Promise<string>;
|
|
22
|
+
/** Writes the refresh token under the account lock and drops any cached access token. */
|
|
23
|
+
readonly storeRefreshToken: (account: Pick<MailAccountRecord, "id" | "tenantId">, refreshToken: string) => Promise<void>;
|
|
24
|
+
/** Seeds the cache from a code-exchange result (access token only). */
|
|
25
|
+
readonly primeFromExchange: (account: Pick<MailAccountRecord, "id">, tokens: OAuthTokenSet) => void;
|
|
26
|
+
readonly invalidate: (accountId: string) => void;
|
|
27
|
+
};
|
|
28
|
+
export declare function createOAuthAccessTokenManager(deps: OAuthAccessTokenManagerDeps): OAuthAccessTokenManager;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// OAuth access-token lifecycle for inbound-mail accounts. Access tokens
|
|
2
|
+
// (~1h) are never persisted: they live in a per-process cache and are
|
|
3
|
+
// re-minted from the refresh token in the account's secret slot.
|
|
4
|
+
//
|
|
5
|
+
// Serialization:
|
|
6
|
+
// - in-process: single-flight per account (concurrent callers share one
|
|
7
|
+
// refresh).
|
|
8
|
+
// - cross-worker: a pg advisory xact lock around "re-read refresh token →
|
|
9
|
+
// provider refresh → persist rotated refresh token". This serializes
|
|
10
|
+
// refreshes and secret writes so a rotating provider can't see two
|
|
11
|
+
// workers redeem the same refresh token concurrently. It does NOT make
|
|
12
|
+
// refresh exactly-once across workers: access tokens are per-process, so
|
|
13
|
+
// each worker still refreshes once itself.
|
|
14
|
+
//
|
|
15
|
+
// The refresh token is re-read inside the lock via `secrets.get` without an
|
|
16
|
+
// audit context. SecretsContext does not cache values (only unwrapped DEKs),
|
|
17
|
+
// so the read always reflects the latest committed write.
|
|
18
|
+
import { transaction } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
19
|
+
import { acquireNamespacedAdvisoryLock } from "@cosmicdrift/kumiko-framework/db";
|
|
20
|
+
import { Temporal } from "temporal-polyfill";
|
|
21
|
+
import { InboundMailAuthMethods, inboundCredentialSecretKey } from "./constants.js";
|
|
22
|
+
import { InboundAuthError, } from "./types.js";
|
|
23
|
+
// 'inoa' as ASCII — disjoint from the other fixed advisory-lock namespaces.
|
|
24
|
+
const OAUTH_REFRESH_LOCK_NAMESPACE = 0x696e6f61;
|
|
25
|
+
const ACCESS_TOKEN_EXPIRY_MARGIN = Temporal.Duration.from({ minutes: 2 });
|
|
26
|
+
// The provider call runs while the lock transaction holds a pool connection,
|
|
27
|
+
// so it gets an abort signal with this upper bound.
|
|
28
|
+
const DEFAULT_REFRESH_TIMEOUT_MS = 30_000;
|
|
29
|
+
/** Shared by the OAuth callback and the refresh path so both write the slot identically. */
|
|
30
|
+
export function inboundRefreshTokenSecretOptions(accountId) {
|
|
31
|
+
return {
|
|
32
|
+
redact: (plaintext) => `${plaintext.slice(0, 4)}…`,
|
|
33
|
+
hint: `OAuth refresh token for inbound mail account ${accountId}`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** True for accounts whose access token the foundation must supply. */
|
|
37
|
+
export function usesFoundationManagedOAuth(account, plugin) {
|
|
38
|
+
return (plugin.oauth !== undefined &&
|
|
39
|
+
(account.authMethod === InboundMailAuthMethods.oauth ||
|
|
40
|
+
account.authMethod === InboundMailAuthMethods.xoauth2));
|
|
41
|
+
}
|
|
42
|
+
function lockKey(account) {
|
|
43
|
+
return `${account.tenantId}:${account.id}`;
|
|
44
|
+
}
|
|
45
|
+
export function createOAuthAccessTokenManager(deps) {
|
|
46
|
+
const now = deps.now ?? (() => Temporal.Now.instant());
|
|
47
|
+
const refreshTimeoutMs = deps.refreshTimeoutMs ?? DEFAULT_REFRESH_TIMEOUT_MS;
|
|
48
|
+
const providerCtx = { ...deps.providerCtx, secrets: deps.secrets };
|
|
49
|
+
const cache = new Map();
|
|
50
|
+
const inFlight = new Map();
|
|
51
|
+
function parseExpiry(iso) {
|
|
52
|
+
try {
|
|
53
|
+
return Temporal.Instant.from(iso);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Unparseable expiry: treat as already expired so it is never cached.
|
|
57
|
+
return now();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function remember(accountId, tokens) {
|
|
61
|
+
cache.set(accountId, {
|
|
62
|
+
accessToken: tokens.accessToken,
|
|
63
|
+
expiresAt: parseExpiry(tokens.expiresAt),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function cachedValid(accountId) {
|
|
67
|
+
const hit = cache.get(accountId);
|
|
68
|
+
if (!hit)
|
|
69
|
+
return undefined;
|
|
70
|
+
const usableUntil = hit.expiresAt.subtract(ACCESS_TOKEN_EXPIRY_MARGIN);
|
|
71
|
+
return Temporal.Instant.compare(now(), usableUntil) < 0 ? hit.accessToken : undefined;
|
|
72
|
+
}
|
|
73
|
+
async function refreshWithTimeout(plugin, account, refreshToken) {
|
|
74
|
+
const oauth = plugin.oauth;
|
|
75
|
+
if (!oauth)
|
|
76
|
+
throw new InboundAuthError("provider has no oauth flow");
|
|
77
|
+
return oauth.refreshAccessToken(providerCtx, account, refreshToken, {
|
|
78
|
+
signal: AbortSignal.timeout(refreshTimeoutMs),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async function refresh(account, plugin) {
|
|
82
|
+
const secretKey = inboundCredentialSecretKey(account.id);
|
|
83
|
+
return transaction(deps.db, async (tx) => {
|
|
84
|
+
await acquireNamespacedAdvisoryLock(tx, OAUTH_REFRESH_LOCK_NAMESPACE, lockKey(account));
|
|
85
|
+
const stored = await deps.secrets.get(account.tenantId, secretKey);
|
|
86
|
+
if (!stored) {
|
|
87
|
+
throw new InboundAuthError("no refresh token stored for account");
|
|
88
|
+
}
|
|
89
|
+
const currentRefreshToken = stored.reveal();
|
|
90
|
+
const tokens = await refreshWithTimeout(plugin, account, currentRefreshToken);
|
|
91
|
+
if (tokens.refreshToken && tokens.refreshToken !== currentRefreshToken) {
|
|
92
|
+
await deps.secrets.set(account.tenantId, secretKey, tokens.refreshToken, inboundRefreshTokenSecretOptions(account.id));
|
|
93
|
+
}
|
|
94
|
+
remember(account.id, tokens);
|
|
95
|
+
return tokens.accessToken;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
async getAccessToken(account, plugin) {
|
|
100
|
+
const hit = cachedValid(account.id);
|
|
101
|
+
if (hit !== undefined)
|
|
102
|
+
return hit;
|
|
103
|
+
const running = inFlight.get(account.id);
|
|
104
|
+
if (running)
|
|
105
|
+
return running;
|
|
106
|
+
const started = refresh(account, plugin)
|
|
107
|
+
.catch((err) => {
|
|
108
|
+
cache.delete(account.id);
|
|
109
|
+
throw err;
|
|
110
|
+
})
|
|
111
|
+
.finally(() => {
|
|
112
|
+
inFlight.delete(account.id);
|
|
113
|
+
});
|
|
114
|
+
inFlight.set(account.id, started);
|
|
115
|
+
return started;
|
|
116
|
+
},
|
|
117
|
+
async storeRefreshToken(account, refreshToken) {
|
|
118
|
+
await transaction(deps.db, async (tx) => {
|
|
119
|
+
await acquireNamespacedAdvisoryLock(tx, OAUTH_REFRESH_LOCK_NAMESPACE, lockKey(account));
|
|
120
|
+
await deps.secrets.set(account.tenantId, inboundCredentialSecretKey(account.id), refreshToken, inboundRefreshTokenSecretOptions(account.id));
|
|
121
|
+
});
|
|
122
|
+
cache.delete(account.id);
|
|
123
|
+
},
|
|
124
|
+
primeFromExchange(account, tokens) {
|
|
125
|
+
remember(account.id, tokens);
|
|
126
|
+
},
|
|
127
|
+
invalidate(accountId) {
|
|
128
|
+
cache.delete(accountId);
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -29,7 +29,7 @@ async function archiveAndDeleteRows(ctx, table, aggregateType) {
|
|
|
29
29
|
declareEscapeHatch({ reason: INBOUND_MAIL_TENANT_DESTROY_ARCHIVE_REASON });
|
|
30
30
|
const rows = await ctx.db.selectMany(table, { tenantId: ctx.tenantId });
|
|
31
31
|
for (const row of rows) {
|
|
32
|
-
await archiveStream(ctx.db.unsafeRaw(
|
|
32
|
+
await archiveStream(ctx.db.unsafeRaw(), {
|
|
33
33
|
tenantId: ctx.tenantId,
|
|
34
34
|
aggregateId: row.id,
|
|
35
35
|
aggregateType,
|
|
@@ -112,7 +112,15 @@ export type InboundOAuthFlow = {
|
|
|
112
112
|
readonly code: string;
|
|
113
113
|
readonly redirectUri: string;
|
|
114
114
|
}) => Promise<OAuthTokenSet>;
|
|
115
|
-
readonly refreshAccessToken: (ctx: InboundMailContext, account: MailAccountRecord, refreshToken: string
|
|
115
|
+
readonly refreshAccessToken: (ctx: InboundMailContext, account: MailAccountRecord, refreshToken: string,
|
|
116
|
+
/** The foundation aborts `signal` after a hard timeout while holding a
|
|
117
|
+
* per-account lock (a pool connection is pinned meanwhile). Providers
|
|
118
|
+
* MUST pass it to their token-endpoint fetch; the foundation awaits the
|
|
119
|
+
* provider's result and never drops a late one, because that could lose
|
|
120
|
+
* a rotated refresh token. */
|
|
121
|
+
opts?: {
|
|
122
|
+
readonly signal?: AbortSignal;
|
|
123
|
+
}) => Promise<OAuthTokenSet>;
|
|
116
124
|
};
|
|
117
125
|
/**
|
|
118
126
|
* Inbound-Mail-Provider-Plugin contract. Jedes Provider-Feature
|
|
@@ -129,6 +137,10 @@ export type InboundMailProviderPlugin = {
|
|
|
129
137
|
readonly fetch: (ctx: InboundMailContext, account: MailAccountRecord, cursor: SyncCursorPayload | null, opts: {
|
|
130
138
|
readonly backfillWindowDays: number;
|
|
131
139
|
readonly maxMessages: number;
|
|
140
|
+
/** Valid OAuth access token, minted by the foundation for accounts
|
|
141
|
+
* whose provider declares `oauth` (authMethod oauth/xoauth2). Absent
|
|
142
|
+
* for password accounts and providers without an oauth flow. */
|
|
143
|
+
readonly accessToken?: string;
|
|
132
144
|
}) => Promise<InboundFetchResult>;
|
|
133
145
|
/** Nur OAuth-Provider. */
|
|
134
146
|
readonly oauth?: InboundOAuthFlow;
|
|
@@ -140,6 +152,11 @@ export type InboundMailProviderPlugin = {
|
|
|
140
152
|
readonly watch?: (ctx: InboundMailContext, account: MailAccountRecord, handlers: {
|
|
141
153
|
readonly onMessages: (msgs: readonly RawInboundMessage[]) => Promise<void>;
|
|
142
154
|
readonly onError: (err: unknown) => void;
|
|
155
|
+
},
|
|
156
|
+
/** Same `accessToken` semantics as `fetch` opts; re-minted on every
|
|
157
|
+
* supervisor (re)start of the watch. */
|
|
158
|
+
opts?: {
|
|
159
|
+
readonly accessToken?: string;
|
|
143
160
|
}) => Promise<() => Promise<void>>;
|
|
144
161
|
};
|
|
145
162
|
export declare function isInboundMailProviderPlugin(o: unknown): o is InboundMailProviderPlugin;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import type { DistributedLock } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
3
|
+
import { type OAuthAccessTokenManager } from "./oauth-access-token.js";
|
|
3
4
|
import { type InboundMailContext, type MailAccountRecord, type RawInboundMessage } from "./types.js";
|
|
4
5
|
export type InboundMailSupervisorDeps = {
|
|
5
6
|
/** Slim-Context für Provider-Calls (registry Pflicht, secrets für
|
|
@@ -34,6 +35,10 @@ export type InboundMailSupervisorDeps = {
|
|
|
34
35
|
* watcher lives. Omit for single-process deployments — every active
|
|
35
36
|
* account is watched locally, same as before #1719. */
|
|
36
37
|
readonly lock?: DistributedLock;
|
|
38
|
+
/** Access-token lifecycle for OAuth accounts. Defaults to a manager built
|
|
39
|
+
* from `db` + `providerCtx.secrets`; pass the instance shared with
|
|
40
|
+
* `createInboundMailConnectRoutes` so a fresh connect primes this cache. */
|
|
41
|
+
readonly oauthTokens?: OAuthAccessTokenManager;
|
|
37
42
|
/** TTL (seconds) for the per-account watch lease. Default 90s, renewed
|
|
38
43
|
* every ttl/3. Only used when `lock` is set. */
|
|
39
44
|
readonly watchLeaseTtlSeconds?: number;
|
|
@@ -48,9 +48,15 @@ import { configuredPiiSubjectKms, decryptPiiFieldValues, } from "@cosmicdrift/ku
|
|
|
48
48
|
import { Temporal } from "temporal-polyfill";
|
|
49
49
|
import { InboundMailAccountStatuses, InboundMailFoundationHandlers } from "./constants.js";
|
|
50
50
|
import { MAIL_ACCOUNT_PII_FIELDS, syncCursorTable } from "./entities.js";
|
|
51
|
+
import { createOAuthAccessTokenManager, usesFoundationManagedOAuth, } from "./oauth-access-token.js";
|
|
51
52
|
import { mailAccountsProjectionTable } from "./projection.js";
|
|
52
53
|
import { resolveInboundProviderForAccount } from "./provider-factory.js";
|
|
53
54
|
import { isInboundAuthError, isInboundCursorInvalidError, isInboundRateLimitError, } from "./types.js";
|
|
55
|
+
// Provider errors can embed token or response data in `message`; logs carry
|
|
56
|
+
// only the error class.
|
|
57
|
+
function describeError(err) {
|
|
58
|
+
return err instanceof Error ? err.name : typeof err;
|
|
59
|
+
}
|
|
54
60
|
const DEFAULT_POLL_INTERVAL_MS = 5 * 60 * 1000;
|
|
55
61
|
const DEFAULT_BACKFILL_WINDOW_DAYS = 30;
|
|
56
62
|
const DEFAULT_MAX_MESSAGES_PER_POLL = 200;
|
|
@@ -72,6 +78,25 @@ export function createInboundMailSupervisor(deps) {
|
|
|
72
78
|
// configured TTL — real deployments (default 90s) never hit it.
|
|
73
79
|
const renewIntervalMs = Math.max(50, Math.floor((leaseTtlSeconds * 1000) / 3));
|
|
74
80
|
const log = deps.log ?? (() => { });
|
|
81
|
+
const oauthTokens = deps.oauthTokens ??
|
|
82
|
+
(deps.providerCtx.secrets
|
|
83
|
+
? createOAuthAccessTokenManager({
|
|
84
|
+
db: deps.db,
|
|
85
|
+
secrets: deps.providerCtx.secrets,
|
|
86
|
+
providerCtx: deps.providerCtx,
|
|
87
|
+
})
|
|
88
|
+
: undefined);
|
|
89
|
+
// Undefined for non-OAuth accounts. A refresh failure propagates into the
|
|
90
|
+
// caller's handleSyncError: InboundAuthError → auth_error, anything else
|
|
91
|
+
// → transient retry.
|
|
92
|
+
async function resolveAccessToken(account, plugin) {
|
|
93
|
+
if (!usesFoundationManagedOAuth(account, plugin))
|
|
94
|
+
return undefined;
|
|
95
|
+
if (!oauthTokens) {
|
|
96
|
+
throw new Error("oauth account needs a secrets context for token refresh");
|
|
97
|
+
}
|
|
98
|
+
return oauthTokens.getAccessToken(account, plugin);
|
|
99
|
+
}
|
|
75
100
|
let running = false;
|
|
76
101
|
let pollTimer = null;
|
|
77
102
|
let pollInFlight = null;
|
|
@@ -213,7 +238,7 @@ export function createInboundMailSupervisor(deps) {
|
|
|
213
238
|
return true;
|
|
214
239
|
}
|
|
215
240
|
// Transient/unbekannt: loggen, nächster Tick retried.
|
|
216
|
-
log(`inbound-mail: account ${account.id} sync error: ${
|
|
241
|
+
log(`inbound-mail: account ${account.id} sync error: ${describeError(err)}`);
|
|
217
242
|
return true;
|
|
218
243
|
}
|
|
219
244
|
// ---------------------------------------------------------------
|
|
@@ -225,9 +250,11 @@ export function createInboundMailSupervisor(deps) {
|
|
|
225
250
|
try {
|
|
226
251
|
// hasMore-Schleife: Pagination innerhalb eines Ticks bis Budget.
|
|
227
252
|
for (;;) {
|
|
253
|
+
const accessToken = await resolveAccessToken(account, plugin);
|
|
228
254
|
const result = await plugin.fetch(deps.providerCtx, account, cursor, {
|
|
229
255
|
backfillWindowDays,
|
|
230
256
|
maxMessages: budget,
|
|
257
|
+
...(accessToken !== undefined && { accessToken }),
|
|
231
258
|
});
|
|
232
259
|
await ingestBatch(account, result.messages, JSON.stringify(result.nextCursor));
|
|
233
260
|
await saveCursor(account, result.nextCursor);
|
|
@@ -249,7 +276,7 @@ export function createInboundMailSupervisor(deps) {
|
|
|
249
276
|
plugin = resolveInboundProviderForAccount(deps.providerCtx, account);
|
|
250
277
|
}
|
|
251
278
|
catch (err) {
|
|
252
|
-
log(`inbound-mail: ${
|
|
279
|
+
log(`inbound-mail: ${describeError(err)}`);
|
|
253
280
|
continue;
|
|
254
281
|
}
|
|
255
282
|
await pollAccount(account, plugin);
|
|
@@ -290,7 +317,7 @@ export function createInboundMailSupervisor(deps) {
|
|
|
290
317
|
renewed = await deps.lock.renew(account.id, state.lockToken, leaseTtlSeconds);
|
|
291
318
|
}
|
|
292
319
|
catch (err) {
|
|
293
|
-
log(`inbound-mail: watch lease renew for account ${account.id} failed: ${
|
|
320
|
+
log(`inbound-mail: watch lease renew for account ${account.id} failed: ${describeError(err)}`);
|
|
294
321
|
state.renewFailures += 1;
|
|
295
322
|
// After failures spanning a full lease TTL, tear down — otherwise a
|
|
296
323
|
// peer can acquire the expired key while we still hold an IDLE conn.
|
|
@@ -379,7 +406,7 @@ export function createInboundMailSupervisor(deps) {
|
|
|
379
406
|
state.stop = null;
|
|
380
407
|
const delay = state.backoffMs;
|
|
381
408
|
state.backoffMs = Math.min(state.backoffMs * 2, backoffMaxMs);
|
|
382
|
-
log(`inbound-mail: watch for account ${account.id} died (${
|
|
409
|
+
log(`inbound-mail: watch for account ${account.id} died (${describeError(err)}) — restart in ${delay}ms`);
|
|
383
410
|
void markAccount(account, { watchState: `backoff:${delay}ms` }, "watch_supervisor");
|
|
384
411
|
state.restartTimer = setTimeout(() => {
|
|
385
412
|
state.restartTimer = null;
|
|
@@ -388,27 +415,28 @@ export function createInboundMailSupervisor(deps) {
|
|
|
388
415
|
};
|
|
389
416
|
state.starting = true;
|
|
390
417
|
try {
|
|
418
|
+
const accessToken = await resolveAccessToken(account, plugin);
|
|
391
419
|
const stop = await plugin.watch(deps.providerCtx, account, {
|
|
392
420
|
onMessages: async (msgs) => {
|
|
393
421
|
try {
|
|
394
422
|
await ingestBatch(account, msgs, "watch");
|
|
395
423
|
}
|
|
396
424
|
catch (err) {
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
log(`inbound-mail: watch-ingest for account ${account.id} failed: ${
|
|
425
|
+
// An ingest failure must not kill the watcher: the next poll
|
|
426
|
+
// tick fetches the messages again and dedup keeps it idempotent.
|
|
427
|
+
log(`inbound-mail: watch-ingest for account ${account.id} failed: ${describeError(err)}`);
|
|
400
428
|
}
|
|
401
429
|
},
|
|
402
430
|
onError: (err) => {
|
|
403
431
|
void (async () => {
|
|
404
432
|
const keepRunning = await handleSyncError(account, err);
|
|
405
|
-
// auth_error
|
|
406
|
-
// scheduleRestart no-op
|
|
433
|
+
// On auth_error stopWatcher already bumped the generation, so
|
|
434
|
+
// scheduleRestart is a no-op; every other error restarts with backoff.
|
|
407
435
|
if (keepRunning)
|
|
408
436
|
scheduleRestart(err);
|
|
409
437
|
})();
|
|
410
438
|
},
|
|
411
|
-
});
|
|
439
|
+
}, accessToken !== undefined ? { accessToken } : undefined);
|
|
412
440
|
if (state.generation !== generation || !running) {
|
|
413
441
|
await stop();
|
|
414
442
|
// skip: stop() kam während des Connects — Watcher wurde sofort wieder abgebaut.
|
|
@@ -425,7 +453,7 @@ export function createInboundMailSupervisor(deps) {
|
|
|
425
453
|
await markAccount(account, { watchState: "watching" }, "watch_supervisor");
|
|
426
454
|
}
|
|
427
455
|
catch (err) {
|
|
428
|
-
log(`inbound-mail: markAccount(watching) for account ${account.id} failed: ${
|
|
456
|
+
log(`inbound-mail: markAccount(watching) for account ${account.id} failed: ${describeError(err)}`);
|
|
429
457
|
}
|
|
430
458
|
}
|
|
431
459
|
catch (err) {
|
|
@@ -464,7 +492,7 @@ export function createInboundMailSupervisor(deps) {
|
|
|
464
492
|
await stop();
|
|
465
493
|
}
|
|
466
494
|
catch (err) {
|
|
467
|
-
log(`inbound-mail: stop watcher ${accountId} failed: ${
|
|
495
|
+
log(`inbound-mail: stop watcher ${accountId} failed: ${describeError(err)}`);
|
|
468
496
|
}
|
|
469
497
|
}
|
|
470
498
|
if (deps.lock && lockToken) {
|
|
@@ -472,7 +500,7 @@ export function createInboundMailSupervisor(deps) {
|
|
|
472
500
|
await deps.lock.release(accountId, lockToken);
|
|
473
501
|
}
|
|
474
502
|
catch (err) {
|
|
475
|
-
log(`inbound-mail: watch lease release for account ${accountId} failed: ${
|
|
503
|
+
log(`inbound-mail: watch lease release for account ${accountId} failed: ${describeError(err)}`);
|
|
476
504
|
}
|
|
477
505
|
}
|
|
478
506
|
}
|
|
@@ -486,7 +514,7 @@ export function createInboundMailSupervisor(deps) {
|
|
|
486
514
|
pollTimer = setTimeout(() => {
|
|
487
515
|
pollTimer = null;
|
|
488
516
|
pollInFlight = pollOnce()
|
|
489
|
-
.catch((err) => log(`inbound-mail: poll tick failed: ${
|
|
517
|
+
.catch((err) => log(`inbound-mail: poll tick failed: ${describeError(err)}`))
|
|
490
518
|
.finally(() => {
|
|
491
519
|
pollInFlight = null;
|
|
492
520
|
scheduleNextPoll();
|
|
@@ -38,6 +38,7 @@ export const setWrite = defineWriteHandler({
|
|
|
38
38
|
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
39
39
|
description: "Creates or overwrites one managed page addressed by slug and language, keeping the existing published flag, description and OG image when the payload omits them; use it for content edits and publish toggles, and as SystemAdmin to write another tenant's pages.",
|
|
40
40
|
escapeHatch: {
|
|
41
|
+
grants: ["unsafeRaw"],
|
|
41
42
|
reason: SET_PAGE_TENANT_OVERRIDE_REASON,
|
|
42
43
|
},
|
|
43
44
|
handler: async (event, ctx) => {
|
|
@@ -53,7 +54,7 @@ export const setWrite = defineWriteHandler({
|
|
|
53
54
|
// on both the existing-check and the executor's stream reads; re-scope to the target.
|
|
54
55
|
// Safe: the override branch is SystemAdmin-gated above.
|
|
55
56
|
const scopedDb = override !== undefined
|
|
56
|
-
? createTenantDb(db.unsafeRaw(
|
|
57
|
+
? createTenantDb(db.unsafeRaw(), override, // @cast-boundary engine-bridge
|
|
57
58
|
"tenant")
|
|
58
59
|
: db;
|
|
59
60
|
const existing = await fetchOne(scopedDb, pagesTable, {
|
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
import { type EntityDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
1
2
|
export declare const noteEntryExecutor: import("@cosmicdrift/kumiko-types/event-store-executor-types").EventStoreExecutor, noteEntryTable: import("@cosmicdrift/kumiko-framework/db").EntityTable;
|
|
3
|
+
export declare function createNoteEntryExecutor(ownership: EntityDefinition["access"] | undefined): typeof noteEntryExecutor;
|
|
2
4
|
export declare const noteMentionExecutor: import("@cosmicdrift/kumiko-types/event-store-executor-types").EventStoreExecutor, noteMentionTable: import("@cosmicdrift/kumiko-framework/db").EntityTable;
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { createEntityExecutor } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import { noteEntryEntity, noteMentionEntity } from "./entity.js";
|
|
2
|
+
import { createNoteEntryEntity, noteEntryEntity, noteMentionEntity } from "./entity.js";
|
|
3
3
|
export const { executor: noteEntryExecutor, table: noteEntryTable } = createEntityExecutor("note-entry", noteEntryEntity);
|
|
4
|
+
// Carries only `ownership.write`: the executor enforces entity.access.write on
|
|
5
|
+
// create, while read ownership is applied by the list handler.
|
|
6
|
+
export function createNoteEntryExecutor(ownership) {
|
|
7
|
+
if (ownership?.write === undefined)
|
|
8
|
+
return noteEntryExecutor;
|
|
9
|
+
return createEntityExecutor("note-entry", createNoteEntryEntity({ write: ownership.write }))
|
|
10
|
+
.executor;
|
|
11
|
+
}
|
|
4
12
|
export const { executor: noteMentionExecutor, table: noteMentionTable } = createEntityExecutor("note-mention", noteMentionEntity);
|
|
@@ -19,9 +19,8 @@ export type NotesHistoryFeatureOptions = {
|
|
|
19
19
|
* it is AND-ed with the host-visibility gate, never a replacement for it.
|
|
20
20
|
*
|
|
21
21
|
* `ownership.write` is separate and does NOT affect list/read. It's
|
|
22
|
-
* consulted
|
|
23
|
-
*
|
|
24
|
-
* boot-guard comment). A `from()` rule there also gates GDPR erasure
|
|
22
|
+
* consulted on add-note (create) and the framework's generic
|
|
23
|
+
* forget/restore paths. A `from()` rule there also gates GDPR erasure
|
|
25
24
|
* (`forget`): if the rule's role map doesn't cover whatever role the
|
|
26
25
|
* erasure/retention pipeline runs as, `forget` denies instead of
|
|
27
26
|
* crypto-shredding — a silent Art.17 failure, not a thrown error. Make
|
|
@@ -16,6 +16,7 @@ import { defineEntityListHandler, defineFeature, } from "@cosmicdrift/kumiko-fra
|
|
|
16
16
|
import { hasWhereRule } from "../shared/index.js";
|
|
17
17
|
import { DEFAULT_NOTES_HISTORY_ACCESS, NOTES_HISTORY_FEATURE_NAME } from "./constants.js";
|
|
18
18
|
import { createNoteEntryEntity, noteMentionEntity } from "./entity.js";
|
|
19
|
+
import { createNoteEntryExecutor } from "./executor.js";
|
|
19
20
|
import { createAddNoteHandler } from "./handlers/add-note.write.js";
|
|
20
21
|
import { NOTES_HISTORY_FEATURE_I18N } from "./i18n.js";
|
|
21
22
|
function registerNotesHistory(r, access, ownership, parents) {
|
|
@@ -33,7 +34,7 @@ function registerNotesHistory(r, access, ownership, parents) {
|
|
|
33
34
|
// just building it inline in the handler) is what makes it visible to the
|
|
34
35
|
// registry-wide GDPR boot guards and to executor.ts's table/projection setup.
|
|
35
36
|
r.entity("note-mention", noteMentionEntity);
|
|
36
|
-
r.writeHandler(createAddNoteHandler(access));
|
|
37
|
+
r.writeHandler(createAddNoteHandler(access, createNoteEntryExecutor(ownership)));
|
|
37
38
|
r.queryHandler(defineEntityListHandler("note-entry", entity, {
|
|
38
39
|
access,
|
|
39
40
|
description: "Lists note-history entries of the caller's tenant with their text, author and timestamp; filter on entityId to read the full note trail of one record.",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { AccessRule, WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
|
|
2
|
+
import { noteEntryExecutor } from "../executor.js";
|
|
3
|
+
export declare function createAddNoteHandler(access?: AccessRule, entryExecutor?: typeof noteEntryExecutor): WriteHandlerDef;
|
|
3
4
|
export declare const addNoteHandler: WriteHandlerDef;
|