@cosmicdrift/kumiko-samples 0.338.0 → 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/package.json +1 -1
- package/packages/bundled-features/package.json +21 -1
- package/packages/bundled-features/src/auth-email-password/changes.json +7 -0
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +2 -6
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept.write.ts +2 -6
- package/packages/bundled-features/src/auth-email-password/handlers/invite-info.query.ts +3 -5
- package/packages/bundled-features/src/auth-email-password/handlers/invite-signup-complete.write.ts +2 -8
- package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +35 -3
- package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +60 -0
- package/packages/bundled-features/src/auth-email-password/web/client-plugin.ts +10 -0
- package/packages/bundled-features/src/auth-email-password/web/index.ts +8 -0
- package/packages/bundled-features/src/auth-email-password/web/session.tsx +15 -2
- package/packages/bundled-features/src/auth-mfa/feature.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +1 -7
- package/packages/bundled-features/src/auth-mfa/handlers/enable-start-preauth.write.ts +1 -5
- package/packages/bundled-features/src/auth-mfa/handlers/verify.write.ts +1 -7
- package/packages/bundled-features/src/auth-mfa/mfa-code-verifier.ts +1 -5
- package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +1 -7
- package/packages/bundled-features/src/billing-foundation/changes.json +34 -0
- package/packages/bundled-features/src/billing-foundation/constants.ts +11 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +3 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +11 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-text.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +48 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +10 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +44 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +32 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +129 -33
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +13 -59
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +20 -21
- package/packages/bundled-features/src/billing-foundation/events.ts +18 -0
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +4 -5
- package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts +6 -5
- package/packages/bundled-features/src/billing-foundation/i18n.ts +3 -0
- package/packages/bundled-features/src/billing-foundation/tenant-destroy-hook.ts +4 -4
- package/packages/bundled-features/src/billing-foundation/types.ts +6 -0
- package/packages/bundled-features/src/billing-foundation/validate-options.ts +6 -0
- package/packages/bundled-features/src/channel-discord/discord-channel.ts +32 -0
- package/packages/bundled-features/src/channel-discord/feature.ts +40 -0
- package/packages/bundled-features/src/channel-discord/index.ts +2 -0
- package/packages/bundled-features/src/channel-slack/feature.ts +38 -0
- package/packages/bundled-features/src/channel-slack/index.ts +2 -0
- package/packages/bundled-features/src/channel-slack/slack-channel.ts +29 -0
- package/packages/bundled-features/src/channel-teams/feature.ts +42 -0
- package/packages/bundled-features/src/channel-teams/index.ts +2 -0
- package/packages/bundled-features/src/channel-teams/teams-channel.ts +50 -0
- package/packages/bundled-features/src/channel-telegram/feature.ts +35 -0
- package/packages/bundled-features/src/channel-telegram/index.ts +2 -0
- package/packages/bundled-features/src/channel-telegram/telegram-channel.ts +74 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +6 -18
- package/packages/bundled-features/src/custom-fields/handlers/clear-custom-field.write.ts +1 -1
- package/packages/bundled-features/src/custom-fields/handlers/define-tenant-field.write.ts +1 -4
- package/packages/bundled-features/src/custom-fields/handlers/set-custom-field.write.ts +1 -1
- package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +1 -1
- package/packages/bundled-features/src/data-retention/feature.ts +2 -3
- package/packages/bundled-features/src/delivery/changes.json +7 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +3 -1
- package/packages/bundled-features/src/delivery/chat-webhook-channel.ts +76 -0
- package/packages/bundled-features/src/delivery/chat-webhook-sender.ts +135 -0
- package/packages/bundled-features/src/delivery/delivery-service.ts +3 -0
- package/packages/bundled-features/src/delivery/index.ts +15 -0
- package/packages/bundled-features/src/delivery/jobs.ts +1 -1
- package/packages/bundled-features/src/delivery/testing.ts +2 -0
- package/packages/bundled-features/src/delivery/types.ts +8 -3
- package/packages/bundled-features/src/file-derivatives/handlers/public-variant-by-file-ref.query.ts +1 -1
- package/packages/bundled-features/src/files-tenant-data/index.ts +1 -1
- package/packages/bundled-features/src/form-draft/feature.ts +1 -1
- package/packages/bundled-features/src/form-draft/handlers/discard.write.ts +1 -1
- package/packages/bundled-features/src/form-draft/handlers/save.write.ts +1 -5
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +7 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +29 -13
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/index.ts +7 -0
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +201 -0
- package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +17 -1
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +74 -36
- package/packages/bundled-features/src/managed-pages/handlers/set.write.ts +1 -1
- package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +7 -10
- package/packages/bundled-features/src/renderer-simple/changes.json +9 -1
- package/packages/bundled-features/src/renderer-simple/feature.ts +14 -4
- package/packages/bundled-features/src/renderer-simple/index.ts +2 -2
- package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +94 -31
- package/packages/bundled-features/src/secrets/feature.ts +1 -1
- package/packages/bundled-features/src/sessions/feature.ts +1 -1
- package/packages/bundled-features/src/sessions/handlers/revoke-all-for-user.write.ts +1 -1
- package/packages/bundled-features/src/tenant-handover/handlers/claim.write.ts +1 -4
- package/packages/bundled-features/src/tenant-lifecycle/feature.ts +1 -3
- package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +3 -5
- package/packages/bundled-features/src/tenant-lifecycle/handlers/request-destruction.write.ts +1 -1
- package/packages/bundled-features/src/tier-engine/feature.ts +1 -1
- package/packages/bundled-features/src/tier-engine/handlers/get-tenant-tier.query.ts +1 -1
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/feature.ts +4 -6
- package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/confirm-deletion-by-token.write.ts +1 -3
- package/packages/bundled-features/src/user-data-rights/handlers/download-by-job.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/download-by-token.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/export-status.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/lift-restriction.write.ts +2 -6
- package/packages/bundled-features/src/user-data-rights/handlers/my-audit-log.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/request-deletion-by-email.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/request-export.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/restrict-account.write.ts +2 -8
- package/packages/bundled-features/src/user-data-rights/handlers/run-forget-cleanup.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/run-user-export.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/changes.json +7 -0
- package/packages/bundled-features/src/workflow-runner/event-subscriber.ts +7 -2
- package/packages/bundled-features/src/workflow-runner/event-trigger.ts +5 -0
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +231 -77
- package/packages/bundled-features/src/workflow-runner/pending-projection.ts +1 -0
- package/packages/bundled-features/src/workflow-runner/runner.ts +12 -2
- package/packages/bundled-features/src/workflow-runner/tables.ts +8 -1
- package/samples/apps/styleguide/src/features/widgets/i18n.ts +5 -0
- package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +11 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +16 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +152 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +82 -0
- package/samples/apps/use-all-bundled/feature-manifest.json +146 -1
- package/samples/apps/use-all-bundled/package.json +1 -0
- package/samples/apps/use-all-bundled/playwright.billing-gallery.config.ts +14 -0
- package/samples/apps/use-all-bundled/schema/generate.ts +20 -0
- package/samples/apps/use-all-bundled/scripts/gen-feature-manifest.ts +1 -0
- package/samples/apps/use-all-bundled/src/run-config.ts +10 -0
- package/samples/apps/use-all-bundled/tsconfig.json +1 -0
- package/samples/recipes/chat-channels/README.md +19 -0
- package/samples/recipes/chat-channels/package.json +9 -0
- package/samples/recipes/chat-channels/src/feature.ts +40 -0
- package/samples/recipes/public-share-token/src/feature.ts +1 -1
- package/samples/recipes/workflow-engine/src/event-trigger.ts +5 -0
- package/samples/recipes/workflow-engine/src/postgres-resume-loop.ts +50 -9
- package/samples/recipes/workflow-engine/src/workflow-runner.ts +12 -2
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import * as z from "zod";
|
|
14
14
|
import { BILLING_FOUNDATION_FEATURE, SubscriptionStatuses } from "./constants.js";
|
|
15
|
+
import { CONSENT_LOCALES } from "./consumer-protection/consent-locale.js";
|
|
15
16
|
|
|
16
17
|
// Aggregate-type für den event-store. Eine subscription pro Tenant ist
|
|
17
18
|
// ein stream; der subscriptionAggregateId-helper liefert die stream-id.
|
|
@@ -196,6 +197,23 @@ export type ContractTerminationRequestedPayload = z.infer<
|
|
|
196
197
|
typeof contractTerminationRequestedPayloadSchema
|
|
197
198
|
>;
|
|
198
199
|
|
|
200
|
+
// A public declaration matched to this tenant. The provider cancel runs in a
|
|
201
|
+
// job triggered by this event, so the request itself only looks up and appends.
|
|
202
|
+
export const CONTRACT_TERMINATION_DECLARED_EVENT_SHORT = "contract-termination-declared" as const;
|
|
203
|
+
export const CONTRACT_TERMINATION_DECLARED_EVENT_QN =
|
|
204
|
+
`${BILLING_FOUNDATION_FEATURE}:event:${CONTRACT_TERMINATION_DECLARED_EVENT_SHORT}` as const;
|
|
205
|
+
|
|
206
|
+
export const contractTerminationDeclaredPayloadSchema = z.object({
|
|
207
|
+
requestId: z.string().min(1).max(100),
|
|
208
|
+
declarationType: z.enum(CONTRACT_TERMINATION_DECLARATION_TYPES),
|
|
209
|
+
terminationKind: z.enum(CONTRACT_TERMINATION_KINDS),
|
|
210
|
+
receivedAtIso: z.string().min(1),
|
|
211
|
+
locale: z.enum(CONSENT_LOCALES),
|
|
212
|
+
});
|
|
213
|
+
export type ContractTerminationDeclaredPayload = z.infer<
|
|
214
|
+
typeof contractTerminationDeclaredPayloadSchema
|
|
215
|
+
>;
|
|
216
|
+
|
|
199
217
|
export const CONTRACT_TERMINATION_UNMATCHED_AGGREGATE_TYPE =
|
|
200
218
|
"contract-termination-unmatched" as const;
|
|
201
219
|
export const CONTRACT_TERMINATION_UNMATCHED_EVENT_SHORT = "contract-termination-unmatched" as const;
|
package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts
CHANGED
|
@@ -64,7 +64,7 @@ export function createCheckoutSessionHandler(
|
|
|
64
64
|
): WriteHandlerDef {
|
|
65
65
|
const consumerProtection = options.consumerProtection;
|
|
66
66
|
const schema = consumerProtection
|
|
67
|
-
? createCheckoutSessionSchema.extend({ consent: consentPayloadSchema })
|
|
67
|
+
? createCheckoutSessionSchema.extend({ consent: consentPayloadSchema.optional() })
|
|
68
68
|
: createCheckoutSessionSchema;
|
|
69
69
|
return {
|
|
70
70
|
name: "create-checkout-session",
|
|
@@ -79,10 +79,9 @@ export function createCheckoutSessionHandler(
|
|
|
79
79
|
const payload = event.payload as CreateCheckoutSessionPayload;
|
|
80
80
|
|
|
81
81
|
const mode = payload.mode ?? "subscription";
|
|
82
|
-
const consent =
|
|
83
|
-
consumerProtection
|
|
84
|
-
|
|
85
|
-
: undefined;
|
|
82
|
+
const consent = consumerProtection
|
|
83
|
+
? await prepareConsent(ctx, consumerProtection, payload.consent, mode)
|
|
84
|
+
: undefined;
|
|
86
85
|
|
|
87
86
|
const result = await openCheckout(
|
|
88
87
|
ctx,
|
package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts
CHANGED
|
@@ -35,7 +35,9 @@ export function createStartPlanCheckoutHandler(
|
|
|
35
35
|
const consumerProtection = options.consumerProtection;
|
|
36
36
|
const tierSchema = z.object({ tier: z.enum(plans) });
|
|
37
37
|
const schema = (
|
|
38
|
-
consumerProtection
|
|
38
|
+
consumerProtection
|
|
39
|
+
? tierSchema.extend({ consent: consentPayloadSchema.optional() })
|
|
40
|
+
: tierSchema
|
|
39
41
|
).strict();
|
|
40
42
|
|
|
41
43
|
return {
|
|
@@ -65,10 +67,9 @@ export function createStartPlanCheckoutHandler(
|
|
|
65
67
|
});
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
const consent =
|
|
69
|
-
consumerProtection
|
|
70
|
-
|
|
71
|
-
: undefined;
|
|
70
|
+
const consent = consumerProtection
|
|
71
|
+
? await prepareConsent(ctx, consumerProtection, payload.consent, "subscription")
|
|
72
|
+
: undefined;
|
|
72
73
|
|
|
73
74
|
const baseUrl = options.baseUrl ?? "";
|
|
74
75
|
const result = await openCheckout(
|
|
@@ -124,6 +124,9 @@ export const BILLING_FOUNDATION_I18N: Readonly<Record<string, LocalizedString>>
|
|
|
124
124
|
"billing-foundation.errors.priceUnavailable": {
|
|
125
125
|
en: "This plan's price is temporarily unavailable.",
|
|
126
126
|
},
|
|
127
|
+
"billing-foundation.errors.consentRequired": {
|
|
128
|
+
en: "Confirm both consent boxes before you continue.",
|
|
129
|
+
},
|
|
127
130
|
"billing-foundation.errors.consentTextOutdated": {
|
|
128
131
|
en: "The consent text has changed. Reload the page and confirm again.",
|
|
129
132
|
},
|
|
@@ -26,7 +26,7 @@ export const PAYMENT_TENANT_DESTROY_ARCHIVE_REASON =
|
|
|
26
26
|
export async function subscriptionTenantDestroyHook(ctx: TenantDataHookCtx): Promise<void> {
|
|
27
27
|
declareEscapeHatch({ reason: SUBSCRIPTION_TENANT_DESTROY_ARCHIVE_REASON });
|
|
28
28
|
const { profile } = await resolveProfileForTenant({
|
|
29
|
-
db: ctx.db.unsafeRaw(
|
|
29
|
+
db: ctx.db.unsafeRaw(),
|
|
30
30
|
tenantId: ctx.tenantId,
|
|
31
31
|
});
|
|
32
32
|
const aggregateId = subscriptionAggregateId(ctx.tenantId);
|
|
@@ -39,7 +39,7 @@ export async function subscriptionTenantDestroyHook(ctx: TenantDataHookCtx): Pro
|
|
|
39
39
|
} else {
|
|
40
40
|
await ctx.db.deleteMany(subscriptionsProjectionTable as EntityTableMeta, { id: aggregateId });
|
|
41
41
|
}
|
|
42
|
-
await archiveStream(ctx.db.unsafeRaw(
|
|
42
|
+
await archiveStream(ctx.db.unsafeRaw(), {
|
|
43
43
|
tenantId: ctx.tenantId,
|
|
44
44
|
aggregateId,
|
|
45
45
|
aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
|
|
@@ -56,7 +56,7 @@ export async function subscriptionTenantDestroyHook(ctx: TenantDataHookCtx): Pro
|
|
|
56
56
|
export async function paymentTenantDestroyHook(ctx: TenantDataHookCtx): Promise<void> {
|
|
57
57
|
declareEscapeHatch({ reason: PAYMENT_TENANT_DESTROY_ARCHIVE_REASON });
|
|
58
58
|
const { profile } = await resolveProfileForTenant({
|
|
59
|
-
db: ctx.db.unsafeRaw(
|
|
59
|
+
db: ctx.db.unsafeRaw(),
|
|
60
60
|
tenantId: ctx.tenantId,
|
|
61
61
|
});
|
|
62
62
|
const aggregateId = paymentAggregateId(ctx.tenantId);
|
|
@@ -71,7 +71,7 @@ export async function paymentTenantDestroyHook(ctx: TenantDataHookCtx): Promise<
|
|
|
71
71
|
tenantId: ctx.tenantId,
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
|
-
await archiveStream(ctx.db.unsafeRaw(
|
|
74
|
+
await archiveStream(ctx.db.unsafeRaw(), {
|
|
75
75
|
tenantId: ctx.tenantId,
|
|
76
76
|
aggregateId,
|
|
77
77
|
aggregateType: PAYMENT_AGGREGATE_TYPE,
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
type SubscriptionCancelTiming,
|
|
36
36
|
type SubscriptionEventType,
|
|
37
37
|
type SubscriptionStatus,
|
|
38
|
+
type TerminationScope,
|
|
38
39
|
} from "./constants.js";
|
|
39
40
|
|
|
40
41
|
// =============================================================================
|
|
@@ -389,6 +390,11 @@ export type ConsumerProtectionOptions = {
|
|
|
389
390
|
/** Per-locale VAT note; must carry at least `de` and `en`. */
|
|
390
391
|
readonly vatNote: Readonly<Record<string, string>>;
|
|
391
392
|
readonly operatorEmail: string;
|
|
393
|
+
/** Where the public § 312k declaration is reachable. `"tenant-host"`
|
|
394
|
+
* (default): on a host that resolves a tenant. `"platform"`: also on a
|
|
395
|
+
* host that resolves none (the platform apex) — the declaration then runs
|
|
396
|
+
* without a request tenant and finds the contract by the declarant's email. */
|
|
397
|
+
readonly terminationScope?: TerminationScope;
|
|
392
398
|
/** Root-relative paths or absolute https URLs. */
|
|
393
399
|
readonly legalLinks: {
|
|
394
400
|
readonly terms: string;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// re-exported from index.ts.
|
|
5
5
|
|
|
6
6
|
import * as z from "zod";
|
|
7
|
+
import { isTerminationScope, TERMINATION_SCOPES } from "./constants.js";
|
|
7
8
|
import type { BillingFoundationOptions, ConsumerProtectionOptions } from "./types.js";
|
|
8
9
|
|
|
9
10
|
function isRootRelativePath(path: string): boolean {
|
|
@@ -35,6 +36,11 @@ function validateConsumerProtection(cp: ConsumerProtectionOptions): void {
|
|
|
35
36
|
`createBillingFoundationFeature: consumerProtection.operatorEmail "${cp.operatorEmail}" is not a valid email address.`,
|
|
36
37
|
);
|
|
37
38
|
}
|
|
39
|
+
if (cp.terminationScope !== undefined && !isTerminationScope(cp.terminationScope)) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`createBillingFoundationFeature: consumerProtection.terminationScope "${String(cp.terminationScope)}" must be one of ${TERMINATION_SCOPES.map((scope) => `"${scope}"`).join(", ")}.`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
38
44
|
for (const [key, link] of Object.entries(cp.legalLinks)) {
|
|
39
45
|
if (!isRootRelativeOrHttpsUrl(link)) {
|
|
40
46
|
throw new Error(
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ChatWebhookChannelOptions,
|
|
3
|
+
chatMessageText,
|
|
4
|
+
createChatWebhookChannel,
|
|
5
|
+
type DeliveryChannel,
|
|
6
|
+
truncateChars,
|
|
7
|
+
} from "../delivery/index.js";
|
|
8
|
+
|
|
9
|
+
export type DiscordChannelOptions = ChatWebhookChannelOptions;
|
|
10
|
+
|
|
11
|
+
export const DISCORD_DEFAULT_ALLOWED_HOSTS = ["discord.com", "discordapp.com"] as const;
|
|
12
|
+
export const DISCORD_WEBHOOK_PATH_PREFIX = "/api/webhooks/";
|
|
13
|
+
export const DISCORD_CONTENT_MAX_CHARS = 2000;
|
|
14
|
+
|
|
15
|
+
export function createDiscordChannel(
|
|
16
|
+
options: DiscordChannelOptions,
|
|
17
|
+
keyFor: (connection: string) => string,
|
|
18
|
+
): DeliveryChannel {
|
|
19
|
+
return createChatWebhookChannel({
|
|
20
|
+
name: "discord",
|
|
21
|
+
featureName: "channel-discord",
|
|
22
|
+
defaultAllowedHosts: DISCORD_DEFAULT_ALLOWED_HOSTS,
|
|
23
|
+
requiredPathPrefix: DISCORD_WEBHOOK_PATH_PREFIX,
|
|
24
|
+
keyFor,
|
|
25
|
+
// parse: [] keeps @everyone / @here / role and user mentions in the text inert.
|
|
26
|
+
buildBody: (message) => ({
|
|
27
|
+
content: truncateChars(chatMessageText(message), DISCORD_CONTENT_MAX_CHARS),
|
|
28
|
+
allowed_mentions: { parse: [] },
|
|
29
|
+
}),
|
|
30
|
+
options,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chatConnectionNameSchema,
|
|
3
|
+
DELIVERY_CHANNEL_EXTENSION,
|
|
4
|
+
} from "@cosmicdrift/kumiko-bundled-features/delivery";
|
|
5
|
+
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
6
|
+
import { createDiscordChannel, type DiscordChannelOptions } from "./discord-channel.js";
|
|
7
|
+
|
|
8
|
+
export function createChannelDiscordFeature(
|
|
9
|
+
options: DiscordChannelOptions = {},
|
|
10
|
+
): FeatureDefinition {
|
|
11
|
+
return defineFeature("channel-discord", (r) => {
|
|
12
|
+
r.describe(
|
|
13
|
+
'Posts delivery notifications to a Discord webhook, registered as the `discord` channel in the delivery system. Tenant-owned chat target: reach it only via `ctx.notify(type, { route: { discord: "<connection name>" } })`. The webhook URL is a tenant secret under `channel-discord:webhooks.<connection name>`; only the connection name is logged. Mentions are disabled and content is cut to 2000 characters. Requires `delivery` and `secrets`. Host allowlist (discord.com, discordapp.com, path /api/webhooks/), https-only and the request timeout are app options of `createChannelDiscordFeature(opts)`, never tenant config.',
|
|
14
|
+
);
|
|
15
|
+
r.uiHints({
|
|
16
|
+
displayLabel: "Discord Channel",
|
|
17
|
+
category: "notifications",
|
|
18
|
+
recommended: false,
|
|
19
|
+
});
|
|
20
|
+
r.requires("delivery");
|
|
21
|
+
r.requires("secrets");
|
|
22
|
+
|
|
23
|
+
const webhooks = r.secretNamespace("webhooks", {
|
|
24
|
+
label: { en: "Discord webhooks", de: "Discord-Webhooks", es: "Webhooks de Discord" },
|
|
25
|
+
hint: {
|
|
26
|
+
en: 'One secret per connection name; the value is the Discord webhook URL. Address it with route: { discord: "<connection name>" }.',
|
|
27
|
+
de: 'Ein Secret pro Verbindungsname; der Wert ist die Discord-Webhook-URL. Adressierung mit route: { discord: "<Verbindungsname>" }.',
|
|
28
|
+
es: 'Un secreto por nombre de conexión; el valor es la URL del webhook de Discord. Se usa con route: { discord: "<nombre de conexión>" }.',
|
|
29
|
+
},
|
|
30
|
+
scope: "tenant",
|
|
31
|
+
nameSchema: chatConnectionNameSchema,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const channel = createDiscordChannel(options, webhooks.keyFor);
|
|
35
|
+
r.useExtension(DELIVERY_CHANNEL_EXTENSION, "discord", {
|
|
36
|
+
mode: channel.mode,
|
|
37
|
+
send: channel.send,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chatConnectionNameSchema,
|
|
3
|
+
DELIVERY_CHANNEL_EXTENSION,
|
|
4
|
+
} from "@cosmicdrift/kumiko-bundled-features/delivery";
|
|
5
|
+
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
6
|
+
import { createSlackChannel, type SlackChannelOptions } from "./slack-channel.js";
|
|
7
|
+
|
|
8
|
+
export function createChannelSlackFeature(options: SlackChannelOptions = {}): FeatureDefinition {
|
|
9
|
+
return defineFeature("channel-slack", (r) => {
|
|
10
|
+
r.describe(
|
|
11
|
+
'Posts delivery notifications to a Slack incoming webhook, registered as the `slack` channel in the delivery system. Tenant-owned chat target: reach it only via `ctx.notify(type, { route: { slack: "<connection name>" } })`. The webhook URL is a tenant secret under `channel-slack:webhooks.<connection name>`; only the connection name is logged. Requires `delivery` and `secrets`. Host allowlist (hooks.slack.com), https-only and the request timeout are app options of `createChannelSlackFeature(opts)`, never tenant config.',
|
|
12
|
+
);
|
|
13
|
+
r.uiHints({
|
|
14
|
+
displayLabel: "Slack Channel",
|
|
15
|
+
category: "notifications",
|
|
16
|
+
recommended: false,
|
|
17
|
+
});
|
|
18
|
+
r.requires("delivery");
|
|
19
|
+
r.requires("secrets");
|
|
20
|
+
|
|
21
|
+
const webhooks = r.secretNamespace("webhooks", {
|
|
22
|
+
label: { en: "Slack webhooks", de: "Slack-Webhooks", es: "Webhooks de Slack" },
|
|
23
|
+
hint: {
|
|
24
|
+
en: 'One secret per connection name; the value is the Slack webhook URL. Address it with route: { slack: "<connection name>" }.',
|
|
25
|
+
de: 'Ein Secret pro Verbindungsname; der Wert ist die Slack-Webhook-URL. Adressierung mit route: { slack: "<Verbindungsname>" }.',
|
|
26
|
+
es: 'Un secreto por nombre de conexión; el valor es la URL del webhook de Slack. Se usa con route: { slack: "<nombre de conexión>" }.',
|
|
27
|
+
},
|
|
28
|
+
scope: "tenant",
|
|
29
|
+
nameSchema: chatConnectionNameSchema,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const channel = createSlackChannel(options, webhooks.keyFor);
|
|
33
|
+
r.useExtension(DELIVERY_CHANNEL_EXTENSION, "slack", {
|
|
34
|
+
mode: channel.mode,
|
|
35
|
+
send: channel.send,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ChatWebhookChannelOptions,
|
|
3
|
+
chatMessageText,
|
|
4
|
+
createChatWebhookChannel,
|
|
5
|
+
type DeliveryChannel,
|
|
6
|
+
} from "../delivery/index.js";
|
|
7
|
+
|
|
8
|
+
export type SlackChannelOptions = ChatWebhookChannelOptions;
|
|
9
|
+
|
|
10
|
+
export const SLACK_DEFAULT_ALLOWED_HOSTS = ["hooks.slack.com"] as const;
|
|
11
|
+
|
|
12
|
+
// Slack mrkdwn treats & < > as control characters (links, @-mentions via <!channel>).
|
|
13
|
+
export function escapeSlackText(text: string): string {
|
|
14
|
+
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createSlackChannel(
|
|
18
|
+
options: SlackChannelOptions,
|
|
19
|
+
keyFor: (connection: string) => string,
|
|
20
|
+
): DeliveryChannel {
|
|
21
|
+
return createChatWebhookChannel({
|
|
22
|
+
name: "slack",
|
|
23
|
+
featureName: "channel-slack",
|
|
24
|
+
defaultAllowedHosts: SLACK_DEFAULT_ALLOWED_HOSTS,
|
|
25
|
+
keyFor,
|
|
26
|
+
buildBody: (message) => ({ text: escapeSlackText(chatMessageText(message)) }),
|
|
27
|
+
options,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chatConnectionNameSchema,
|
|
3
|
+
DELIVERY_CHANNEL_EXTENSION,
|
|
4
|
+
} from "@cosmicdrift/kumiko-bundled-features/delivery";
|
|
5
|
+
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
6
|
+
import { createTeamsChannel, type TeamsChannelOptions } from "./teams-channel.js";
|
|
7
|
+
|
|
8
|
+
export function createChannelTeamsFeature(options: TeamsChannelOptions = {}): FeatureDefinition {
|
|
9
|
+
return defineFeature("channel-teams", (r) => {
|
|
10
|
+
r.describe(
|
|
11
|
+
'Posts delivery notifications to a Microsoft Teams webhook (Workflows or connector) as an Adaptive Card, registered as the `teams` channel in the delivery system. Tenant-owned chat target: reach it only via `ctx.notify(type, { route: { teams: "<connection name>" } })`. The webhook URL is a tenant secret under `channel-teams:webhooks.<connection name>`; only the connection name is logged. Requires `delivery` and `secrets`. Host allowlist (.webhook.office.com, .logic.azure.com, .powerplatform.com), https-only and the request timeout are app options of `createChannelTeamsFeature(opts)`, never tenant config.',
|
|
12
|
+
);
|
|
13
|
+
r.uiHints({
|
|
14
|
+
displayLabel: "Microsoft Teams Channel",
|
|
15
|
+
category: "notifications",
|
|
16
|
+
recommended: false,
|
|
17
|
+
});
|
|
18
|
+
r.requires("delivery");
|
|
19
|
+
r.requires("secrets");
|
|
20
|
+
|
|
21
|
+
const webhooks = r.secretNamespace("webhooks", {
|
|
22
|
+
label: {
|
|
23
|
+
en: "Microsoft Teams webhooks",
|
|
24
|
+
de: "Microsoft Teams-Webhooks",
|
|
25
|
+
es: "Webhooks de Microsoft Teams",
|
|
26
|
+
},
|
|
27
|
+
hint: {
|
|
28
|
+
en: 'One secret per connection name; the value is the Microsoft Teams webhook URL. Address it with route: { teams: "<connection name>" }.',
|
|
29
|
+
de: 'Ein Secret pro Verbindungsname; der Wert ist die Microsoft Teams-Webhook-URL. Adressierung mit route: { teams: "<Verbindungsname>" }.',
|
|
30
|
+
es: 'Un secreto por nombre de conexión; el valor es la URL del webhook de Microsoft Teams. Se usa con route: { teams: "<nombre de conexión>" }.',
|
|
31
|
+
},
|
|
32
|
+
scope: "tenant",
|
|
33
|
+
nameSchema: chatConnectionNameSchema,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const channel = createTeamsChannel(options, webhooks.keyFor);
|
|
37
|
+
r.useExtension(DELIVERY_CHANNEL_EXTENSION, "teams", {
|
|
38
|
+
mode: channel.mode,
|
|
39
|
+
send: channel.send,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ChatWebhookChannelOptions,
|
|
3
|
+
createChatWebhookChannel,
|
|
4
|
+
type DeliveryChannel,
|
|
5
|
+
} from "../delivery/index.js";
|
|
6
|
+
|
|
7
|
+
export type TeamsChannelOptions = ChatWebhookChannelOptions;
|
|
8
|
+
|
|
9
|
+
export const TEAMS_DEFAULT_ALLOWED_HOSTS = [
|
|
10
|
+
".webhook.office.com",
|
|
11
|
+
".logic.azure.com",
|
|
12
|
+
".powerplatform.com",
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
// Text only ever goes into TextBlocks (no markup interpretation of titles/bodies
|
|
16
|
+
// beyond Adaptive Card's own text rendering).
|
|
17
|
+
export function buildTeamsAdaptiveCard(title: string, body: string | undefined): unknown {
|
|
18
|
+
return {
|
|
19
|
+
type: "message",
|
|
20
|
+
attachments: [
|
|
21
|
+
{
|
|
22
|
+
contentType: "application/vnd.microsoft.card.adaptive",
|
|
23
|
+
contentUrl: null,
|
|
24
|
+
content: {
|
|
25
|
+
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
|
26
|
+
type: "AdaptiveCard",
|
|
27
|
+
version: "1.4",
|
|
28
|
+
body: [
|
|
29
|
+
{ type: "TextBlock", text: title, weight: "Bolder", wrap: true },
|
|
30
|
+
...(body ? [{ type: "TextBlock", text: body, wrap: true }] : []),
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createTeamsChannel(
|
|
39
|
+
options: TeamsChannelOptions,
|
|
40
|
+
keyFor: (connection: string) => string,
|
|
41
|
+
): DeliveryChannel {
|
|
42
|
+
return createChatWebhookChannel({
|
|
43
|
+
name: "teams",
|
|
44
|
+
featureName: "channel-teams",
|
|
45
|
+
defaultAllowedHosts: TEAMS_DEFAULT_ALLOWED_HOSTS,
|
|
46
|
+
keyFor,
|
|
47
|
+
buildBody: (message) => buildTeamsAdaptiveCard(message.title, message.body),
|
|
48
|
+
options,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { DELIVERY_CHANNEL_EXTENSION } from "@cosmicdrift/kumiko-bundled-features/delivery";
|
|
2
|
+
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { createTelegramChannel, type TelegramChannelOptions } from "./telegram-channel.js";
|
|
4
|
+
|
|
5
|
+
export function createChannelTelegramFeature(
|
|
6
|
+
options: TelegramChannelOptions = {},
|
|
7
|
+
): FeatureDefinition {
|
|
8
|
+
return defineFeature("channel-telegram", (r) => {
|
|
9
|
+
r.describe(
|
|
10
|
+
'Sends delivery notifications through the Telegram Bot API (`sendMessage`, plain text), registered as the `telegram` channel in the delivery system. Tenant-owned chat target: reach it only via `ctx.notify(type, { route: { telegram: "<chat id or @channel>" } })`. The bot token is the tenant secret `channel-telegram:secret:bot-token`; the token never reaches logs or delivery attempts. Requires `delivery` and `secrets`. The API base URL and request timeout are app options of `createChannelTelegramFeature(opts)`, never tenant config.',
|
|
11
|
+
);
|
|
12
|
+
r.uiHints({
|
|
13
|
+
displayLabel: "Telegram Channel",
|
|
14
|
+
category: "notifications",
|
|
15
|
+
recommended: false,
|
|
16
|
+
});
|
|
17
|
+
r.requires("delivery");
|
|
18
|
+
r.requires("secrets");
|
|
19
|
+
|
|
20
|
+
const botToken = r.secret("botToken", {
|
|
21
|
+
label: { en: "Telegram bot token" },
|
|
22
|
+
hint: {
|
|
23
|
+
en: 'Token from @BotFather. Used for every Telegram chat the tenant addresses via route: { telegram: "<chat id>" }.',
|
|
24
|
+
},
|
|
25
|
+
redact: () => "••••••••",
|
|
26
|
+
scope: "tenant",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const channel = createTelegramChannel(options, botToken.name);
|
|
30
|
+
r.useExtension(DELIVERY_CHANNEL_EXTENSION, "telegram", {
|
|
31
|
+
mode: channel.mode,
|
|
32
|
+
send: channel.send,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { SYSTEM_USER_ID } from "@cosmicdrift/kumiko-types/identifiers";
|
|
2
|
+
import {
|
|
3
|
+
chatMessageText,
|
|
4
|
+
DEFAULT_CHAT_TIMEOUT_MS,
|
|
5
|
+
type DeliveryChannel,
|
|
6
|
+
postChatWebhook,
|
|
7
|
+
toChannelResult,
|
|
8
|
+
truncateChars,
|
|
9
|
+
} from "../delivery/index.js";
|
|
10
|
+
|
|
11
|
+
export type TelegramChannelOptions = {
|
|
12
|
+
// App-level override (tests, self-hosted Bot API server); never tenant config.
|
|
13
|
+
readonly apiBaseUrl?: string;
|
|
14
|
+
readonly timeoutMs?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const TELEGRAM_DEFAULT_API_BASE_URL = "https://api.telegram.org";
|
|
18
|
+
export const TELEGRAM_TEXT_MAX_CHARS = 4096;
|
|
19
|
+
|
|
20
|
+
// Numeric chat/group id (groups and channels are negative) or a public @channelname.
|
|
21
|
+
const TELEGRAM_CHAT_ID_PATTERN = /^(-?\d{1,20}|@[A-Za-z][A-Za-z0-9_]{3,31})$/;
|
|
22
|
+
// `<bot id>:<secret>`; the token is spliced into the URL path, so only the real shape passes.
|
|
23
|
+
const TELEGRAM_BOT_TOKEN_PATTERN = /^\d{1,20}:[A-Za-z0-9_-]{10,}$/;
|
|
24
|
+
|
|
25
|
+
export function createTelegramChannel(
|
|
26
|
+
options: TelegramChannelOptions,
|
|
27
|
+
botTokenKey: string,
|
|
28
|
+
): DeliveryChannel {
|
|
29
|
+
const apiBaseUrl = options.apiBaseUrl ?? TELEGRAM_DEFAULT_API_BASE_URL;
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
name: "telegram",
|
|
33
|
+
mode: "queued",
|
|
34
|
+
|
|
35
|
+
async send(address, message, ctx) {
|
|
36
|
+
if (!TELEGRAM_CHAT_ID_PATTERN.test(address)) {
|
|
37
|
+
return toChannelResult(address, { ok: false, code: "invalid_address" });
|
|
38
|
+
}
|
|
39
|
+
let token: string | undefined;
|
|
40
|
+
try {
|
|
41
|
+
const revealed = await ctx.secrets?.get(ctx.tenantId, botTokenKey, {
|
|
42
|
+
userId: SYSTEM_USER_ID,
|
|
43
|
+
handlerName: "channel-telegram:send",
|
|
44
|
+
});
|
|
45
|
+
token = revealed?.reveal();
|
|
46
|
+
} catch {
|
|
47
|
+
token = undefined;
|
|
48
|
+
}
|
|
49
|
+
if (!token || !TELEGRAM_BOT_TOKEN_PATTERN.test(token)) {
|
|
50
|
+
return toChannelResult(address, { ok: false, code: "missing_credentials" });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The base URL is the allowlist: only its own host is reachable.
|
|
54
|
+
let baseHost: string;
|
|
55
|
+
try {
|
|
56
|
+
baseHost = new URL(apiBaseUrl).hostname;
|
|
57
|
+
} catch {
|
|
58
|
+
return toChannelResult(address, { ok: false, code: "host_not_allowed" });
|
|
59
|
+
}
|
|
60
|
+
const result = await postChatWebhook({
|
|
61
|
+
url: `${apiBaseUrl.replace(/\/+$/, "")}/bot${token}/sendMessage`,
|
|
62
|
+
allowedHosts: [baseHost],
|
|
63
|
+
requireHttps: apiBaseUrl.startsWith("https:"),
|
|
64
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_CHAT_TIMEOUT_MS,
|
|
65
|
+
// No parse_mode: Telegram renders the text literally.
|
|
66
|
+
body: {
|
|
67
|
+
chat_id: address,
|
|
68
|
+
text: truncateChars(chatMessageText(message), TELEGRAM_TEXT_MAX_CHARS),
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
return toChannelResult(address, result);
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -334,9 +334,7 @@ export const forgetSubjectWrite = defineWriteHandler({
|
|
|
334
334
|
const subjectKey = subjectIdToKey(subject);
|
|
335
335
|
|
|
336
336
|
const tenantScopeDenial = await resolveTenantScopeDenial(
|
|
337
|
-
ctx.db.unsafeRaw(
|
|
338
|
-
"tenant-scope check runs against the subject's tenant, not necessarily the caller's",
|
|
339
|
-
),
|
|
337
|
+
ctx.db.unsafeRaw(),
|
|
340
338
|
ctx.registry.features,
|
|
341
339
|
event.user,
|
|
342
340
|
raw,
|
|
@@ -348,9 +346,7 @@ export const forgetSubjectWrite = defineWriteHandler({
|
|
|
348
346
|
subjectKey,
|
|
349
347
|
raw.kind,
|
|
350
348
|
denialReasonOf(tenantScopeDenial),
|
|
351
|
-
ctx.dbOutsideTransaction?.unsafeRaw(
|
|
352
|
-
"denial audit append: names the prober's own tenant stream on the outside-transaction db",
|
|
353
|
-
),
|
|
349
|
+
ctx.dbOutsideTransaction?.unsafeRaw(),
|
|
354
350
|
);
|
|
355
351
|
return auditFailure ?? tenantScopeDenial;
|
|
356
352
|
}
|
|
@@ -358,9 +354,7 @@ export const forgetSubjectWrite = defineWriteHandler({
|
|
|
358
354
|
// Runs after the tenant gate but before any key is touched: a retention
|
|
359
355
|
// check ahead of the tenant gate would leak a foreign entity's retention
|
|
360
356
|
// posture to a cross-tenant prober.
|
|
361
|
-
const retentionDenial = await resolveRetentionDenial(ctx, raw, () =>
|
|
362
|
-
ctx.db.unsafeRaw("retention check reads the record row's owning tenant, not the caller's"),
|
|
363
|
-
);
|
|
357
|
+
const retentionDenial = await resolveRetentionDenial(ctx, raw, () => ctx.db.unsafeRaw());
|
|
364
358
|
if (retentionDenial) {
|
|
365
359
|
const auditFailure = await appendDenialAuditEvent(
|
|
366
360
|
ctx,
|
|
@@ -368,9 +362,7 @@ export const forgetSubjectWrite = defineWriteHandler({
|
|
|
368
362
|
subjectKey,
|
|
369
363
|
raw.kind,
|
|
370
364
|
TARGET_RECORD_RETENTION_BLOCK_DELETE,
|
|
371
|
-
ctx.dbOutsideTransaction?.unsafeRaw(
|
|
372
|
-
"denial audit append: names the prober's own tenant stream on the outside-transaction db",
|
|
373
|
-
),
|
|
365
|
+
ctx.dbOutsideTransaction?.unsafeRaw(),
|
|
374
366
|
);
|
|
375
367
|
return auditFailure ?? retentionDenial;
|
|
376
368
|
}
|
|
@@ -387,9 +379,7 @@ export const forgetSubjectWrite = defineWriteHandler({
|
|
|
387
379
|
|
|
388
380
|
// Blind-index sweep (#818): nulls bidx columns now so the deterministic
|
|
389
381
|
// HMAC doesn't stay equality-matchable; raw because the ciphertext prefix addresses the subject across tenants.
|
|
390
|
-
const crossTenantSubjectRunner = ctx.db.unsafeRaw(
|
|
391
|
-
"blind-index sweep and search purge address the subject across tenants",
|
|
392
|
-
);
|
|
382
|
+
const crossTenantSubjectRunner = ctx.db.unsafeRaw();
|
|
393
383
|
await nullBlindIndexesForSubject(crossTenantSubjectRunner, ctx.registry.features, subjectKey);
|
|
394
384
|
|
|
395
385
|
// Derived search index still holds plaintext (#1610) — purge next to the
|
|
@@ -425,9 +415,7 @@ export const forgetSubjectWrite = defineWriteHandler({
|
|
|
425
415
|
// update is best-effort for real users.
|
|
426
416
|
if (raw.kind === "user" && ctx.registry.features.has("user")) {
|
|
427
417
|
try {
|
|
428
|
-
const userLifecycleRunner = ctx.db.unsafeRaw(
|
|
429
|
-
"user lifecycle update and PAT revoke run on the SYSTEM user stream",
|
|
430
|
-
);
|
|
418
|
+
const userLifecycleRunner = ctx.db.unsafeRaw();
|
|
431
419
|
await updateUserLifecycle(userLifecycleRunner, raw.userId, { status: USER_STATUS.Deleted });
|
|
432
420
|
if (ctx.registry.features.has("personal-access-tokens")) {
|
|
433
421
|
await revokeAllPatTokensForUser(userLifecycleRunner, raw.userId);
|
|
@@ -40,7 +40,7 @@ export const clearCustomFieldHandler: WriteHandlerDef = {
|
|
|
40
40
|
const payload = event.payload as ClearCustomFieldPayload; // @cast-boundary engine-payload
|
|
41
41
|
|
|
42
42
|
const accessCheck = await checkFieldAccessForWrite(
|
|
43
|
-
ctx.db.unsafeRaw(
|
|
43
|
+
ctx.db.unsafeRaw(),
|
|
44
44
|
event.user.tenantId,
|
|
45
45
|
payload.entityName,
|
|
46
46
|
payload.fieldKey,
|
|
@@ -74,10 +74,7 @@ export function createDefineTenantFieldHandler(
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
if (limit !== undefined) {
|
|
77
|
-
const current = await countTenantFieldDefinitions(
|
|
78
|
-
ctx.db.unsafeRaw(DEFINE_TENANT_FIELD_REASON),
|
|
79
|
-
tenantId,
|
|
80
|
-
);
|
|
77
|
+
const current = await countTenantFieldDefinitions(ctx.db.unsafeRaw(), tenantId);
|
|
81
78
|
if (current >= limit) {
|
|
82
79
|
return failUnprocessable("cap_exceeded", {
|
|
83
80
|
capName: "customFields.fieldDefinition.count",
|
|
@@ -63,7 +63,7 @@ export const setCustomFieldHandler: WriteHandlerDef = {
|
|
|
63
63
|
const payload = event.payload as SetCustomFieldPayload; // @cast-boundary engine-payload
|
|
64
64
|
|
|
65
65
|
const loaded = await loadFieldDefinition(
|
|
66
|
-
ctx.db.unsafeRaw(
|
|
66
|
+
ctx.db.unsafeRaw(),
|
|
67
67
|
event.user.tenantId,
|
|
68
68
|
payload.entityName,
|
|
69
69
|
payload.fieldKey,
|
|
@@ -73,7 +73,7 @@ export function wireCustomFieldsUserDataRightsFor<TReg extends FeatureRegistrar<
|
|
|
73
73
|
// biome-ignore lint/correctness/useHookAtTopLevel: r.useExtension is a registrar API, not a React hook.
|
|
74
74
|
r.useExtension(EXT_USER_DATA, opts.entityName, {
|
|
75
75
|
export: async (ctx: UserDataHookCtx) =>
|
|
76
|
-
exportCustomFields(ctx.db.unsafeRaw(
|
|
76
|
+
exportCustomFields(ctx.db.unsafeRaw(), ctx, tableName, opts),
|
|
77
77
|
escapeHatch: { reason: CUSTOM_FIELDS_EXPORT_REASON },
|
|
78
78
|
});
|
|
79
79
|
}
|