@cosmicdrift/kumiko-bundled-features 0.338.0 → 0.340.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/auth-email-password/handlers/invite-accept-with-login.write.js +2 -2
- package/dist/auth-email-password/handlers/invite-accept.write.js +2 -4
- package/dist/auth-email-password/handlers/invite-info.query.js +3 -1
- package/dist/auth-email-password/handlers/invite-signup-complete.write.js +2 -4
- package/dist/auth-email-password/handlers/signup-confirm.write.js +1 -1
- package/dist/auth-email-password/handlers/signup-request.write.js +1 -1
- 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 +1 -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/constants.d.ts +7 -0
- package/dist/billing-foundation/constants.js +8 -0
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +3 -3
- package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +3 -3
- package/dist/billing-foundation/consumer-protection/consent-gate.js +10 -2
- package/dist/billing-foundation/consumer-protection/consent-text.js +2 -2
- package/dist/billing-foundation/consumer-protection/register-termination.d.ts +1 -1
- package/dist/billing-foundation/consumer-protection/register-termination.js +31 -3
- package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +3 -1
- package/dist/billing-foundation/consumer-protection/termination-mail.js +1 -1
- 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.js +31 -2
- package/dist/billing-foundation/consumer-protection/termination-record.d.ts +24 -2
- package/dist/billing-foundation/consumer-protection/termination-record.js +102 -34
- package/dist/billing-foundation/consumer-protection/termination-request.js +10 -42
- package/dist/billing-foundation/consumer-protection/termination-texts.js +20 -20
- package/dist/billing-foundation/events.d.ts +19 -0
- package/dist/billing-foundation/events.js +12 -0
- package/dist/billing-foundation/handlers/create-checkout-session.write.js +2 -2
- package/dist/billing-foundation/handlers/start-plan-checkout.write.js +4 -2
- package/dist/billing-foundation/i18n.js +3 -0
- package/dist/billing-foundation/projection.js +23 -40
- package/dist/billing-foundation/tenant-destroy-hook.js +4 -4
- package/dist/billing-foundation/types.d.ts +6 -1
- package/dist/billing-foundation/validate-options.js +4 -0
- 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/crypto-shredding/handlers/forget-subject.write.js +6 -6
- package/dist/custom-fields/handlers/clear-custom-field.write.js +1 -1
- package/dist/custom-fields/handlers/define-tenant-field.write.js +1 -1
- package/dist/custom-fields/handlers/set-custom-field.write.js +1 -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 +3 -0
- 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/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 +1 -1
- package/dist/form-draft/handlers/save.write.js +1 -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 +1 -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/projection.js +37 -45
- 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 +1 -1
- package/dist/notes-history/handlers/add-note.write.js +1 -1
- 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 +1 -1
- package/dist/tenant-handover/handlers/claim.write.js +1 -2
- package/dist/tenant-lifecycle/feature.js +1 -1
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +3 -1
- package/dist/tenant-lifecycle/handlers/request-destruction.write.js +1 -1
- package/dist/tier-engine/feature.js +1 -1
- package/dist/tier-engine/handlers/get-tenant-tier.query.js +1 -1
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +1 -1
- package/dist/user/handlers/update-roles.js +7 -18
- package/dist/user-data-rights/feature.js +2 -2
- package/dist/user-data-rights/handlers/cancel-deletion.write.js +1 -1
- package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +1 -1
- package/dist/user-data-rights/handlers/download-by-job.query.js +1 -1
- package/dist/user-data-rights/handlers/download-by-token.query.js +1 -1
- package/dist/user-data-rights/handlers/export-status.query.js +1 -1
- package/dist/user-data-rights/handlers/lift-restriction.write.js +2 -4
- package/dist/user-data-rights/handlers/my-audit-log.query.js +1 -1
- package/dist/user-data-rights/handlers/request-deletion-by-email.write.js +1 -1
- package/dist/user-data-rights/handlers/request-deletion.write.js +1 -1
- package/dist/user-data-rights/handlers/request-export.write.js +1 -1
- package/dist/user-data-rights/handlers/restrict-account.write.js +2 -2
- package/dist/user-data-rights/handlers/run-forget-cleanup.write.js +1 -1
- package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
- package/dist/workflow-runner/db/queries/due-runs.d.ts +4 -5
- package/dist/workflow-runner/db/queries/due-runs.js +5 -4
- package/dist/workflow-runner/event-subscriber.js +6 -2
- package/dist/workflow-runner/event-trigger.js +5 -0
- package/dist/workflow-runner/feature.js +4 -6
- 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/auth-email-password/changes.json +7 -0
- package/src/billing-foundation/changes.json +41 -0
- package/src/delivery/changes.json +7 -0
- package/src/inbound-mail-foundation/changes.json +14 -0
- package/src/renderer-simple/changes.json +9 -1
- package/src/user/changes.json +13 -6
- package/src/workflow-runner/changes.json +14 -0
- package/dist/billing-foundation/db/queries/payment-projection.d.ts +0 -2
- package/dist/billing-foundation/db/queries/payment-projection.js +0 -13
- package/dist/billing-foundation/db/queries/subscription-projection.d.ts +0 -2
- package/dist/billing-foundation/db/queries/subscription-projection.js +0 -7
- package/dist/inbound-mail-foundation/db/queries/inbound-projections.d.ts +0 -7
- package/dist/inbound-mail-foundation/db/queries/inbound-projections.js +0 -24
|
@@ -16,51 +16,45 @@
|
|
|
16
16
|
// kommen bereits als Ciphertext an — encrypted VOR dem append im
|
|
17
17
|
// write-handler. Die applies kopieren nur durch; decrypted wird erst
|
|
18
18
|
// in den list-queries.
|
|
19
|
+
import { insertOnConflictDoNothing, requireEntityTableMeta, upsertOnConflict, } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
19
20
|
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
20
21
|
import { defineApply } from "@cosmicdrift/kumiko-framework/engine";
|
|
21
|
-
import { insertIgnoreProjectionRow, upsertProjectionRow, } from "./db/queries/inbound-projections.js";
|
|
22
22
|
import { inboundMessageEntity, mailAccountEntity, mailThreadEntity } from "./entities.js";
|
|
23
23
|
// Drizzle-table-instances aus den entity-shapes — geteilt zwischen
|
|
24
24
|
// projection-applies und list-queries (ein column-namespace).
|
|
25
25
|
export const mailAccountsProjectionTable = buildEntityTable("mail-account", mailAccountEntity);
|
|
26
26
|
export const inboundMessagesProjectionTable = buildEntityTable("inbound-message", inboundMessageEntity);
|
|
27
27
|
export const mailThreadsProjectionTable = buildEntityTable("mail-thread", mailThreadEntity);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
// Unbranded metas: the apply functions below ARE the executor of these
|
|
29
|
+
// projections, so they may write through the typed helpers.
|
|
30
|
+
const mailAccountsProjectionMeta = requireEntityTableMeta(mailAccountsProjectionTable, "mail-account");
|
|
31
|
+
const inboundMessagesProjectionMeta = requireEntityTableMeta(inboundMessagesProjectionTable, "inbound-message");
|
|
32
|
+
const mailThreadsProjectionMeta = requireEntityTableMeta(mailThreadsProjectionTable, "mail-thread");
|
|
31
33
|
// =============================================================================
|
|
32
34
|
// mail-account — connected/updated/disconnected teilen den payload-shape,
|
|
33
35
|
// der event-type taggt was passiert ist. Alle drei UPSERT-full für
|
|
34
36
|
// defensive consistency (rebuild-aus-dem-Nichts, out-of-order).
|
|
35
37
|
// =============================================================================
|
|
36
|
-
const ACCOUNT_SET_CLAUSES = [
|
|
37
|
-
`"provider" = EXCLUDED."provider"`,
|
|
38
|
-
`"auth_method" = EXCLUDED."auth_method"`,
|
|
39
|
-
`"owner_user_id" = EXCLUDED."owner_user_id"`,
|
|
40
|
-
`"display_name" = EXCLUDED."display_name"`,
|
|
41
|
-
`"address" = EXCLUDED."address"`,
|
|
42
|
-
`"status" = EXCLUDED."status"`,
|
|
43
|
-
`"watch_state" = EXCLUDED."watch_state"`,
|
|
44
|
-
// connected_at bewusst NICHT im SET: der Erst-Connect-Zeitpunkt
|
|
45
|
-
// bleibt stehen, updated/disconnected überschreiben ihn nicht.
|
|
46
|
-
];
|
|
47
38
|
const applyMailAccountUpsert = defineApply(async (event, tx) => {
|
|
48
39
|
const p = event.payload;
|
|
49
|
-
const
|
|
50
|
-
id: event.aggregateId,
|
|
51
|
-
tenant_id: event.tenantId,
|
|
40
|
+
const mutable = {
|
|
52
41
|
provider: p.provider,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
42
|
+
authMethod: p.authMethod,
|
|
43
|
+
ownerUserId: p.ownerUserId,
|
|
44
|
+
displayName: p.displayName,
|
|
56
45
|
address: p.address,
|
|
57
46
|
status: p.status,
|
|
58
|
-
|
|
59
|
-
// Insert-Pfad = Erst-Connect (bzw. Rebuild: createdAt des ersten
|
|
60
|
-
// events des Streams — fachlich derselbe Zeitpunkt).
|
|
61
|
-
connected_at: event.createdAt.toString(),
|
|
47
|
+
watchState: p.watchState,
|
|
62
48
|
};
|
|
63
|
-
await
|
|
49
|
+
await upsertOnConflict(tx, mailAccountsProjectionMeta, {
|
|
50
|
+
id: event.aggregateId,
|
|
51
|
+
tenantId: event.tenantId,
|
|
52
|
+
...mutable,
|
|
53
|
+
// Only the insert path is the first connect (on rebuild: createdAt of the
|
|
54
|
+
// stream's first event, the same moment). Left out of `update` so the
|
|
55
|
+
// first-connect time survives updated/disconnected events.
|
|
56
|
+
connectedAt: event.createdAt,
|
|
57
|
+
}, { conflictKeys: ["id"], update: mutable });
|
|
64
58
|
});
|
|
65
59
|
/** mail-account-connected → UPSERT full. */
|
|
66
60
|
export const applyMailAccountConnected = applyMailAccountUpsert;
|
|
@@ -77,20 +71,20 @@ export const applyMailAccountDisconnected = applyMailAccountUpsert;
|
|
|
77
71
|
/** inbound-message-received → INSERT ... ON CONFLICT DO NOTHING. */
|
|
78
72
|
export const applyInboundMessageReceived = defineApply(async (event, tx) => {
|
|
79
73
|
const p = event.payload;
|
|
80
|
-
await
|
|
74
|
+
await insertOnConflictDoNothing(tx, inboundMessagesProjectionMeta, {
|
|
81
75
|
id: event.aggregateId,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
76
|
+
tenantId: event.tenantId,
|
|
77
|
+
accountId: p.accountId,
|
|
78
|
+
ownerUserId: p.ownerUserId,
|
|
79
|
+
messageIdHeader: p.messageIdHeader,
|
|
80
|
+
threadKey: p.threadKey,
|
|
87
81
|
from: p.from,
|
|
88
82
|
to: p.to,
|
|
89
83
|
cc: p.cc,
|
|
90
84
|
subject: p.subject,
|
|
91
85
|
snippet: p.snippet,
|
|
92
|
-
|
|
93
|
-
|
|
86
|
+
receivedAt: p.receivedAtIso,
|
|
87
|
+
bodyRef: p.bodyRef,
|
|
94
88
|
scope: p.scope,
|
|
95
89
|
});
|
|
96
90
|
});
|
|
@@ -103,17 +97,15 @@ export const applyInboundMessageReceived = defineApply(async (event, tx) => {
|
|
|
103
97
|
/** mail-thread-updated → UPSERT full mit dem Payload-Snapshot. */
|
|
104
98
|
export const applyMailThreadUpdated = defineApply(async (event, tx) => {
|
|
105
99
|
const p = event.payload;
|
|
106
|
-
const
|
|
107
|
-
id: event.aggregateId,
|
|
108
|
-
tenant_id: event.tenantId,
|
|
109
|
-
thread_key: p.threadKey,
|
|
100
|
+
const mutable = {
|
|
110
101
|
subject: p.subject,
|
|
111
|
-
|
|
112
|
-
|
|
102
|
+
lastMessageAt: p.lastMessageAtIso,
|
|
103
|
+
messageCount: p.messageCount,
|
|
113
104
|
};
|
|
114
|
-
await
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
105
|
+
await upsertOnConflict(tx, mailThreadsProjectionMeta, {
|
|
106
|
+
id: event.aggregateId,
|
|
107
|
+
tenantId: event.tenantId,
|
|
108
|
+
threadKey: p.threadKey,
|
|
109
|
+
...mutable,
|
|
110
|
+
}, { conflictKeys: ["id"], update: mutable });
|
|
119
111
|
});
|
|
@@ -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();
|
|
@@ -54,7 +54,7 @@ export const setWrite = defineWriteHandler({
|
|
|
54
54
|
// on both the existing-check and the executor's stream reads; re-scope to the target.
|
|
55
55
|
// Safe: the override branch is SystemAdmin-gated above.
|
|
56
56
|
const scopedDb = override !== undefined
|
|
57
|
-
? createTenantDb(db.unsafeRaw(
|
|
57
|
+
? createTenantDb(db.unsafeRaw(), override, // @cast-boundary engine-bridge
|
|
58
58
|
"tenant")
|
|
59
59
|
: db;
|
|
60
60
|
const existing = await fetchOne(scopedDb, pagesTable, {
|
|
@@ -54,7 +54,7 @@ export function createAddNoteHandler(access = DEFAULT_NOTES_HISTORY_ACCESS, entr
|
|
|
54
54
|
// Bun.SQL poisons the whole tx after any error inside it, even one that's
|
|
55
55
|
// caught — so the lookup runs in a savepoint where available, otherwise directly
|
|
56
56
|
// (pool statements are their own units).
|
|
57
|
-
authorName = await runInSavepointIfSupported(ctx.db.unsafeRaw(
|
|
57
|
+
authorName = await runInSavepointIfSupported(ctx.db.unsafeRaw(), async (sp) => {
|
|
58
58
|
const userRow = await fetchOne(sp, userTable, {
|
|
59
59
|
id: event.user.id,
|
|
60
60
|
});
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import type { NotificationRenderer } from "../delivery/index.js";
|
|
2
3
|
import { type RendererContext, type RenderRequest, type RenderResponse } from "../renderer-foundation/index.js";
|
|
3
|
-
|
|
4
|
-
export declare function
|
|
4
|
+
import { type MailBranding } from "./simple-renderer.js";
|
|
5
|
+
export declare function adaptToFoundation(req: RenderRequest, ctx: RendererContext, renderer?: NotificationRenderer): Promise<RenderResponse>;
|
|
6
|
+
export type RendererSimpleOptions = {
|
|
7
|
+
/** Branding applied to every notification mail, including the framework auth mails. */
|
|
8
|
+
readonly mailBranding?: MailBranding;
|
|
9
|
+
};
|
|
10
|
+
export declare function createRendererSimpleFeature(options?: RendererSimpleOptions): FeatureDefinition;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { RENDERER_EXTENSION, RendererError, } from "../renderer-foundation/index.js";
|
|
3
3
|
import { resolveNotificationVariables } from "./resolve-variables.js";
|
|
4
|
-
import { simpleRenderer } from "./simple-renderer.js";
|
|
4
|
+
import { createSimpleRenderer, simpleRenderer } from "./simple-renderer.js";
|
|
5
5
|
// Adapter: simpleRenderer.render hat `Promise<string>`-Signatur (Legacy
|
|
6
6
|
// NotificationRenderer-Contract), renderer-foundation erwartet
|
|
7
7
|
// `Promise<RenderResponse>` mit discriminated union. Mapper bewahrt
|
|
@@ -9,20 +9,23 @@ import { simpleRenderer } from "./simple-renderer.js";
|
|
|
9
9
|
// Inline-CSS) und packt sie in den RendererPlugin-Contract.
|
|
10
10
|
//
|
|
11
11
|
// Exported damit der Adapter-Pfad direkt testbar ist (unit-test).
|
|
12
|
-
export async function adaptToFoundation(req, ctx) {
|
|
12
|
+
export async function adaptToFoundation(req, ctx, renderer = simpleRenderer) {
|
|
13
13
|
if (req.kind !== "notification") {
|
|
14
14
|
// Defensiver Guard — Foundation wählt Plugins nur für matching kinds,
|
|
15
15
|
// dieser Pfad sollte unter normalen Umständen nie erreicht werden.
|
|
16
16
|
throw new RendererError(`renderer-simple supports only kind="notification", got "${req.kind}"`, "invalid_payload");
|
|
17
17
|
}
|
|
18
18
|
const variables = await resolveNotificationVariables(req, ctx);
|
|
19
|
-
const html = await
|
|
19
|
+
const html = await renderer.render({
|
|
20
20
|
template: req.payload.template ?? "",
|
|
21
21
|
variables,
|
|
22
22
|
});
|
|
23
23
|
return { kind: "notification", html };
|
|
24
24
|
}
|
|
25
|
-
export function createRendererSimpleFeature() {
|
|
25
|
+
export function createRendererSimpleFeature(options) {
|
|
26
|
+
const renderer = options?.mailBranding
|
|
27
|
+
? createSimpleRenderer(options.mailBranding)
|
|
28
|
+
: simpleRenderer;
|
|
26
29
|
return defineFeature("renderer-simple", (r) => {
|
|
27
30
|
r.describe('Default renderer plugin for `kind="notification"`: takes a structured `EmailTemplateData` variable map (with `header`, `sections[]` of text/button objects, and optional `footer`; falls back to `title`/`body` if no structured fields are present) and returns rendered HTML with inline CSS. Requires `renderer-foundation`; sufficient for plain notification emails \u2014 swap it for `renderer-mail-html` if you need MJML/Markdown layouts.');
|
|
28
31
|
r.uiHints({
|
|
@@ -34,7 +37,7 @@ export function createRendererSimpleFeature() {
|
|
|
34
37
|
r.optionalRequires("template-resolver");
|
|
35
38
|
r.useExtension(RENDERER_EXTENSION, "simple", {
|
|
36
39
|
kinds: ["notification"],
|
|
37
|
-
render: adaptToFoundation,
|
|
40
|
+
render: (req, ctx) => adaptToFoundation(req, ctx, renderer),
|
|
38
41
|
});
|
|
39
42
|
});
|
|
40
43
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { createRendererSimpleFeature } from "./feature.js";
|
|
2
|
-
export { simpleRenderer } from "./simple-renderer.js";
|
|
1
|
+
export { createRendererSimpleFeature, type RendererSimpleOptions } from "./feature.js";
|
|
2
|
+
export { createSimpleRenderer, type MailBranding, simpleRenderer } from "./simple-renderer.js";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { createRendererSimpleFeature } from "./feature.js";
|
|
2
|
-
export { simpleRenderer } from "./simple-renderer.js";
|
|
2
|
+
export { createSimpleRenderer, simpleRenderer } from "./simple-renderer.js";
|
|
@@ -1,2 +1,17 @@
|
|
|
1
1
|
import type { NotificationRenderer } from "../delivery/index.js";
|
|
2
|
+
export type MailBranding = {
|
|
3
|
+
/** Shown as text when there is no logo, and as the logo alt text. */
|
|
4
|
+
readonly productName?: string;
|
|
5
|
+
/** Hex color (#rgb, #rrggbb, #rrggbbaa); used for buttons and the header accent. */
|
|
6
|
+
readonly primaryColor?: string;
|
|
7
|
+
/** Absolute http(s) URL; anything else is dropped. */
|
|
8
|
+
readonly logoUrl?: string;
|
|
9
|
+
readonly footerText?: string;
|
|
10
|
+
/** Absolute http(s) URLs only; other entries are dropped. */
|
|
11
|
+
readonly footerLinks?: readonly {
|
|
12
|
+
readonly label: string;
|
|
13
|
+
readonly url: string;
|
|
14
|
+
}[];
|
|
15
|
+
};
|
|
16
|
+
export declare function createSimpleRenderer(branding?: MailBranding): NotificationRenderer;
|
|
2
17
|
export declare const simpleRenderer: NotificationRenderer;
|
|
@@ -1,37 +1,84 @@
|
|
|
1
|
-
import { escapeHtml } from "@cosmicdrift/kumiko-headless";
|
|
2
|
-
|
|
1
|
+
import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
|
|
2
|
+
const DEFAULT_PRIMARY_COLOR = "#2563eb";
|
|
3
|
+
const HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
4
|
+
function isHttpUrl(value) {
|
|
5
|
+
try {
|
|
6
|
+
const { protocol } = new URL(value);
|
|
7
|
+
return protocol === "http:" || protocol === "https:";
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function resolvePrimaryColor(branding) {
|
|
14
|
+
const color = branding?.primaryColor;
|
|
15
|
+
return color && HEX_COLOR_PATTERN.test(color) ? color : DEFAULT_PRIMARY_COLOR;
|
|
16
|
+
}
|
|
17
|
+
function renderBrandingHeader(branding, primaryColor) {
|
|
18
|
+
if (!branding)
|
|
19
|
+
return "";
|
|
20
|
+
const logoUrl = branding.logoUrl && isHttpUrl(branding.logoUrl) ? branding.logoUrl : undefined;
|
|
21
|
+
if (!logoUrl && !branding.productName)
|
|
22
|
+
return "";
|
|
23
|
+
const logo = logoUrl
|
|
24
|
+
? `<img src="${escapeHtmlAttr(logoUrl)}" alt="${escapeHtmlAttr(branding.productName ?? "")}" style="display:block;max-height:40px;max-width:200px;border:0" />`
|
|
25
|
+
: `<span style="font-size:18px;font-weight:700;color:${escapeHtmlAttr(primaryColor)}">${escapeHtml(branding.productName ?? "")}</span>`;
|
|
26
|
+
return `<div style="margin:0 0 24px;padding:0 0 16px;border-bottom:3px solid ${escapeHtmlAttr(primaryColor)}">${logo}</div>`;
|
|
27
|
+
}
|
|
28
|
+
function renderBrandingFooter(branding) {
|
|
29
|
+
if (!branding)
|
|
30
|
+
return "";
|
|
31
|
+
const links = (branding.footerLinks ?? [])
|
|
32
|
+
.filter((link) => isHttpUrl(link.url))
|
|
33
|
+
.map((link) => `<a href="${escapeHtmlAttr(link.url)}" style="color:#999">${escapeHtml(link.label)}</a>`);
|
|
34
|
+
const footerPartsHtml = [
|
|
35
|
+
branding.footerText ? escapeHtml(branding.footerText) : "",
|
|
36
|
+
links.join(" · "),
|
|
37
|
+
].filter((part) => part !== "");
|
|
38
|
+
if (footerPartsHtml.length === 0)
|
|
39
|
+
return "";
|
|
40
|
+
const footerHtml = footerPartsHtml.join("<br />");
|
|
41
|
+
return `<p style="margin:16px 0 0;color:#999;font-size:12px">${footerHtml}</p>`;
|
|
42
|
+
}
|
|
43
|
+
function renderSection(section, primaryColor) {
|
|
3
44
|
if ("text" in section) {
|
|
4
45
|
return `<p style="margin:0 0 16px;color:#333;font-size:14px;line-height:1.5">${escapeHtml(section.text)}</p>`;
|
|
5
46
|
}
|
|
6
47
|
if ("button" in section) {
|
|
7
|
-
return `<p style="margin:0 0 16px"><a href="${escapeHtml(section.button.url)}" style="display:inline-block;padding:10px 24px;background
|
|
48
|
+
return `<p style="margin:0 0 16px"><a href="${escapeHtml(section.button.url)}" style="display:inline-block;padding:10px 24px;background:${escapeHtmlAttr(primaryColor)};color:#fff;text-decoration:none;border-radius:4px;font-size:14px">${escapeHtml(section.button.label)}</a></p>`;
|
|
8
49
|
}
|
|
9
50
|
return "";
|
|
10
51
|
}
|
|
11
52
|
// Simple Renderer: turns structured email template data into HTML with inline CSS.
|
|
12
53
|
// No external dependencies, no template engine — just string concatenation.
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
parts.push(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
54
|
+
export function createSimpleRenderer(branding) {
|
|
55
|
+
const primaryColor = resolvePrimaryColor(branding);
|
|
56
|
+
return {
|
|
57
|
+
name: "simple",
|
|
58
|
+
async render(input) {
|
|
59
|
+
const data = input.variables; // @cast-boundary render-helper
|
|
60
|
+
// Fallback: if no structured fields, use title + body as header + single text section
|
|
61
|
+
const header = data.header ?? data.title;
|
|
62
|
+
const sections = data.sections ?? (data.body ? [{ text: data.body }] : undefined);
|
|
63
|
+
const parts = [];
|
|
64
|
+
parts.push('<!DOCTYPE html><html><body style="margin:0;padding:0;font-family:sans-serif">');
|
|
65
|
+
parts.push('<div style="max-width:600px;margin:0 auto;padding:24px">');
|
|
66
|
+
parts.push(renderBrandingHeader(branding, primaryColor));
|
|
67
|
+
if (header) {
|
|
68
|
+
parts.push(`<h1 style="margin:0 0 24px;color:#111;font-size:20px;font-weight:600">${escapeHtml(header)}</h1>`);
|
|
69
|
+
}
|
|
70
|
+
if (sections) {
|
|
71
|
+
for (const section of sections) {
|
|
72
|
+
parts.push(renderSection(section, primaryColor));
|
|
73
|
+
}
|
|
29
74
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
};
|
|
75
|
+
if (data.footer) {
|
|
76
|
+
parts.push(`<p style="margin:24px 0 0;color:#999;font-size:12px;border-top:1px solid #eee;padding-top:16px">${escapeHtml(data.footer)}</p>`);
|
|
77
|
+
}
|
|
78
|
+
parts.push(renderBrandingFooter(branding));
|
|
79
|
+
parts.push("</div></body></html>");
|
|
80
|
+
return parts.join("");
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export const simpleRenderer = createSimpleRenderer();
|
package/dist/secrets/feature.js
CHANGED
|
@@ -135,7 +135,7 @@ export function createSecretsFeature(opts = {}) {
|
|
|
135
135
|
escapeHatch: {
|
|
136
136
|
reason: ROTATE_ESCAPE_HATCH_REASON,
|
|
137
137
|
},
|
|
138
|
-
handler: (payload, ctx) => rotateJob(payload, ctx, ctx.db.unsafeRaw(
|
|
138
|
+
handler: (payload, ctx) => rotateJob(payload, ctx, ctx.db.unsafeRaw()),
|
|
139
139
|
});
|
|
140
140
|
// Pre-ES had a separate `retention-cleanup` job scrubbing the audit
|
|
141
141
|
// table on a compliance-driven schedule (90d default). Post-ES the
|
package/dist/sessions/feature.js
CHANGED
|
@@ -121,7 +121,7 @@ export function createSessionsFeature(options) {
|
|
|
121
121
|
name: "cleanup",
|
|
122
122
|
trigger: { manual: true },
|
|
123
123
|
escapeHatch: { reason: CLEANUP_ESCAPE_HATCH_REASON },
|
|
124
|
-
handler: (payload, ctx) => cleanupJob(payload, ctx, ctx.db.unsafeRaw(
|
|
124
|
+
handler: (payload, ctx) => cleanupJob(payload, ctx, ctx.db.unsafeRaw()),
|
|
125
125
|
});
|
|
126
126
|
// Cross-feature entity hook on "user". `r.entityHook` (NOT `r.hook`) is
|
|
127
127
|
// the supported cross-feature path: entity-keyed, not prefixed by the
|
|
@@ -41,7 +41,7 @@ export const revokeAllForUserWrite = defineWriteHandler({
|
|
|
41
41
|
reason: REVOKE_ALL_SESSIONS_REASON,
|
|
42
42
|
},
|
|
43
43
|
handler: async (event, ctx) => {
|
|
44
|
-
const runner = ctx.db.unsafeRaw(
|
|
44
|
+
const runner = ctx.db.unsafeRaw();
|
|
45
45
|
const updated = await updateMany(runner, userSessionTable, { revokedAt: Temporal.Now.instant() }, {
|
|
46
46
|
userId: event.payload.userId,
|
|
47
47
|
revokedAt: null,
|
|
@@ -36,8 +36,7 @@ export function createClaimTenantHandoverHandler(opts = {}) {
|
|
|
36
36
|
agent: { expose: false },
|
|
37
37
|
rateLimit: { per: "user", limit: 10, windowSeconds: 60 },
|
|
38
38
|
handler: async (event, ctx) => {
|
|
39
|
-
const db = ctx.db.unsafeRaw(
|
|
40
|
-
"row-bound grant redeemed below");
|
|
39
|
+
const db = ctx.db.unsafeRaw();
|
|
41
40
|
const rootLocation = resolveRootAnchorLocation(ctx.registry, db, event.payload.entityType);
|
|
42
41
|
if (!rootLocation)
|
|
43
42
|
return notTransferable(event.payload.entityType);
|
|
@@ -74,7 +74,7 @@ export function createTenantLifecycleFeature() {
|
|
|
74
74
|
}
|
|
75
75
|
const T = (await import("@cosmicdrift/kumiko-framework/time")).getTemporal();
|
|
76
76
|
await runTenantDestructionSweep({
|
|
77
|
-
db: ctx.db.unsafeRaw(
|
|
77
|
+
db: ctx.db.unsafeRaw(), // @cast-boundary db-operator — jobs never run inside a DbTx
|
|
78
78
|
registry: ctx.registry,
|
|
79
79
|
now: T.Now.instant(),
|
|
80
80
|
log: (message) => ctx.log?.warn(message),
|