@cosmicdrift/kumiko-bundled-features 0.352.0 → 0.354.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/agent-doc-lint.d.ts +2 -0
- package/dist/agent-tools/agent-doc-lint.js +61 -1
- package/dist/agent-tools/agent-manifest.d.ts +1 -0
- package/dist/agent-tools/agent-manifest.js +17 -5
- package/dist/agent-tools/feature.js +7 -2
- package/dist/agent-tools/index.d.ts +1 -1
- package/dist/agent-tools/index.js +1 -1
- package/dist/agent-tools/manifest-deny-qns.d.ts +3 -0
- package/dist/agent-tools/manifest-deny-qns.js +17 -0
- package/dist/agent-tools/tool-catalog.d.ts +2 -1
- package/dist/agent-tools/tool-catalog.js +4 -1
- package/dist/agent-tools/tool-dispatch.js +1 -1
- package/dist/audit/handlers/list.query.js +13 -8
- package/dist/auth-email-password/handlers/change-password.write.js +1 -0
- 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 -2
- package/dist/auth-email-password/handlers/invite-create.write.js +17 -0
- package/dist/auth-email-password/handlers/invite-signup-complete.write.js +4 -1
- package/dist/auth-email-password/handlers/login.write.js +1 -0
- package/dist/auth-email-password/handlers/reset-password.write.js +1 -0
- package/dist/auth-email-password/handlers/signup-confirm.write.js +6 -4
- package/dist/auth-email-password/handlers/signup-request.write.js +51 -77
- package/dist/auth-email-password/handlers/token-request-handler.js +1 -0
- package/dist/auth-email-password/handlers/verify-email.write.js +1 -0
- package/dist/auth-email-password/index.d.ts +2 -0
- package/dist/auth-email-password/index.js +2 -0
- package/dist/auth-email-password/seeding.d.ts +2 -0
- package/dist/auth-email-password/seeding.js +30 -8
- package/dist/auth-email-password/signup-activation.d.ts +24 -0
- package/dist/auth-email-password/signup-activation.js +41 -0
- package/dist/auth-email-password/signup-token-store.d.ts +5 -0
- package/dist/auth-email-password/signup-token-store.js +8 -3
- package/dist/auth-email-password/web/auth-gate.d.ts +2 -2
- package/dist/auth-email-password/web/auth-gate.js +5 -5
- package/dist/auth-email-password/web/auth-redirect.d.ts +1 -0
- package/dist/auth-email-password/web/auth-redirect.js +14 -0
- package/dist/auth-email-password/web/login-screen.js +2 -2
- package/dist/auth-email-password/web/user-menu.js +1 -1
- package/dist/auth-mfa/feature.js +5 -0
- package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +1 -0
- package/dist/auth-mfa/handlers/verify.write.js +1 -0
- package/dist/auth-mfa/schema/user-mfa.d.ts +6 -0
- package/dist/auth-mfa/screens.js +2 -2
- package/dist/auth-mfa/web/mfa-recovery-codes-reveal.js +1 -1
- package/dist/auth-mfa/web/mfa-setup-preauth-screen.d.ts +1 -1
- package/dist/auth-mfa/web/mfa-setup-preauth-screen.js +3 -3
- package/dist/auth-mfa/web/mfa-verify-screen.d.ts +1 -1
- package/dist/auth-mfa/web/mfa-verify-screen.js +1 -1
- package/dist/billing-foundation/billing-info-query.d.ts +5 -2
- package/dist/billing-foundation/checkout-core.d.ts +7 -0
- package/dist/billing-foundation/checkout-core.js +7 -2
- package/dist/billing-foundation/entities.d.ts +8 -0
- package/dist/billing-foundation/handlers/start-plan-checkout.write.js +7 -1
- package/dist/billing-foundation/projection.d.ts +8 -0
- package/dist/cap-counter/entity.d.ts +1 -0
- package/dist/cap-overview/access.js +2 -9
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +2 -2
- package/dist/cap-overview/handlers/tenant-options.query.js +4 -5
- package/dist/channel-in-app/in-app-channel.js +1 -1
- package/dist/channel-in-app/tables.js +4 -2
- package/dist/compliance-profiles/schema/profile-selection.d.ts +10 -8
- package/dist/compliance-profiles-ops/handlers/tenants-missing-profile.query.js +2 -4
- package/dist/config/handlers/values.query.d.ts +3 -1
- package/dist/config/handlers/values.query.js +7 -2
- package/dist/config/table.d.ts +6 -0
- package/dist/crypto-shredding/handlers/forget-subject.write.d.ts +1 -0
- package/dist/crypto-shredding/handlers/forget-subject.write.js +110 -61
- package/dist/custom-fields/entity.d.ts +4 -0
- package/dist/data-retention/schema/tenant-retention-override.d.ts +4 -0
- package/dist/delivery/db/queries/address-opt-outs.d.ts +3 -1
- package/dist/delivery/db/queries/address-opt-outs.js +12 -7
- package/dist/delivery/delivery-service.js +60 -12
- package/dist/delivery/public-names.js +1 -1
- package/dist/delivery/tables.d.ts +6 -0
- package/dist/delivery/unsubscribe.js +66 -14
- package/dist/document-ingest-foundation/entity.d.ts +4 -0
- package/dist/feature-toggles/feature.js +2 -3
- package/dist/feature-toggles/toggle-runtime.d.ts +5 -1
- package/dist/feature-toggles/toggle-runtime.js +30 -6
- package/dist/feature-toggles/toggle-sync-signal.d.ts +3 -2
- package/dist/feature-toggles/toggle-sync-signal.js +29 -10
- package/dist/file-derivatives/handlers/public-variant.query.d.ts +1 -0
- package/dist/file-derivatives/handlers/public-variant.query.js +7 -1
- package/dist/files-tenant-data/handlers/sweep-orphaned-derivatives.job.js +5 -0
- package/dist/files-tenant-data/hooks.js +61 -24
- package/dist/folders/entity.d.ts +8 -0
- package/dist/folders/feature.js +2 -10
- package/dist/folders/handlers/delete-folder.write.js +31 -4
- package/dist/form-draft/db/queries/cleanup.d.ts +1 -1
- package/dist/form-draft/db/queries/cleanup.js +4 -3
- package/dist/form-draft/entity.d.ts +2 -0
- package/dist/form-draft/handlers/cleanup.job.d.ts +5 -1
- package/dist/form-draft/handlers/cleanup.job.js +11 -5
- package/dist/foundation-shared/index.d.ts +1 -1
- package/dist/foundation-shared/index.js +1 -1
- package/dist/foundation-shared/mail-host-policy.d.ts +4 -0
- package/dist/foundation-shared/mail-host-policy.js +13 -4
- package/dist/inbound-mail-foundation/entities.d.ts +25 -0
- package/dist/inbound-mail-foundation/projection.d.ts +20 -0
- package/dist/inbound-provider-imap/feature.js +2 -0
- package/dist/inbound-provider-imap/imap-client.js +3 -4
- package/dist/jobs/constants.d.ts +1 -0
- package/dist/jobs/constants.js +1 -0
- package/dist/jobs/db/queries/retention.d.ts +1 -0
- package/dist/jobs/db/queries/retention.js +5 -0
- package/dist/jobs/db/queries/stale-run-sweep.d.ts +2 -0
- package/dist/jobs/db/queries/stale-run-sweep.js +13 -2
- package/dist/jobs/feature.js +7 -1
- package/dist/jobs/handlers/list.query.d.ts +2 -1
- package/dist/jobs/handlers/list.query.js +40 -15
- package/dist/jobs/handlers/tenant-failures.query.js +1 -1
- package/dist/jobs/handlers/tenant-runs.query.d.ts +17 -0
- package/dist/jobs/handlers/tenant-runs.query.js +72 -0
- package/dist/jobs/i18n.js +1 -0
- package/dist/jobs/index.d.ts +2 -0
- package/dist/jobs/index.js +1 -0
- package/dist/jobs/job-run-logger.d.ts +2 -2
- package/dist/jobs/job-run-logger.js +98 -9
- package/dist/jobs/job-run-table.d.ts +6 -0
- package/dist/jobs/tenant-job-failure-table.js +15 -2
- package/dist/jobs/tenant-job-run-table.d.ts +4 -0
- package/dist/jobs/tenant-job-run-table.js +34 -0
- package/dist/ledger/entity.d.ts +24 -8
- package/dist/ledger/entity.js +17 -5
- package/dist/ledger/feature.js +3 -0
- package/dist/ledger/handlers/confirm-schedule-period.write.js +9 -3
- package/dist/ledger/handlers/create-transaction.write.js +1 -0
- package/dist/ledger/handlers/reverse-transaction.write.js +1 -0
- package/dist/ledger/schedule-subject-pair.d.ts +2 -0
- package/dist/ledger/schedule-subject-pair.js +24 -0
- package/dist/ledger/schemas.js +8 -2
- package/dist/mail-transport-smtp/feature.d.ts +3 -4
- package/dist/mail-transport-smtp/feature.js +4 -13
- package/dist/managed-pages/feature.js +28 -10
- package/dist/managed-pages/table.d.ts +12 -0
- package/dist/notes-history/entity.d.ts +12 -0
- package/dist/notes-history/entity.js +4 -0
- package/dist/notes-history/feature.js +2 -10
- package/dist/personal-access-tokens/handlers/create.write.js +1 -0
- package/dist/personal-access-tokens/schema/api-token.d.ts +6 -0
- package/dist/personal-access-tokens/screens.js +14 -3
- package/dist/secrets/table.d.ts +1 -0
- package/dist/seo/feature.js +28 -6
- package/dist/sessions/feature.d.ts +1 -0
- package/dist/sessions/feature.js +9 -8
- package/dist/sessions/handlers/detail.query.d.ts +1 -1
- package/dist/sessions/handlers/detail.query.js +2 -2
- package/dist/sessions/handlers/list.query.d.ts +1 -1
- package/dist/sessions/handlers/list.query.js +5 -3
- package/dist/sessions/schema/user-session.d.ts +4 -0
- package/dist/sessions/screens.d.ts +2 -2
- package/dist/sessions/screens.js +6 -7
- package/dist/shared/assert-no-where-rule-in-ownership-write.d.ts +2 -0
- package/dist/shared/assert-no-where-rule-in-ownership-write.js +18 -0
- package/dist/shared/encrypt-for-direct-write.js +10 -2
- package/dist/shared/index.d.ts +4 -3
- package/dist/shared/index.js +4 -3
- package/dist/shared/parent-visibility.d.ts +2 -1
- package/dist/shared/parent-visibility.js +21 -0
- package/dist/shared/rate-limited-text-response.d.ts +2 -0
- package/dist/shared/rate-limited-text-response.js +9 -0
- package/dist/shared/row-bound-grant.js +16 -2
- package/dist/shared/session-locale-field.d.ts +1 -0
- package/dist/shared/session-locale-field.js +6 -0
- package/dist/tags/entity.d.ts +9 -0
- package/dist/tags/feature.js +2 -10
- package/dist/template-resolver/api.d.ts +2 -2
- package/dist/template-resolver/handlers/list.query.d.ts +2 -0
- package/dist/template-resolver/handlers/list.query.js +2 -2
- package/dist/template-resolver/table.d.ts +48 -30
- package/dist/template-resolver/user-content-table.d.ts +34 -20
- package/dist/tenant/decrypt-tenant-names.d.ts +3 -0
- package/dist/tenant/decrypt-tenant-names.js +10 -0
- package/dist/tenant/feature.js +7 -1
- package/dist/tenant/handlers/me.query.js +5 -1
- package/dist/tenant/handlers/memberships.query.js +3 -2
- package/dist/tenant/handlers/tenant-directory.query.js +9 -3
- package/dist/tenant/handlers/toggle-enabled.write.js +2 -0
- package/dist/tenant/handlers/update-member-roles.write.js +1 -0
- package/dist/tenant/index.d.ts +1 -0
- package/dist/tenant/index.js +1 -0
- package/dist/tenant/invitation-table.d.ts +54 -8
- package/dist/tenant/invitation-table.js +6 -1
- package/dist/tenant/invited-membership.d.ts +8 -6
- package/dist/tenant/invited-membership.js +28 -7
- package/dist/tenant/membership-table.d.ts +4 -0
- package/dist/tenant/schema/tenant.d.ts +18 -10
- package/dist/tenant/schema/tenant.js +4 -6
- package/dist/tenant/screens.js +1 -1
- package/dist/tenant-handover/move-entity-graph.js +11 -18
- package/dist/tenant-handover/transfer-graph.d.ts +2 -21
- package/dist/tenant-handover/transfer-graph.js +8 -75
- package/dist/tenant-lifecycle/boot-checks.d.ts +1 -0
- package/dist/tenant-lifecycle/boot-checks.js +13 -0
- package/dist/tenant-lifecycle/constants.d.ts +3 -0
- package/dist/tenant-lifecycle/constants.js +5 -0
- package/dist/tenant-lifecycle/events.d.ts +5 -0
- package/dist/tenant-lifecycle/events.js +5 -0
- package/dist/tenant-lifecycle/feature.js +6 -3
- package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +10 -5
- package/dist/tenant-lifecycle/run-tenant-destroy.js +59 -17
- package/dist/tenant-lifecycle/stages.d.ts +8 -1
- package/dist/tenant-lifecycle/stages.js +33 -4
- package/dist/tenant-lifecycle/testing.d.ts +8 -0
- package/dist/tenant-lifecycle/testing.js +42 -0
- package/dist/tier-engine/entity.d.ts +2 -0
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +1 -0
- package/dist/tier-engine/tier-resolver.d.ts +2 -0
- package/dist/user/handlers/detail.query.js +2 -0
- package/dist/user/handlers/list.query.js +6 -2
- package/dist/user/handlers/update.write.js +1 -0
- package/dist/user/schema/user.d.ts +22 -8
- package/dist/user/seeding.d.ts +1 -0
- package/dist/user/seeding.js +20 -0
- package/dist/user-data-rights/boot-checks.js +5 -0
- package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +2 -2
- package/dist/user-data-rights/handlers/request-deletion.write.js +1 -0
- package/dist/user-data-rights/index.d.ts +1 -0
- package/dist/user-data-rights/index.js +1 -0
- package/dist/user-data-rights/schema/download-attempt.d.ts +14 -0
- package/dist/user-data-rights/schema/download-attempt.js +17 -9
- package/dist/user-data-rights/schema/download-token.d.ts +8 -0
- package/dist/user-data-rights/schema/export-job.d.ts +16 -8
- package/dist/user-profile/handlers/change-email.write.js +8 -2
- package/dist/waitlist/entity.d.ts +22 -6
- package/dist/workflow-runner/event-trigger.d.ts +1 -0
- package/dist/workflow-runner/event-trigger.js +63 -15
- package/dist/workflow-runner/handlers/resume-run.write.js +8 -2
- package/dist/workflow-runner/runner.d.ts +2 -1
- package/dist/workflow-runner/runner.js +5 -1
- package/package.json +35 -10
- package/src/agent-tools/changes.json +12 -0
- package/src/audit/changes.json +6 -0
- package/src/auth-email-password/changes.json +24 -0
- package/src/auth-mfa/changes.json +7 -0
- package/src/billing-foundation/changes.json +6 -0
- package/src/channel-in-app/changes.json +8 -1
- package/src/crypto-shredding/changes.json +12 -0
- package/src/delivery/changes.json +6 -0
- package/src/files/changes.json +24 -0
- package/src/files-tenant-data/changes.json +5 -0
- package/src/folders/changes.json +6 -0
- package/src/form-draft/changes.json +7 -1
- package/src/inbound-provider-imap/changes.json +6 -0
- package/src/jobs/changes.json +30 -0
- package/src/ledger/changes.json +23 -0
- package/src/managed-pages/changes.json +6 -0
- package/src/notes-history/changes.json +13 -0
- package/src/seo/changes.json +5 -0
- package/src/template-resolver/changes.json +5 -0
- package/src/tenant/changes.json +19 -0
- package/src/tenant-handover/changes.json +12 -0
- package/src/tenant-lifecycle/changes.json +22 -0
- package/src/tier-engine/changes.json +5 -0
- package/src/user/changes.json +15 -0
- package/src/user-data-rights/changes.json +6 -0
- package/src/workflow-runner/changes.json +5 -0
- package/dist/shared/has-where-rule.d.ts +0 -2
- package/dist/shared/has-where-rule.js +0 -10
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { requestContext } from "@cosmicdrift/kumiko-framework/api";
|
|
2
2
|
import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
|
|
3
|
+
import { runInSavepointIfSupported } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
3
4
|
import { computeBlindIndex, configuredBlindIndexKey, configuredPiiSubjectKms, subjectIdSchema, subjectIdToKey, } from "@cosmicdrift/kumiko-framework/crypto";
|
|
4
5
|
import { nullBlindIndexesForSubject, recordEventsOwnedExclusivelyByTenant, recordRowExistsInTenant, recordRowOwningTenantId, subjectRowExistsInTenant, } from "@cosmicdrift/kumiko-framework/db";
|
|
5
6
|
import { defineWriteHandler, } from "@cosmicdrift/kumiko-framework/engine";
|
|
6
7
|
import { AccessDeniedError, InternalError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
7
|
-
import {
|
|
8
|
-
import { assertIrreversibleOperationAllowed } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
8
|
+
import { appendDomainEventCore, assertIrreversibleOperationAllowed, } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
9
9
|
import { purgeSearchDocumentsForSubject } from "@cosmicdrift/kumiko-framework/search";
|
|
10
10
|
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
11
11
|
import * as z from "zod";
|
|
@@ -13,7 +13,7 @@ import { resolveRetentionPolicyForTenant } from "../../data-retention/index.js";
|
|
|
13
13
|
import { revokeAllPatTokensForUser } from "../../personal-access-tokens/index.js";
|
|
14
14
|
import { USER_STATUS } from "../../user/index.js";
|
|
15
15
|
import { denyIfTargetOutsideAdminTenant, isSystemAdminActor, updateUserLifecycle, } from "../../user-data-rights/index.js";
|
|
16
|
-
import { CRYPTO_SHREDDING_AGGREGATE_TYPE, RECORD_ENTITY_NOT_REGISTERED, SUBJECT_FORGET_DENIED_EVENT_NAME, SUBJECT_FORGOTTEN_EVENT_NAME, TARGET_RECORD_NOT_ADMIN_TENANT, TARGET_RECORD_RETENTION_BLOCK_DELETE, TARGET_TENANT_NOT_ADMIN_TENANT, } from "../constants.js";
|
|
16
|
+
import { CRYPTO_SHREDDING_AGGREGATE_TYPE, CRYPTO_SHREDDING_FEATURE_NAME, RECORD_ENTITY_NOT_REGISTERED, SUBJECT_FORGET_DENIED_EVENT_NAME, SUBJECT_FORGOTTEN_EVENT_NAME, TARGET_RECORD_NOT_ADMIN_TENANT, TARGET_RECORD_RETENTION_BLOCK_DELETE, TARGET_TENANT_NOT_ADMIN_TENANT, } from "../constants.js";
|
|
17
17
|
export { subjectIdSchema };
|
|
18
18
|
export const forgetSubjectSchema = z.object({
|
|
19
19
|
subject: subjectIdSchema,
|
|
@@ -23,6 +23,9 @@ export const subjectForgottenSchema = z.object({
|
|
|
23
23
|
subjectKey: z.string().min(1),
|
|
24
24
|
reason: z.string().min(10),
|
|
25
25
|
forgottenBy: z.string().min(1),
|
|
26
|
+
// Error code or name of a failed derived-data sweep, never its message (which
|
|
27
|
+
// can name the subject). Present only when the handler failed after the erase.
|
|
28
|
+
cleanupError: z.string().min(1).optional(),
|
|
26
29
|
});
|
|
27
30
|
export const subjectForgetDeniedSchema = z.object({
|
|
28
31
|
subjectKeyDigest: z.string().min(1).optional(),
|
|
@@ -160,14 +163,8 @@ async function appendDenialAuditEvent(ctx, event, subjectKey, subjectKind, denia
|
|
|
160
163
|
"ctx.dbOutsideTransaction — dispatch wiring is missing the outside-tx db source.",
|
|
161
164
|
}));
|
|
162
165
|
}
|
|
163
|
-
const eventDef = ctx.registry.getEvent(SUBJECT_FORGET_DENIED_EVENT_NAME);
|
|
164
|
-
if (!eventDef) {
|
|
165
|
-
return writeFailure(new InternalError({
|
|
166
|
-
message: `[crypto-shredding] event "${SUBJECT_FORGET_DENIED_EVENT_NAME}" is not registered.`,
|
|
167
|
-
}));
|
|
168
|
-
}
|
|
169
166
|
const blindIndexKey = configuredBlindIndexKey();
|
|
170
|
-
const payload =
|
|
167
|
+
const payload = {
|
|
171
168
|
// The denial lands in the REQUESTING actor's own tenant-scoped stream —
|
|
172
169
|
// it must never materialise the foreign subject's identifiers there. A
|
|
173
170
|
// plaintext subjectKey/aggregateId would survive as a permanent record
|
|
@@ -184,30 +181,103 @@ async function appendDenialAuditEvent(ctx, event, subjectKey, subjectKind, denia
|
|
|
184
181
|
forgottenBy: event.user.id,
|
|
185
182
|
actorTenantId: event.user.tenantId,
|
|
186
183
|
denial: denialCode,
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// crypto-shredding:event:forget-denied.
|
|
192
|
-
await append(denialAuditRunner, {
|
|
193
|
-
aggregateId: generateId(),
|
|
194
|
-
aggregateType: CRYPTO_SHREDDING_AGGREGATE_TYPE,
|
|
184
|
+
};
|
|
185
|
+
await appendDomainEventCore({
|
|
186
|
+
registry: ctx.registry,
|
|
187
|
+
db: denialAuditRunner,
|
|
195
188
|
// MUST be event.user.tenantId, never SYSTEM_TENANT_ID — unsafeRaw
|
|
196
189
|
// bypasses TenantDb's scoping, so this is the only guard against a cross-tenant denial event (fw#2452).
|
|
197
190
|
tenantId: event.user.tenantId,
|
|
198
|
-
|
|
191
|
+
userId: String(event.user.id),
|
|
192
|
+
callSiteLabel: "forget-subject denial audit",
|
|
193
|
+
callerFeature: CRYPTO_SHREDDING_FEATURE_NAME,
|
|
194
|
+
}, {
|
|
195
|
+
// Fresh stream per denial: no version to read, nothing to conflict with.
|
|
196
|
+
aggregateId: generateId(),
|
|
197
|
+
aggregateType: CRYPTO_SHREDDING_AGGREGATE_TYPE,
|
|
199
198
|
type: SUBJECT_FORGET_DENIED_EVENT_NAME,
|
|
200
|
-
eventVersion: eventDef.version,
|
|
201
199
|
payload,
|
|
202
|
-
metadata: {
|
|
203
|
-
userId: event.user.id,
|
|
204
|
-
...(reqCtx?.requestId ? { requestId: reqCtx.requestId } : {}),
|
|
205
|
-
...(reqCtx?.correlationId ? { correlationId: reqCtx.correlationId } : {}),
|
|
206
|
-
...(reqCtx?.causationId ? { causationId: reqCtx.causationId } : {}),
|
|
207
|
-
},
|
|
208
200
|
});
|
|
209
201
|
return null;
|
|
210
202
|
}
|
|
203
|
+
// Name or driver code of the error, never its message: a message can carry the
|
|
204
|
+
// subject key or row values and would land in a permanent audit event.
|
|
205
|
+
function sweepErrorLabel(err) {
|
|
206
|
+
if (typeof err === "object" && err !== null && "code" in err) {
|
|
207
|
+
const code = err.code;
|
|
208
|
+
if (typeof code === "string" && code.length > 0)
|
|
209
|
+
return code;
|
|
210
|
+
}
|
|
211
|
+
return err instanceof Error && err.name.length > 0 ? err.name : "UnknownError";
|
|
212
|
+
}
|
|
213
|
+
// The key is already erased when this runs, so a failing sweep must not skip the
|
|
214
|
+
// audit event: the failure is returned as a label for the event instead of thrown.
|
|
215
|
+
// The savepoint keeps a database error from aborting the handler transaction.
|
|
216
|
+
async function sweepDerivedSubjectData(ctx, subject, subjectKey, crossTenantRunner) {
|
|
217
|
+
try {
|
|
218
|
+
await runInSavepointIfSupported(crossTenantRunner, async (crossTenantSubjectRunner) => {
|
|
219
|
+
// Blind-index sweep (#818): nulls bidx columns now so the deterministic
|
|
220
|
+
// HMAC doesn't stay equality-matchable; raw because the ciphertext prefix addresses the subject across tenants.
|
|
221
|
+
await nullBlindIndexesForSubject(crossTenantSubjectRunner, ctx.registry.features, subjectKey);
|
|
222
|
+
// Derived search index still holds plaintext (#1610) — purge next to the
|
|
223
|
+
// blind-index sweep. No adapter → no-op (apps without search).
|
|
224
|
+
if (ctx.searchAdapter) {
|
|
225
|
+
await purgeSearchDocumentsForSubject(crossTenantSubjectRunner, ctx.registry.features, ctx.searchAdapter, subjectKey, subject);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
return sweepErrorLabel(err);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Graceful fail: email-subscribers and other non-user entities may use
|
|
235
|
+
// user-style subject keys without having an actual user row — the key erase is
|
|
236
|
+
// the important part for GDPR compliance; the lifecycle update is best-effort for
|
|
237
|
+
// real users. The savepoint confines the failure so the handler transaction
|
|
238
|
+
// stays usable for the audit event.
|
|
239
|
+
async function closeUserLoginDoorBestEffort(ctx, userId, lifecycleRunner) {
|
|
240
|
+
try {
|
|
241
|
+
await runInSavepointIfSupported(lifecycleRunner, async (userLifecycleRunner) => {
|
|
242
|
+
await updateUserLifecycle(userLifecycleRunner, userId, { status: USER_STATUS.Deleted });
|
|
243
|
+
if (ctx.registry.features.has("personal-access-tokens")) {
|
|
244
|
+
await revokeAllPatTokensForUser(userLifecycleRunner, userId);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// skip: no user row for this subject key (e.g. email subscribers); key erase and sweeps already ran.
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Through the outside-transaction db when available, so the audit event commits
|
|
253
|
+
// even though the handler may fail afterwards (sweep error) and roll back its own
|
|
254
|
+
// transaction. Without that source the event stays in the handler transaction.
|
|
255
|
+
async function appendSubjectForgotten(ctx, event, aggregateId, payload, outsideRunner) {
|
|
256
|
+
if (!outsideRunner) {
|
|
257
|
+
await ctx.unsafeAppendEvent({
|
|
258
|
+
aggregateId,
|
|
259
|
+
aggregateType: CRYPTO_SHREDDING_AGGREGATE_TYPE,
|
|
260
|
+
type: SUBJECT_FORGOTTEN_EVENT_NAME,
|
|
261
|
+
payload,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
await appendDomainEventCore({
|
|
266
|
+
registry: ctx.registry,
|
|
267
|
+
db: outsideRunner,
|
|
268
|
+
// MUST be event.user.tenantId, never SYSTEM_TENANT_ID — unsafeRaw bypasses TenantDb's scoping (fw#2452).
|
|
269
|
+
tenantId: event.user.tenantId,
|
|
270
|
+
userId: String(event.user.id),
|
|
271
|
+
callSiteLabel: "forget-subject audit",
|
|
272
|
+
callerFeature: CRYPTO_SHREDDING_FEATURE_NAME,
|
|
273
|
+
}, {
|
|
274
|
+
aggregateId,
|
|
275
|
+
aggregateType: CRYPTO_SHREDDING_AGGREGATE_TYPE,
|
|
276
|
+
type: SUBJECT_FORGOTTEN_EVENT_NAME,
|
|
277
|
+
payload,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
211
281
|
// Manual crypto-shred for a DPO / platform operator: erases the subject's
|
|
212
282
|
// DEK immediately (all its PII ciphertext becomes unreadable, reads render
|
|
213
283
|
// "[[erased]]") and appends the audit event. Forget is final — the adapter
|
|
@@ -227,7 +297,7 @@ export const forgetSubjectWrite = defineWriteHandler({
|
|
|
227
297
|
agent: { expose: false, risk: "high" },
|
|
228
298
|
escapeHatch: {
|
|
229
299
|
grants: ["unsafeRaw"],
|
|
230
|
-
reason: "denial audit
|
|
300
|
+
reason: "the denial and SUBJECT_FORGOTTEN audit appends name the caller's own tenant stream on the outside-transaction db; " +
|
|
231
301
|
"the tenant-scope and retention checks run against the subject's tenant, not necessarily the caller's; " +
|
|
232
302
|
"the blind-index sweep and search purge address the subject across tenants; the user " +
|
|
233
303
|
"lifecycle update and PAT revoke run on the SYSTEM user stream.",
|
|
@@ -270,15 +340,7 @@ export const forgetSubjectWrite = defineWriteHandler({
|
|
|
270
340
|
userId: event.user.id,
|
|
271
341
|
eraseReason: event.payload.reason,
|
|
272
342
|
});
|
|
273
|
-
|
|
274
|
-
// HMAC doesn't stay equality-matchable; raw because the ciphertext prefix addresses the subject across tenants.
|
|
275
|
-
const crossTenantSubjectRunner = ctx.db.unsafeRaw();
|
|
276
|
-
await nullBlindIndexesForSubject(crossTenantSubjectRunner, ctx.registry.features, subjectKey);
|
|
277
|
-
// Derived search index still holds plaintext (#1610) — purge next to the
|
|
278
|
-
// blind-index sweep. No adapter → no-op (apps without search).
|
|
279
|
-
if (ctx.searchAdapter) {
|
|
280
|
-
await purgeSearchDocumentsForSubject(crossTenantSubjectRunner, ctx.registry.features, ctx.searchAdapter, subjectKey, subject);
|
|
281
|
-
}
|
|
343
|
+
const cleanupError = await sweepDerivedSubjectData(ctx, subject, subjectKey, ctx.db.unsafeRaw());
|
|
282
344
|
// User subject: close the login door. DEK-erase makes the passwordHash
|
|
283
345
|
// ciphertext unreadable, but status + PATs are standalone credentials —
|
|
284
346
|
// the PAT resolver only checks revokedAt/expiresAt, NOT user.status
|
|
@@ -294,33 +356,20 @@ export const forgetSubjectWrite = defineWriteHandler({
|
|
|
294
356
|
// crash recovery is safe. User-feature guard: without the user feature
|
|
295
357
|
// read_users doesn't exist (crypto-only stack). Tenant subjects have no
|
|
296
358
|
// credentials.
|
|
297
|
-
// Graceful fail: email-subscribers and other non-user entities may use
|
|
298
|
-
// user-style subject keys without having an actual user row — the key
|
|
299
|
-
// erase above is the important part for GDPR compliance; the lifecycle
|
|
300
|
-
// update is best-effort for real users.
|
|
301
359
|
if (raw.kind === "user" && ctx.registry.features.has("user")) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
360
|
+
await closeUserLoginDoorBestEffort(ctx, raw.userId, ctx.db.unsafeRaw());
|
|
361
|
+
}
|
|
362
|
+
await appendSubjectForgotten(ctx, event, subjectAggregateId(raw), {
|
|
363
|
+
subjectKey,
|
|
364
|
+
reason: event.payload.reason,
|
|
365
|
+
forgottenBy: event.user.id,
|
|
366
|
+
...(cleanupError !== undefined && { cleanupError }),
|
|
367
|
+
}, ctx.dbOutsideTransaction?.unsafeRaw());
|
|
368
|
+
if (cleanupError !== undefined) {
|
|
369
|
+
throw new InternalError({
|
|
370
|
+
message: `[crypto-shredding] forget-subject erased the key but a derived-data sweep failed (${cleanupError}); retry to finish the cleanup.`,
|
|
371
|
+
});
|
|
313
372
|
}
|
|
314
|
-
await ctx.unsafeAppendEvent({
|
|
315
|
-
aggregateId: subjectAggregateId(raw),
|
|
316
|
-
aggregateType: CRYPTO_SHREDDING_AGGREGATE_TYPE,
|
|
317
|
-
type: SUBJECT_FORGOTTEN_EVENT_NAME,
|
|
318
|
-
payload: {
|
|
319
|
-
subjectKey,
|
|
320
|
-
reason: event.payload.reason,
|
|
321
|
-
forgottenBy: event.user.id,
|
|
322
|
-
},
|
|
323
|
-
});
|
|
324
373
|
return { isSuccess: true, data: { subjectKey } };
|
|
325
374
|
},
|
|
326
375
|
});
|
|
@@ -3,6 +3,7 @@ export declare const fieldDefinitionEntity: import("@cosmicdrift/kumiko-types/fi
|
|
|
3
3
|
readonly type: "text";
|
|
4
4
|
readonly description?: string;
|
|
5
5
|
readonly maxLength?: number;
|
|
6
|
+
readonly minLength?: number;
|
|
6
7
|
readonly required?: boolean;
|
|
7
8
|
readonly searchable?: boolean;
|
|
8
9
|
readonly sortable?: boolean;
|
|
@@ -24,6 +25,7 @@ export declare const fieldDefinitionEntity: import("@cosmicdrift/kumiko-types/fi
|
|
|
24
25
|
readonly type: "text";
|
|
25
26
|
readonly description?: string;
|
|
26
27
|
readonly maxLength?: number;
|
|
28
|
+
readonly minLength?: number;
|
|
27
29
|
readonly required?: boolean;
|
|
28
30
|
readonly searchable?: boolean;
|
|
29
31
|
readonly sortable?: boolean;
|
|
@@ -45,6 +47,7 @@ export declare const fieldDefinitionEntity: import("@cosmicdrift/kumiko-types/fi
|
|
|
45
47
|
readonly type: "text";
|
|
46
48
|
readonly description?: string;
|
|
47
49
|
readonly maxLength?: number;
|
|
50
|
+
readonly minLength?: number;
|
|
48
51
|
readonly required?: boolean;
|
|
49
52
|
readonly searchable?: boolean;
|
|
50
53
|
readonly sortable?: boolean;
|
|
@@ -91,6 +94,7 @@ export declare const fieldDefinitionEntity: import("@cosmicdrift/kumiko-types/fi
|
|
|
91
94
|
readonly type: "text";
|
|
92
95
|
readonly description?: string;
|
|
93
96
|
readonly maxLength?: number;
|
|
97
|
+
readonly minLength?: number;
|
|
94
98
|
readonly required?: boolean;
|
|
95
99
|
readonly searchable?: boolean;
|
|
96
100
|
readonly sortable?: boolean;
|
|
@@ -3,6 +3,7 @@ export declare const tenantRetentionOverrideEntity: import("@cosmicdrift/kumiko-
|
|
|
3
3
|
readonly type: "text";
|
|
4
4
|
readonly description?: string;
|
|
5
5
|
readonly maxLength?: number;
|
|
6
|
+
readonly minLength?: number;
|
|
6
7
|
readonly required?: boolean;
|
|
7
8
|
readonly searchable?: boolean;
|
|
8
9
|
readonly sortable?: boolean;
|
|
@@ -40,6 +41,7 @@ export declare const tenantRetentionOverrideEntity: import("@cosmicdrift/kumiko-
|
|
|
40
41
|
readonly type: "text";
|
|
41
42
|
readonly description?: string;
|
|
42
43
|
readonly maxLength?: number;
|
|
44
|
+
readonly minLength?: number;
|
|
43
45
|
readonly required?: boolean;
|
|
44
46
|
readonly searchable?: boolean;
|
|
45
47
|
readonly sortable?: boolean;
|
|
@@ -63,6 +65,7 @@ export declare const tenantRetentionOverrideTable: import("@cosmicdrift/kumiko-f
|
|
|
63
65
|
readonly type: "text";
|
|
64
66
|
readonly description?: string;
|
|
65
67
|
readonly maxLength?: number;
|
|
68
|
+
readonly minLength?: number;
|
|
66
69
|
readonly required?: boolean;
|
|
67
70
|
readonly searchable?: boolean;
|
|
68
71
|
readonly sortable?: boolean;
|
|
@@ -100,6 +103,7 @@ export declare const tenantRetentionOverrideTable: import("@cosmicdrift/kumiko-f
|
|
|
100
103
|
readonly type: "text";
|
|
101
104
|
readonly description?: string;
|
|
102
105
|
readonly maxLength?: number;
|
|
106
|
+
readonly minLength?: number;
|
|
103
107
|
readonly required?: boolean;
|
|
104
108
|
readonly searchable?: boolean;
|
|
105
109
|
readonly sortable?: boolean;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
-
export
|
|
3
|
+
export type AddressOptOutKey = string;
|
|
4
|
+
export declare function addressOptOutKey(addressHash: string, channel: string): AddressOptOutKey;
|
|
5
|
+
export declare function selectOptedOutAddresses(db: DbConnection, tenantId: TenantId, notificationType: string, addressHashes: readonly string[]): Promise<ReadonlySet<AddressOptOutKey>>;
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { notificationAddressOptOutsTable } from "../../tables.js";
|
|
3
|
+
export function addressOptOutKey(addressHash, channel) {
|
|
4
|
+
return `${addressHash}|${channel}`;
|
|
5
|
+
}
|
|
2
6
|
// Exact match only — unlike notification-preferences there is no wildcard
|
|
3
7
|
// ("*") semantics for address opt-outs, the token that creates a row always
|
|
4
|
-
// carries one concrete notificationType/channel pair.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
// carries one concrete notificationType/channel pair. One query covers every
|
|
9
|
+
// recipient of a notify() call.
|
|
10
|
+
export async function selectOptedOutAddresses(db, tenantId, notificationType, addressHashes) {
|
|
11
|
+
if (addressHashes.length === 0)
|
|
12
|
+
return new Set();
|
|
13
|
+
const rows = await selectMany(db, notificationAddressOptOutsTable, { tenantId, notificationType, addressHash: { in: addressHashes } });
|
|
14
|
+
return new Set(rows.map((row) => addressOptOutKey(row.addressHash, row.channel)));
|
|
10
15
|
}
|
|
@@ -9,7 +9,7 @@ import { hashUnsubscribeAddress } from "./address-opt-out.js";
|
|
|
9
9
|
import { appendAttemptEvent, logAttempt } from "./attempt-log.js";
|
|
10
10
|
import { buildChannelContext } from "./channel-context.js";
|
|
11
11
|
import { DELIVERY_CHANNEL_EXTENSION, DeliveryJobs, deliveryPriorityRank } from "./constants.js";
|
|
12
|
-
import {
|
|
12
|
+
import { addressOptOutKey, selectOptedOutAddresses, } from "./db/queries/address-opt-outs.js";
|
|
13
13
|
import { selectNotificationPreferences } from "./db/queries/preferences.js";
|
|
14
14
|
import { isDeliveryChannelPlugin, } from "./types.js";
|
|
15
15
|
export function redactedMessageOf(err) {
|
|
@@ -255,12 +255,54 @@ export function createDeliveryService(options) {
|
|
|
255
255
|
}
|
|
256
256
|
// Shared by deliverToUser (resolved account address) and deliverDirect (a
|
|
257
257
|
// route address with no user account) — same suppression rule either way:
|
|
258
|
-
// no blind-index key configured means no hash, so nothing to look up.
|
|
259
|
-
|
|
258
|
+
// no blind-index key configured means no hash, so nothing to look up. One
|
|
259
|
+
// query per notify() call, not per recipient and channel.
|
|
260
|
+
async function loadSuppressedAddresses(tenantId, notificationType, priority, addresses) {
|
|
261
|
+
// Critical priority skips address opt-outs, same rule as user preferences.
|
|
262
|
+
if (priority === "critical")
|
|
263
|
+
return new Set();
|
|
264
|
+
const addressHashes = new Set();
|
|
265
|
+
for (const address of addresses) {
|
|
266
|
+
const addressHash = hashUnsubscribeAddress(address);
|
|
267
|
+
if (addressHash !== undefined)
|
|
268
|
+
addressHashes.add(addressHash);
|
|
269
|
+
}
|
|
270
|
+
return selectOptedOutAddresses(db, tenantId, notificationType, [...addressHashes]);
|
|
271
|
+
}
|
|
272
|
+
function isAddressSuppressed(suppressed, address, channelName) {
|
|
260
273
|
const addressHash = hashUnsubscribeAddress(address);
|
|
261
|
-
|
|
274
|
+
return addressHash !== undefined && suppressed.has(addressOptOutKey(addressHash, channelName));
|
|
275
|
+
}
|
|
276
|
+
const recipientChannelKey = (userId, channelName) => `${userId}|${channelName}`;
|
|
277
|
+
async function isChannelDeliverable(userId, tenantId, notificationType, channelName) {
|
|
278
|
+
if (isChannelKilled && (await isChannelKilled(tenantId, channelName)))
|
|
262
279
|
return false;
|
|
263
|
-
return
|
|
280
|
+
return isChannelEnabled(userId, tenantId, notificationType, channelName);
|
|
281
|
+
}
|
|
282
|
+
// Resolves every user's channel addresses up front so the opt-out lookup can
|
|
283
|
+
// cover them all at once. A resolve that throws stays out of the map: the
|
|
284
|
+
// delivery loop resolves it again and reports channel_error as before.
|
|
285
|
+
// Channels the kill switch or the user's preferences disable are never resolved
|
|
286
|
+
// here: resolve may have side effects and deliverToUser skips those channels.
|
|
287
|
+
async function prefetchUserAddresses(userIds, tenantId, notificationType) {
|
|
288
|
+
const addresses = new Map();
|
|
289
|
+
const channelCtx = buildChannelContext(db, registry, sseBroker, tenantId, secrets);
|
|
290
|
+
for (const userId of userIds) {
|
|
291
|
+
for (const channel of channels) {
|
|
292
|
+
if (!channel.resolve)
|
|
293
|
+
continue;
|
|
294
|
+
try {
|
|
295
|
+
if (!(await isChannelDeliverable(userId, tenantId, notificationType, channel.name))) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
addresses.set(recipientChannelKey(userId, channel.name), await channel.resolve(userId, channelCtx));
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// skip: re-resolved (and logged) by deliverToUser
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return addresses;
|
|
264
306
|
}
|
|
265
307
|
// Check if user has disabled this notification+channel combo.
|
|
266
308
|
// Specificity order: exact > any wildcard. When only wildcards match and they
|
|
@@ -283,7 +325,7 @@ export function createDeliveryService(options) {
|
|
|
283
325
|
// and conservative — DB ordering no longer decides the outcome).
|
|
284
326
|
return !prefs.some((p) => p.enabled === false);
|
|
285
327
|
}
|
|
286
|
-
async function deliverToUser(userId, notificationType, data, tenantId, priority, run) {
|
|
328
|
+
async function deliverToUser(userId, notificationType, data, tenantId, priority, run, prefetchedAddresses, suppressed) {
|
|
287
329
|
const channelCtx = buildChannelContext(db, registry, sseBroker, tenantId, secrets);
|
|
288
330
|
for (const channel of channels) {
|
|
289
331
|
// Route-only channel (no per-user address): not a user-notification target.
|
|
@@ -342,7 +384,10 @@ export function createDeliveryService(options) {
|
|
|
342
384
|
}
|
|
343
385
|
}
|
|
344
386
|
try {
|
|
345
|
-
const
|
|
387
|
+
const prefetchKey = recipientChannelKey(userId, channel.name);
|
|
388
|
+
const address = prefetchedAddresses.has(prefetchKey)
|
|
389
|
+
? prefetchedAddresses.get(prefetchKey)
|
|
390
|
+
: await channel.resolve(userId, channelCtx);
|
|
346
391
|
if (!address) {
|
|
347
392
|
await logDelivery(run, {
|
|
348
393
|
tenantId,
|
|
@@ -356,8 +401,7 @@ export function createDeliveryService(options) {
|
|
|
356
401
|
});
|
|
357
402
|
continue;
|
|
358
403
|
}
|
|
359
|
-
if (priority !== "critical" &&
|
|
360
|
-
(await isAddressSuppressed(address, tenantId, notificationType, channel.name))) {
|
|
404
|
+
if (priority !== "critical" && isAddressSuppressed(suppressed, address, channel.name)) {
|
|
361
405
|
await logDelivery(run, {
|
|
362
406
|
tenantId,
|
|
363
407
|
notificationType,
|
|
@@ -402,6 +446,7 @@ export function createDeliveryService(options) {
|
|
|
402
446
|
}
|
|
403
447
|
async function deliverDirect(route, notificationType, data, tenantId, priority, recipientId, run) {
|
|
404
448
|
const channelCtx = buildChannelContext(db, registry, sseBroker, tenantId, secrets);
|
|
449
|
+
const suppressed = await loadSuppressedAddresses(tenantId, notificationType, priority, Object.values(route));
|
|
405
450
|
// Direct routing skips preferences (no user account) but NOT rate limit
|
|
406
451
|
// — direct sends can still be abused (webhook replays, test harnesses).
|
|
407
452
|
for (const channel of channels) {
|
|
@@ -411,8 +456,7 @@ export function createDeliveryService(options) {
|
|
|
411
456
|
continue;
|
|
412
457
|
// Address opt-out (critical priority skips it, same rule as user
|
|
413
458
|
// preferences).
|
|
414
|
-
if (priority !== "critical" &&
|
|
415
|
-
(await isAddressSuppressed(address, tenantId, notificationType, channel.name))) {
|
|
459
|
+
if (priority !== "critical" && isAddressSuppressed(suppressed, address, channel.name)) {
|
|
416
460
|
await logDelivery(run, {
|
|
417
461
|
tenantId,
|
|
418
462
|
notificationType,
|
|
@@ -513,8 +557,12 @@ export function createDeliveryService(options) {
|
|
|
513
557
|
else {
|
|
514
558
|
userIds = to;
|
|
515
559
|
}
|
|
560
|
+
const prefetchedAddresses = priority === "critical"
|
|
561
|
+
? new Map()
|
|
562
|
+
: await prefetchUserAddresses(userIds, tenantId, notificationType);
|
|
563
|
+
const suppressed = await loadSuppressedAddresses(tenantId, notificationType, priority, [...prefetchedAddresses.values()].filter((address) => !!address));
|
|
516
564
|
for (const userId of userIds) {
|
|
517
|
-
await deliverToUser(userId, notificationType, data, tenantId, priority, run);
|
|
565
|
+
await deliverToUser(userId, notificationType, data, tenantId, priority, run, prefetchedAddresses, suppressed);
|
|
518
566
|
}
|
|
519
567
|
}
|
|
520
568
|
return { deliveries: run.deliveries };
|
|
@@ -8,7 +8,7 @@ export const DeliveryHandlers = {
|
|
|
8
8
|
resubscribeAddress: "delivery:write:resubscribe-address",
|
|
9
9
|
resubscribeUser: "delivery:write:resubscribe-user",
|
|
10
10
|
};
|
|
11
|
-
//
|
|
11
|
+
// Stable path: links signed against it keep working. The unsubscribe route is
|
|
12
12
|
// mounted at this exact path via `extraRoutes: [...createUnsubscribeRoutes(...)]`.
|
|
13
13
|
export const DELIVERY_UNSUBSCRIBE_PATH = "/api/delivery/unsubscribe";
|
|
14
14
|
// Mounted alongside the unsubscribe route by the same
|
|
@@ -6,6 +6,7 @@ export declare const notificationPreferenceEntity: import("@cosmicdrift/kumiko-t
|
|
|
6
6
|
readonly type: "text";
|
|
7
7
|
readonly description?: string;
|
|
8
8
|
readonly maxLength?: number;
|
|
9
|
+
readonly minLength?: number;
|
|
9
10
|
readonly required?: boolean;
|
|
10
11
|
readonly searchable?: boolean;
|
|
11
12
|
readonly sortable?: boolean;
|
|
@@ -27,6 +28,7 @@ export declare const notificationPreferenceEntity: import("@cosmicdrift/kumiko-t
|
|
|
27
28
|
readonly type: "text";
|
|
28
29
|
readonly description?: string;
|
|
29
30
|
readonly maxLength?: number;
|
|
31
|
+
readonly minLength?: number;
|
|
30
32
|
readonly required?: boolean;
|
|
31
33
|
readonly searchable?: boolean;
|
|
32
34
|
readonly sortable?: boolean;
|
|
@@ -48,6 +50,7 @@ export declare const notificationPreferenceEntity: import("@cosmicdrift/kumiko-t
|
|
|
48
50
|
readonly type: "text";
|
|
49
51
|
readonly description?: string;
|
|
50
52
|
readonly maxLength?: number;
|
|
53
|
+
readonly minLength?: number;
|
|
51
54
|
readonly required?: boolean;
|
|
52
55
|
readonly searchable?: boolean;
|
|
53
56
|
readonly sortable?: boolean;
|
|
@@ -75,6 +78,7 @@ export declare const notificationAddressOptOutEntity: import("@cosmicdrift/kumik
|
|
|
75
78
|
readonly type: "text";
|
|
76
79
|
readonly description?: string;
|
|
77
80
|
readonly maxLength?: number;
|
|
81
|
+
readonly minLength?: number;
|
|
78
82
|
readonly required?: boolean;
|
|
79
83
|
readonly searchable?: boolean;
|
|
80
84
|
readonly sortable?: boolean;
|
|
@@ -96,6 +100,7 @@ export declare const notificationAddressOptOutEntity: import("@cosmicdrift/kumik
|
|
|
96
100
|
readonly type: "text";
|
|
97
101
|
readonly description?: string;
|
|
98
102
|
readonly maxLength?: number;
|
|
103
|
+
readonly minLength?: number;
|
|
99
104
|
readonly required?: boolean;
|
|
100
105
|
readonly searchable?: boolean;
|
|
101
106
|
readonly sortable?: boolean;
|
|
@@ -117,6 +122,7 @@ export declare const notificationAddressOptOutEntity: import("@cosmicdrift/kumik
|
|
|
117
122
|
readonly type: "text";
|
|
118
123
|
readonly description?: string;
|
|
119
124
|
readonly maxLength?: number;
|
|
125
|
+
readonly minLength?: number;
|
|
120
126
|
readonly required?: boolean;
|
|
121
127
|
readonly searchable?: boolean;
|
|
122
128
|
readonly sortable?: boolean;
|