@cosmicdrift/kumiko-bundled-features 0.351.0 → 0.353.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-manifest.js +8 -2
- 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/audit/handlers/list.query.js +13 -8
- package/dist/auth-email-password/handlers/signup-confirm.write.js +2 -3
- package/dist/auth-mfa/schema/user-mfa.d.ts +6 -0
- package/dist/auth-mfa/screens.js +2 -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/book-cap-usage.d.ts +5 -1
- package/dist/cap-counter/book-cap-usage.js +65 -44
- package/dist/cap-counter/cap-reservation.d.ts +12 -0
- package/dist/cap-counter/cap-reservation.js +135 -0
- package/dist/cap-counter/constants.d.ts +3 -0
- package/dist/cap-counter/constants.js +6 -0
- package/dist/cap-counter/entity.d.ts +1 -0
- package/dist/cap-counter/feature.js +7 -2
- package/dist/cap-counter/handlers/increment-rolling.write.d.ts +5 -0
- package/dist/cap-counter/handlers/increment-rolling.write.js +4 -0
- package/dist/cap-counter/index.d.ts +4 -2
- package/dist/cap-counter/index.js +3 -1
- package/dist/cap-counter/payload-field.d.ts +1 -0
- package/dist/cap-counter/payload-field.js +11 -0
- package/dist/cap-counter/reservation-chain.d.ts +2 -0
- package/dist/cap-counter/reservation-chain.js +44 -0
- package/dist/cap-counter/stock-cap-guard.d.ts +3 -0
- package/dist/cap-counter/tables.d.ts +3 -0
- package/dist/cap-counter/tables.js +36 -0
- package/dist/cap-counter/value-cap-guard.d.ts +18 -0
- package/dist/cap-counter/value-cap-guard.js +33 -0
- package/dist/cap-counter/with-cap-enforcement.d.ts +5 -7
- package/dist/cap-counter/with-cap-enforcement.js +35 -51
- 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-discord/feature.js +1 -0
- package/dist/channel-slack/feature.js +1 -0
- package/dist/channel-teams/feature.js +1 -0
- 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/custom-fields/entity.d.ts +4 -0
- package/dist/data-retention/schema/tenant-retention-override.d.ts +4 -0
- package/dist/delivery/attempt-log-retention.d.ts +9 -0
- package/dist/delivery/attempt-log-retention.js +23 -0
- package/dist/delivery/attempt-log.d.ts +1 -1
- package/dist/delivery/attempt-log.js +21 -2
- package/dist/delivery/constants.d.ts +1 -1
- package/dist/delivery/constants.js +1 -1
- package/dist/delivery/events.d.ts +1 -0
- package/dist/delivery/events.js +2 -0
- package/dist/delivery/feature.d.ts +1 -0
- package/dist/delivery/feature.js +39 -3
- package/dist/delivery/index.d.ts +2 -1
- package/dist/delivery/index.js +2 -1
- package/dist/delivery/mask-recipient-address.d.ts +1 -0
- package/dist/delivery/mask-recipient-address.js +41 -0
- package/dist/delivery/public-names.d.ts +2 -0
- package/dist/delivery/public-names.js +4 -1
- package/dist/delivery/tables.d.ts +6 -0
- package/dist/delivery/types.d.ts +1 -0
- package/dist/delivery/types.js +6 -2
- package/dist/document-ingest-foundation/entity.d.ts +4 -0
- package/dist/files-tenant-data/hooks.js +3 -2
- package/dist/folders/entity.d.ts +8 -0
- package/dist/folders/feature.js +2 -10
- 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/inbound-mail-foundation/entities.d.ts +25 -0
- package/dist/inbound-mail-foundation/projection.d.ts +20 -0
- package/dist/jobs/handlers/list.query.d.ts +2 -1
- package/dist/jobs/handlers/list.query.js +39 -14
- package/dist/jobs/i18n.js +1 -0
- package/dist/jobs/job-run-table.d.ts +6 -0
- package/dist/ledger/entity.d.ts +24 -8
- package/dist/ledger/entity.js +4 -0
- package/dist/ledger/schemas.js +8 -2
- 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/feature.js +2 -10
- package/dist/personal-access-tokens/schema/api-token.d.ts +6 -0
- package/dist/personal-access-tokens/screens.js +14 -3
- package/dist/renderer-foundation/types.d.ts +1 -0
- package/dist/renderer-simple/feature.js +4 -6
- package/dist/renderer-simple/index.d.ts +1 -0
- package/dist/renderer-simple/mail-blocks.d.ts +21 -0
- package/dist/renderer-simple/mail-blocks.js +107 -0
- package/dist/renderer-simple/simple-renderer.js +21 -3
- package/dist/secrets/table.d.ts +1 -0
- package/dist/seo/feature.js +28 -6
- package/dist/sessions/schema/user-session.d.ts +4 -0
- 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/index.d.ts +2 -1
- package/dist/shared/index.js +2 -1
- package/dist/shared/rate-limited-text-response.d.ts +2 -0
- package/dist/shared/rate-limited-text-response.js +9 -0
- package/dist/tags/entity.d.ts +9 -0
- package/dist/tags/feature.js +2 -10
- 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/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/index.d.ts +1 -0
- package/dist/tenant/index.js +1 -0
- package/dist/tenant/invitation-table.d.ts +16 -8
- 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-lifecycle/feature.js +1 -0
- package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +3 -0
- package/dist/tenant-lifecycle/run-tenant-destroy.js +2 -0
- package/dist/tenant-lifecycle/stages.d.ts +2 -0
- package/dist/tenant-lifecycle/stages.js +7 -1
- package/dist/tier-engine/entity.d.ts +2 -0
- package/dist/tier-engine/index.d.ts +1 -0
- package/dist/tier-engine/index.js +1 -0
- package/dist/tier-engine/tier-option-gate.d.ts +32 -0
- package/dist/tier-engine/tier-option-gate.js +62 -0
- package/dist/tier-engine/tier-resolver.d.ts +2 -0
- package/dist/user/handlers/list.query.js +2 -2
- package/dist/user/schema/user.d.ts +22 -8
- 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/schema/download-attempt.d.ts +14 -0
- 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-data-rights-defaults/hooks/delivery-attempt.userdata-hook.js +25 -6
- package/dist/waitlist/entity.d.ts +22 -6
- package/dist/workflow-runner/event-trigger.js +6 -3
- package/dist/workflow-runner/handlers/resume-run.write.js +3 -0
- package/package.json +10 -10
- package/src/agent-tools/changes.json +12 -0
- package/src/audit/changes.json +6 -0
- package/src/cap-counter/changes.json +28 -0
- package/src/delivery/changes.json +12 -0
- package/src/derivatives-sharp/changes.json +6 -0
- package/src/form-draft/changes.json +7 -1
- package/src/legal-pages/changes.json +6 -0
- package/src/managed-pages/changes.json +6 -0
- package/src/renderer-simple/changes.json +6 -0
- package/src/seo/changes.json +5 -0
- package/src/tenant/changes.json +7 -0
- package/src/tenant-handover/changes.json +6 -0
- package/src/tier-engine/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
|
@@ -2,7 +2,7 @@ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
|
2
2
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
4
|
import * as z from "zod";
|
|
5
|
-
import { tenantTable } from "../../tenant/index.js";
|
|
5
|
+
import { decryptTenantNames, tenantTable } from "../../tenant/index.js";
|
|
6
6
|
export const tenantOptionsQuery = defineQueryHandler({
|
|
7
7
|
name: "tenant-options",
|
|
8
8
|
description: "Returns all tenants as id/name option pairs sorted by name; use it to populate the tenant picker of a platform-wide overview.",
|
|
@@ -15,9 +15,8 @@ export const tenantOptionsQuery = defineQueryHandler({
|
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
17
|
const db = ctx.systemDb.acknowledgeCrossTenant("cap-overview:tenant-options — SystemAdmin dashboard tenant-filter options");
|
|
18
|
-
const tenants = await selectMany(db, tenantTable, {},
|
|
19
|
-
|
|
20
|
-
});
|
|
21
|
-
return { rows: tenants.map((tenant) => ({ value: tenant.id, label: tenant.name })) };
|
|
18
|
+
const tenants = await decryptTenantNames(await selectMany(db, tenantTable, {}), "cap-overview:tenant-options");
|
|
19
|
+
const sorted = [...tenants].sort((a, b) => a.name.localeCompare(b.name));
|
|
20
|
+
return { rows: sorted.map((tenant) => ({ value: tenant.id, label: tenant.name })) };
|
|
22
21
|
},
|
|
23
22
|
});
|
|
@@ -30,6 +30,7 @@ export function createChannelDiscordFeature(options = {}) {
|
|
|
30
30
|
r.translations({ keys: { "delivery.channel.discord": { en: "Discord" } } });
|
|
31
31
|
r.useExtension(DELIVERY_CHANNEL_EXTENSION, "discord", {
|
|
32
32
|
mode: channel.mode,
|
|
33
|
+
addressKind: "connection-name",
|
|
33
34
|
send: channel.send,
|
|
34
35
|
});
|
|
35
36
|
});
|
|
@@ -27,6 +27,7 @@ export function createChannelSlackFeature(options = {}) {
|
|
|
27
27
|
r.translations({ keys: { "delivery.channel.slack": { en: "Slack" } } });
|
|
28
28
|
r.useExtension(DELIVERY_CHANNEL_EXTENSION, "slack", {
|
|
29
29
|
mode: channel.mode,
|
|
30
|
+
addressKind: "connection-name",
|
|
30
31
|
send: channel.send,
|
|
31
32
|
});
|
|
32
33
|
});
|
|
@@ -31,6 +31,7 @@ export function createChannelTeamsFeature(options = {}) {
|
|
|
31
31
|
r.translations({ keys: { "delivery.channel.teams": { en: "Microsoft Teams" } } });
|
|
32
32
|
r.useExtension(DELIVERY_CHANNEL_EXTENSION, "teams", {
|
|
33
33
|
mode: channel.mode,
|
|
34
|
+
addressKind: "connection-name",
|
|
34
35
|
send: channel.send,
|
|
35
36
|
});
|
|
36
37
|
});
|
|
@@ -11,13 +11,14 @@ export declare const tenantComplianceProfileEntity: import("@cosmicdrift/kumiko-
|
|
|
11
11
|
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
12
12
|
readonly display?: "radio" | "dropdown";
|
|
13
13
|
readonly optionTones?: {
|
|
14
|
-
readonly "de-hr-dsgvo-hgb"?:
|
|
15
|
-
readonly "eu-dsgvo"?:
|
|
16
|
-
readonly "minimal-no-region"?:
|
|
17
|
-
readonly "swiss-dsg"?:
|
|
14
|
+
readonly "de-hr-dsgvo-hgb"?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
15
|
+
readonly "eu-dsgvo"?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
16
|
+
readonly "minimal-no-region"?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
17
|
+
readonly "swiss-dsg"?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
18
18
|
} | undefined;
|
|
19
19
|
readonly optionsQuery?: string;
|
|
20
20
|
readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
|
|
21
|
+
readonly optionsAvailabilityQuery?: string;
|
|
21
22
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
22
23
|
required: true;
|
|
23
24
|
};
|
|
@@ -51,13 +52,14 @@ export declare const tenantComplianceProfileTable: import("@cosmicdrift/kumiko-f
|
|
|
51
52
|
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
52
53
|
readonly display?: "radio" | "dropdown";
|
|
53
54
|
readonly optionTones?: {
|
|
54
|
-
readonly "de-hr-dsgvo-hgb"?:
|
|
55
|
-
readonly "eu-dsgvo"?:
|
|
56
|
-
readonly "minimal-no-region"?:
|
|
57
|
-
readonly "swiss-dsg"?:
|
|
55
|
+
readonly "de-hr-dsgvo-hgb"?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
56
|
+
readonly "eu-dsgvo"?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
57
|
+
readonly "minimal-no-region"?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
58
|
+
readonly "swiss-dsg"?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
58
59
|
} | undefined;
|
|
59
60
|
readonly optionsQuery?: string;
|
|
60
61
|
readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
|
|
62
|
+
readonly optionsAvailabilityQuery?: string;
|
|
61
63
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
62
64
|
required: true;
|
|
63
65
|
};
|
|
@@ -3,7 +3,7 @@ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
|
3
3
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
4
|
import * as z from "zod";
|
|
5
5
|
import { tenantComplianceProfileTable } from "../../compliance-profiles/index.js";
|
|
6
|
-
import { tenantTable } from "../../tenant/index.js";
|
|
6
|
+
import { decryptTenantNames, tenantTable } from "../../tenant/index.js";
|
|
7
7
|
// SystemAdmin platform-wide counterpart to needs-profile (#2089).
|
|
8
8
|
//
|
|
9
9
|
// #2084 stopped needs-profile from nagging a TenantAdmin who can no longer
|
|
@@ -28,9 +28,7 @@ export const tenantsMissingProfileQuery = defineQueryHandler({
|
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
30
|
const db = ctx.systemDb.acknowledgeCrossTenant("platform operator scans every tenant for a missing compliance-profile selection");
|
|
31
|
-
const tenants = await db.selectMany(tenantTable, {
|
|
32
|
-
isEnabled: true,
|
|
33
|
-
});
|
|
31
|
+
const tenants = await decryptTenantNames(await db.selectMany(tenantTable, { isEnabled: true }), "compliance-profiles-ops:tenants-missing-profile");
|
|
34
32
|
const profileRows = await db.selectMany(tenantComplianceProfileTable, {});
|
|
35
33
|
const tenantIdsWithProfile = new Set(profileRows.map((row) => row.tenantId));
|
|
36
34
|
return {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type ConfigScope, type ConfigValueSource } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import * as z from "zod";
|
|
3
|
-
export declare const valuesQuery: import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"values", z.ZodObject<{
|
|
3
|
+
export declare const valuesQuery: import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"values", z.ZodObject<{
|
|
4
|
+
keys: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
5
|
+
}, z.core.$strip>, Record<string, {
|
|
4
6
|
value: string | number | boolean | undefined;
|
|
5
7
|
scope: ConfigScope;
|
|
6
8
|
source: ConfigValueSource;
|
|
@@ -5,8 +5,10 @@ import { MASKED, redactInheritedCascade, shouldRedactInherited } from "../read-r
|
|
|
5
5
|
import { hasConfigAccess } from "../write-helpers.js";
|
|
6
6
|
export const valuesQuery = defineQueryHandler({
|
|
7
7
|
name: "values",
|
|
8
|
-
description: "Returns the effective value, scope and winning source for every config key the caller may read, with encrypted and secret-backed values masked; use it to inspect the current settings.",
|
|
9
|
-
schema: z.object({
|
|
8
|
+
description: "Returns the effective value, scope and winning source for every config key the caller may read (optionally narrowed to `keys`), with encrypted and secret-backed values masked; use it to inspect the current settings.",
|
|
9
|
+
schema: z.object({
|
|
10
|
+
keys: z.array(z.string()).optional(),
|
|
11
|
+
}),
|
|
10
12
|
// Per-key read access enforced via hasConfigAccess inside the handler.
|
|
11
13
|
access: {
|
|
12
14
|
openToAll: {
|
|
@@ -26,7 +28,10 @@ export const valuesQuery = defineQueryHandler({
|
|
|
26
28
|
const allKeys = registry.getAllConfigKeys();
|
|
27
29
|
const keyDefs = new Map();
|
|
28
30
|
const filteredKeys = [];
|
|
31
|
+
const requestedKeys = query.payload.keys === undefined ? undefined : new Set(query.payload.keys);
|
|
29
32
|
for (const [qualifiedKey, keyDef] of allKeys) {
|
|
33
|
+
if (requestedKeys !== undefined && !requestedKeys.has(qualifiedKey))
|
|
34
|
+
continue;
|
|
30
35
|
if (!hasConfigAccess(keyDef.access.read, query.user.roles))
|
|
31
36
|
continue;
|
|
32
37
|
keyDefs.set(qualifiedKey, keyDef);
|
package/dist/config/table.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export declare const configValueEntity: import("@cosmicdrift/kumiko-types/fields
|
|
|
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 configValueEntity: import("@cosmicdrift/kumiko-types/fields
|
|
|
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 configValueEntity: import("@cosmicdrift/kumiko-types/fields
|
|
|
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;
|
|
@@ -68,6 +71,7 @@ export declare const configValuesTable: import("@cosmicdrift/kumiko-framework/db
|
|
|
68
71
|
readonly type: "text";
|
|
69
72
|
readonly description?: string;
|
|
70
73
|
readonly maxLength?: number;
|
|
74
|
+
readonly minLength?: number;
|
|
71
75
|
readonly required?: boolean;
|
|
72
76
|
readonly searchable?: boolean;
|
|
73
77
|
readonly sortable?: boolean;
|
|
@@ -89,6 +93,7 @@ export declare const configValuesTable: import("@cosmicdrift/kumiko-framework/db
|
|
|
89
93
|
readonly type: "text";
|
|
90
94
|
readonly description?: string;
|
|
91
95
|
readonly maxLength?: number;
|
|
96
|
+
readonly minLength?: number;
|
|
92
97
|
readonly required?: boolean;
|
|
93
98
|
readonly searchable?: boolean;
|
|
94
99
|
readonly sortable?: boolean;
|
|
@@ -110,6 +115,7 @@ export declare const configValuesTable: import("@cosmicdrift/kumiko-framework/db
|
|
|
110
115
|
readonly type: "text";
|
|
111
116
|
readonly description?: string;
|
|
112
117
|
readonly maxLength?: number;
|
|
118
|
+
readonly minLength?: number;
|
|
113
119
|
readonly required?: boolean;
|
|
114
120
|
readonly searchable?: boolean;
|
|
115
121
|
readonly sortable?: boolean;
|
|
@@ -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;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import { type PruneEventsOptions } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
3
|
+
export declare const DEFAULT_ATTEMPT_LOG_RETENTION_DAYS = 90;
|
|
4
|
+
export type AttemptLogRetentionOptions = Pick<PruneEventsOptions, "olderThan" | "olderThanDays">;
|
|
5
|
+
export declare function resolveAttemptLogRetentionDays(option: number | false | undefined): number | undefined;
|
|
6
|
+
export declare function runAttemptLogRetention(db: DbConnection, options: AttemptLogRetentionOptions): Promise<{
|
|
7
|
+
readonly deletedEvents: number;
|
|
8
|
+
readonly deletedRows: number;
|
|
9
|
+
}>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { deleteManyBatched } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { pruneEvents } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
3
|
+
import { deliveryAttemptsTable } from "./tables.js";
|
|
4
|
+
export const DEFAULT_ATTEMPT_LOG_RETENTION_DAYS = 90;
|
|
5
|
+
const ROW_DELETE_BATCH_SIZE = 1000;
|
|
6
|
+
export function resolveAttemptLogRetentionDays(option) {
|
|
7
|
+
if (option === false)
|
|
8
|
+
return undefined;
|
|
9
|
+
const days = option ?? DEFAULT_ATTEMPT_LOG_RETENTION_DAYS;
|
|
10
|
+
if (!Number.isInteger(days) || days <= 0) {
|
|
11
|
+
throw new Error(`delivery: attemptLogRetentionDays must be a positive integer or false, got ${String(option)}`);
|
|
12
|
+
}
|
|
13
|
+
return days;
|
|
14
|
+
}
|
|
15
|
+
// Prunes the attempt events first and then the projection rows older than the same cutoff: a
|
|
16
|
+
// rebuild would not recreate rows whose events are gone, so they would otherwise stay forever.
|
|
17
|
+
// A lagging event consumer makes pruneEvents throw ConsumerLagError before anything is deleted;
|
|
18
|
+
// the caller retries on its next run.
|
|
19
|
+
export async function runAttemptLogRetention(db, options) {
|
|
20
|
+
const pruned = await pruneEvents(db, { ...options, aggregateTypes: ["deliveryAttempt"] });
|
|
21
|
+
const rows = await deleteManyBatched(db, deliveryAttemptsTable, { createdAt: { lt: pruned.cutoff } }, { limit: ROW_DELETE_BATCH_SIZE });
|
|
22
|
+
return { deletedEvents: pruned.deletedCount, deletedRows: rows.deleted };
|
|
23
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
-
import type
|
|
3
|
+
import { type DeliveryLogEntry } from "./types.js";
|
|
4
4
|
export declare function appendAttemptEvent(db: DbConnection, registry: Registry, attemptId: string, entry: DeliveryLogEntry): Promise<void>;
|
|
5
5
|
export declare function logAttempt(db: DbConnection, registry: Registry, entry: DeliveryLogEntry): Promise<string>;
|
|
@@ -5,16 +5,35 @@
|
|
|
5
5
|
import { append, getStreamVersion } from "@cosmicdrift/kumiko-framework/event-store";
|
|
6
6
|
import { runProjectionsForEvent } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
7
7
|
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
8
|
-
import { DELIVERY_ATTEMPT_EVENT } from "./constants.js";
|
|
8
|
+
import { DELIVERY_ATTEMPT_EVENT, DELIVERY_CHANNEL_EXTENSION } from "./constants.js";
|
|
9
9
|
import { deliveryAttemptSchema } from "./events.js";
|
|
10
|
+
import { maskRecipientAddress } from "./mask-recipient-address.js";
|
|
11
|
+
import { isDeliveryChannelPlugin } from "./types.js";
|
|
12
|
+
// Unknown channels count as personal: masking is the fail-closed default.
|
|
13
|
+
function addressIsConnectionName(registry, channelName) {
|
|
14
|
+
return registry
|
|
15
|
+
.getExtensionUsages(DELIVERY_CHANNEL_EXTENSION)
|
|
16
|
+
.some((usage) => usage.entityName === channelName &&
|
|
17
|
+
isDeliveryChannelPlugin(usage.options) &&
|
|
18
|
+
usage.options.addressKind === "connection-name");
|
|
19
|
+
}
|
|
20
|
+
function loggedAddress(registry, entry) {
|
|
21
|
+
return addressIsConnectionName(registry, entry.channel)
|
|
22
|
+
? entry.recipientAddress
|
|
23
|
+
: maskRecipientAddress(entry.recipientAddress);
|
|
24
|
+
}
|
|
10
25
|
// Shared append + inline-projection write (low-level append() does not
|
|
11
26
|
// auto-fire projections — only the dispatcher/executor paths do).
|
|
12
27
|
async function writeAttemptEvent(db, registry, attemptId, expectedVersion, entry) {
|
|
13
28
|
const { tenantId, ...rest } = entry;
|
|
29
|
+
// Masked here, the only place attempt events are written, so no event or projection row ever holds the full address.
|
|
14
30
|
// Schema-parse to match ctx.appendEvent's guarantee: a payload drift between
|
|
15
31
|
// service/job + feature-registration fails loudly here instead of landing on
|
|
16
32
|
// the events-table and crashing a consumer later.
|
|
17
|
-
const payload = deliveryAttemptSchema.parse(
|
|
33
|
+
const payload = deliveryAttemptSchema.parse({
|
|
34
|
+
...rest,
|
|
35
|
+
recipientAddress: loggedAddress(registry, entry),
|
|
36
|
+
});
|
|
18
37
|
const stored = await append(db, {
|
|
19
38
|
aggregateId: attemptId,
|
|
20
39
|
aggregateType: "deliveryAttempt",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { NotifyPriority } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
export declare const DELIVERY_CHANNEL_EXTENSION: "deliveryChannel";
|
|
3
|
-
export { DELIVERY_ATTEMPT_EVENT, DELIVERY_CHANNEL_CELL_COMPONENT, DELIVERY_ERROR_CELL_COMPONENT, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_RESUBSCRIBE_PATH, DELIVERY_STATUS_CELL_COMPONENT, DELIVERY_TIME_CELL_COMPONENT, DELIVERY_TYPE_CELL_COMPONENT, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobNames, DeliveryQueries, DeliveryStatus, type DeliveryStatusValue, } from "./public-names.js";
|
|
3
|
+
export { DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT, DELIVERY_ATTEMPT_EVENT, DELIVERY_CHANNEL_CELL_COMPONENT, DELIVERY_ERROR_CELL_COMPONENT, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_RESUBSCRIBE_PATH, DELIVERY_STATUS_CELL_COMPONENT, DELIVERY_TIME_CELL_COMPONENT, DELIVERY_TYPE_CELL_COMPONENT, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobNames, DeliveryQueries, DeliveryStatus, type DeliveryStatusValue, } from "./public-names.js";
|
|
4
4
|
export declare const deliveryPriorityRank: Record<NotifyPriority, number>;
|
|
5
5
|
export declare const DeliveryJobs: {
|
|
6
6
|
readonly render: string;
|
|
@@ -3,7 +3,7 @@ import { DELIVERY_FEATURE, DeliveryJobNames } from "./public-names.js";
|
|
|
3
3
|
// Extension-point name for delivery-channel plugins (channel-email,
|
|
4
4
|
// channel-in-app, channel-push, ...).
|
|
5
5
|
export const DELIVERY_CHANNEL_EXTENSION = "deliveryChannel";
|
|
6
|
-
export { DELIVERY_ATTEMPT_EVENT, DELIVERY_CHANNEL_CELL_COMPONENT, DELIVERY_ERROR_CELL_COMPONENT, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_RESUBSCRIBE_PATH, DELIVERY_STATUS_CELL_COMPONENT, DELIVERY_TIME_CELL_COMPONENT, DELIVERY_TYPE_CELL_COMPONENT, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobNames, DeliveryQueries, DeliveryStatus, } from "./public-names.js";
|
|
6
|
+
export { DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT, DELIVERY_ATTEMPT_EVENT, DELIVERY_CHANNEL_CELL_COMPONENT, DELIVERY_ERROR_CELL_COMPONENT, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_RESUBSCRIBE_PATH, DELIVERY_STATUS_CELL_COMPONENT, DELIVERY_TIME_CELL_COMPONENT, DELIVERY_TYPE_CELL_COMPONENT, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobNames, DeliveryQueries, DeliveryStatus, } from "./public-names.js";
|
|
7
7
|
// notify() priority → BullMQ job priority. Lower number = processed first; all
|
|
8
8
|
// > 0 so prioritised delivery jobs never mix with BullMQ's "0 = unprioritised
|
|
9
9
|
// FIFO" bucket. critical jobs jump ahead of normal/low in the worker queue.
|
|
@@ -36,3 +36,4 @@ export declare const deliveryAttemptSchema: z.ZodObject<{
|
|
|
36
36
|
confirmed: z.ZodOptional<z.ZodLiteral<false>>;
|
|
37
37
|
}, z.core.$strip>;
|
|
38
38
|
export type DeliveryAttemptPayload = z.infer<typeof deliveryAttemptSchema>;
|
|
39
|
+
export declare const deliveryAttemptAddressErasedSchema: z.ZodObject<{}, z.core.$strip>;
|
package/dist/delivery/events.js
CHANGED
|
@@ -28,3 +28,5 @@ export const deliveryAttemptSchema = z.object({
|
|
|
28
28
|
// Provider accepted the request but does not confirm delivery (e.g. Teams 202).
|
|
29
29
|
confirmed: z.literal(false).optional(),
|
|
30
30
|
});
|
|
31
|
+
// No payload: the event only marks the attempt row's address as erased.
|
|
32
|
+
export const deliveryAttemptAddressErasedSchema = z.object({});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
export type DeliveryFeatureOptions = {
|
|
3
3
|
readonly access?: readonly string[];
|
|
4
|
+
readonly attemptLogRetentionDays?: number | false;
|
|
4
5
|
};
|
|
5
6
|
export declare function createDeliveryFeature(options?: DeliveryFeatureOptions): FeatureDefinition;
|
package/dist/delivery/feature.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { upsertByPk } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
|
+
import { updateMany, upsertByPk } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { access, defineFeature, i18nKey, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { ConsumerLagError } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
4
|
+
import { resolveAttemptLogRetentionDays, runAttemptLogRetention } from "./attempt-log-retention.js";
|
|
5
|
+
import { DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT, DELIVERY_ATTEMPT_EVENT, DELIVERY_CHANNEL_CELL_COMPONENT, DELIVERY_CHANNEL_EXTENSION, DELIVERY_ERROR_CELL_COMPONENT, DELIVERY_LOG_SCREEN_ID, DELIVERY_STATUS_CELL_COMPONENT, DELIVERY_TIME_CELL_COMPONENT, DELIVERY_TYPE_CELL_COMPONENT, DeliveryJobNames, DeliveryQueries, } from "./constants.js";
|
|
6
|
+
import { deliveryAttemptAddressErasedSchema, deliveryAttemptSchema } from "./events.js";
|
|
5
7
|
import { logQuery } from "./handlers/log.query.js";
|
|
6
8
|
import { preferencesQuery } from "./handlers/preferences.query.js";
|
|
7
9
|
import { resubscribeAddressWrite } from "./handlers/resubscribe-address.write.js";
|
|
@@ -12,8 +14,10 @@ import { unsubscribeUserWrite } from "./handlers/unsubscribe-user.write.js";
|
|
|
12
14
|
import { DELIVERY_I18N } from "./i18n.js";
|
|
13
15
|
import { deliveryRenderJob, deliverySendJob } from "./jobs.js";
|
|
14
16
|
import { deliveryAttemptsTable, deliveryAttemptsTableMeta, notificationAddressOptOutEntity, notificationAddressOptOutsTable, notificationPreferenceEntity, notificationPreferencesTable, } from "./tables.js";
|
|
17
|
+
const ATTEMPT_LOG_RETENTION_ESCAPE_HATCH_REASON = "prunes delivery attempt events and log rows of every tenant";
|
|
15
18
|
export function createDeliveryFeature(options) {
|
|
16
19
|
const resolvedAccess = options?.access ?? access.admin;
|
|
20
|
+
const attemptLogRetentionDays = resolveAttemptLogRetentionDays(options?.attemptLogRetentionDays);
|
|
17
21
|
return defineFeature("delivery", (r) => {
|
|
18
22
|
r.describe("The notification dispatch core: call `ctx.notify(notificationType, { to, route, data, priority, idempotencyKey })` from any handler to fan out a notification across all registered channels (email, in-app, push). It stores per-user channel preferences in the `notification-preference` entity, opt-outs for no-account recipient addresses in `notification-address-opt-out` (keyed by a blind-index hash, never the plaintext address), logs every attempt to `store_delivery_attempts`, and enforces idempotency and rate-limiting \u2014 add `channel-email`, `channel-in-app`, or `channel-push` on top to actually send anything. Unsubscribe/resubscribe links are served by `createUnsubscribeRoutes({ secret })` mounted via the app's `extraRoutes` at `/api/delivery/unsubscribe` and `/api/delivery/resubscribe`: `GET /unsubscribe?token=` renders a confirmation page (no write), `POST /unsubscribe` performs the opt-out (RFC 8058 one-click, token from the form body or query), `POST /resubscribe` undoes it (JSON `{token}` or form body only, never the query — reachable by a mail-client link prefetcher) — sign links with `signUnsubscribeToken` / `signAddressUnsubscribeToken` using the same secret. `channel-email` sets `List-Unsubscribe` / `List-Unsubscribe-Post` automatically when a message's `data.unsubscribeUrl` points at the unsubscribe route.");
|
|
19
23
|
r.uiHints({
|
|
@@ -54,6 +58,11 @@ export function createDeliveryFeature(options) {
|
|
|
54
58
|
recipientAddress: { personal: { of: "recipientId", whenAbsent: "tenant" } },
|
|
55
59
|
},
|
|
56
60
|
});
|
|
61
|
+
// Written when the recipient is forgotten: clears the address on the projection row and, being
|
|
62
|
+
// an event, survives a projection rebuild (a plain UPDATE would be undone by the replay).
|
|
63
|
+
r.defineEvent("attempt-address-erased", deliveryAttemptAddressErasedSchema, {
|
|
64
|
+
piiFields: "none",
|
|
65
|
+
});
|
|
57
66
|
// Inline projection that materialises every delivery attempt into
|
|
58
67
|
// deliveryAttemptsTable. Runs in the SAME transaction as the low-level
|
|
59
68
|
// append(), so callers see their write immediately — no dispatcher
|
|
@@ -89,6 +98,9 @@ export function createDeliveryFeature(options) {
|
|
|
89
98
|
confirmed: p.confirmed ?? null,
|
|
90
99
|
});
|
|
91
100
|
},
|
|
101
|
+
[DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT]: async (event, tx) => {
|
|
102
|
+
await updateMany(tx, deliveryAttemptsTable, { recipientAddress: null }, { id: event.aggregateId });
|
|
103
|
+
},
|
|
92
104
|
},
|
|
93
105
|
});
|
|
94
106
|
// Extension point: delivery-channels (email/in-app/push). Renderer-
|
|
@@ -106,6 +118,30 @@ export function createDeliveryFeature(options) {
|
|
|
106
118
|
// from the delivery-service, hence manual trigger.
|
|
107
119
|
r.job(DeliveryJobNames.render, { trigger: { manual: true }, retries: 3, backoff: "exponential" }, deliveryRenderJob);
|
|
108
120
|
r.job(DeliveryJobNames.send, { trigger: { manual: true }, retries: 3, backoff: "exponential" }, deliverySendJob);
|
|
121
|
+
if (attemptLogRetentionDays !== undefined) {
|
|
122
|
+
r.job({
|
|
123
|
+
name: DeliveryJobNames.attemptLogRetention,
|
|
124
|
+
trigger: { cron: "15 3 * * *" },
|
|
125
|
+
concurrency: "skip",
|
|
126
|
+
escapeHatch: { reason: ATTEMPT_LOG_RETENTION_ESCAPE_HATCH_REASON },
|
|
127
|
+
handler: async (_payload, ctx) => {
|
|
128
|
+
const db = ctx.db.unsafeRaw(); // @cast-boundary db-operator — jobs never run inside a DbTx
|
|
129
|
+
try {
|
|
130
|
+
const pruned = await runAttemptLogRetention(db, {
|
|
131
|
+
olderThanDays: attemptLogRetentionDays,
|
|
132
|
+
});
|
|
133
|
+
ctx.log.info("delivery attempt log pruned", pruned);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (!(error instanceof ConsumerLagError))
|
|
137
|
+
throw error;
|
|
138
|
+
ctx.log.warn("delivery attempt log retention postponed: an event consumer lags", {
|
|
139
|
+
consumer: error.laggingConsumer,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
|
109
145
|
const handlers = {
|
|
110
146
|
setPreference: r.writeHandler(setPreferenceWrite),
|
|
111
147
|
unsubscribeAddress: r.writeHandler(unsubscribeAddressWrite),
|
package/dist/delivery/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export { hashUnsubscribeAddress } from "./address-opt-out.js";
|
|
2
|
+
export { type AttemptLogRetentionOptions, DEFAULT_ATTEMPT_LOG_RETENTION_DAYS, runAttemptLogRetention, } from "./attempt-log-retention.js";
|
|
2
3
|
export { type ChatWebhookChannelOptions, chatMessageText, createChatWebhookChannel, resolveChatWebhookTarget, toChannelResult, } from "./chat-webhook-channel.js";
|
|
3
4
|
export { type ChatSendFailureCode, type ChatSendResult, type ChatWebhookRequest, type ChatWebhookResponse, type ChatWebhookTarget, chatConnectionNameSchema, chatWebhookUrlSchema, checkChatWebhookTarget, DEFAULT_CHAT_TIMEOUT_MS, postChatWebhook, truncateChars, } from "./chat-webhook-sender.js";
|
|
4
5
|
export type { DeliveryStatusValue } from "./constants.js";
|
|
5
|
-
export { DELIVERY_CHANNEL_EXTENSION, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobs, DeliveryQueries, DeliveryStatus, } from "./constants.js";
|
|
6
|
+
export { DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT, DELIVERY_CHANNEL_EXTENSION, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobs, DeliveryQueries, DeliveryStatus, } from "./constants.js";
|
|
6
7
|
export { collectChannels, createDeliveryService, type DeliveryServiceOptions, type KillSwitchResolver, type RateLimitConfig, } from "./delivery-service.js";
|
|
7
8
|
export { createDeliveryFeature, type DeliveryFeatureOptions } from "./feature.js";
|
|
8
9
|
export { createDeliveryNotifyFactory } from "./notify-factory.js";
|
package/dist/delivery/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { hashUnsubscribeAddress } from "./address-opt-out.js";
|
|
2
|
+
export { DEFAULT_ATTEMPT_LOG_RETENTION_DAYS, runAttemptLogRetention, } from "./attempt-log-retention.js";
|
|
2
3
|
export { chatMessageText, createChatWebhookChannel, resolveChatWebhookTarget, toChannelResult, } from "./chat-webhook-channel.js";
|
|
3
4
|
export { chatConnectionNameSchema, chatWebhookUrlSchema, checkChatWebhookTarget, DEFAULT_CHAT_TIMEOUT_MS, postChatWebhook, truncateChars, } from "./chat-webhook-sender.js";
|
|
4
|
-
export { DELIVERY_CHANNEL_EXTENSION, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobs, DeliveryQueries, DeliveryStatus, } from "./constants.js";
|
|
5
|
+
export { DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT, DELIVERY_CHANNEL_EXTENSION, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobs, DeliveryQueries, DeliveryStatus, } from "./constants.js";
|
|
5
6
|
export { collectChannels, createDeliveryService, } from "./delivery-service.js";
|
|
6
7
|
export { createDeliveryFeature } from "./feature.js";
|
|
7
8
|
export { createDeliveryNotifyFactory } from "./notify-factory.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function maskRecipientAddress(address: string | null): string | null;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { isPiiCiphertext } from "@cosmicdrift/kumiko-framework/crypto";
|
|
2
|
+
const MASK = "***";
|
|
3
|
+
const VISIBLE_TAIL_LENGTH = 4;
|
|
4
|
+
const MIN_LENGTH_FOR_VISIBLE_TAIL = 8;
|
|
5
|
+
function maskedUrl(address) {
|
|
6
|
+
if (!URL.canParse(address))
|
|
7
|
+
return undefined;
|
|
8
|
+
const url = new URL(address);
|
|
9
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
10
|
+
return undefined;
|
|
11
|
+
return `${url.origin}/${MASK}`;
|
|
12
|
+
}
|
|
13
|
+
function maskedEmail(address) {
|
|
14
|
+
const parts = address.split("@");
|
|
15
|
+
const [local, domain] = parts;
|
|
16
|
+
if (parts.length !== 2 || !local || domain === undefined)
|
|
17
|
+
return undefined;
|
|
18
|
+
return `${local.charAt(0)}${MASK}@${domain}`;
|
|
19
|
+
}
|
|
20
|
+
// The attempt log is an audit trail, not a contact list: it keeps enough of the address to
|
|
21
|
+
// recognise where a message went, never the full value. URLs go first because webhook URLs can
|
|
22
|
+
// contain an "@" and their path carries the secret.
|
|
23
|
+
export function maskRecipientAddress(address) {
|
|
24
|
+
if (address === null)
|
|
25
|
+
return null;
|
|
26
|
+
return maskPlainAddress(address);
|
|
27
|
+
}
|
|
28
|
+
function maskPlainAddress(address) {
|
|
29
|
+
// isPiiCiphertext is a type predicate; its false branch would narrow `string` to `never`.
|
|
30
|
+
if (isPiiCiphertext(address))
|
|
31
|
+
return MASK;
|
|
32
|
+
return maskStructuredOrTail(address);
|
|
33
|
+
}
|
|
34
|
+
function maskStructuredOrTail(address) {
|
|
35
|
+
const structured = maskedUrl(address) ?? maskedEmail(address);
|
|
36
|
+
if (structured !== undefined)
|
|
37
|
+
return structured;
|
|
38
|
+
if (address.length < MIN_LENGTH_FOR_VISIBLE_TAIL)
|
|
39
|
+
return MASK;
|
|
40
|
+
return `${MASK}${address.slice(-VISIBLE_TAIL_LENGTH)}`;
|
|
41
|
+
}
|
|
@@ -34,4 +34,6 @@ export declare const DELIVERY_ATTEMPT_EVENT: "delivery:event:attempt";
|
|
|
34
34
|
export declare const DeliveryJobNames: {
|
|
35
35
|
readonly render: "render";
|
|
36
36
|
readonly send: "send";
|
|
37
|
+
readonly attemptLogRetention: "attempt-log-retention";
|
|
37
38
|
};
|
|
39
|
+
export declare const DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT: "delivery:event:attempt-address-erased";
|
|
@@ -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
|
|
@@ -48,4 +48,7 @@ export const DELIVERY_ATTEMPT_EVENT = "delivery:event:attempt";
|
|
|
48
48
|
export const DeliveryJobNames = {
|
|
49
49
|
render: "render",
|
|
50
50
|
send: "send",
|
|
51
|
+
attemptLogRetention: "attempt-log-retention",
|
|
51
52
|
};
|
|
53
|
+
// Clears recipientAddress on an attempt row after its recipient was forgotten.
|
|
54
|
+
export const DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT = "delivery:event:attempt-address-erased";
|
|
@@ -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;
|
package/dist/delivery/types.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ export type DeliveryChannel = {
|
|
|
34
34
|
readonly name: string;
|
|
35
35
|
readonly mode: DeliveryChannelMode;
|
|
36
36
|
resolve?(userId: string, ctx: ChannelContext): Promise<string | null>;
|
|
37
|
+
readonly addressKind?: "connection-name";
|
|
37
38
|
render?(message: ChannelMessage, ctx: ChannelContext): Promise<RenderedMessage>;
|
|
38
39
|
send(address: string, message: ChannelMessage, ctx: ChannelContext, rendered?: RenderedMessage): Promise<ChannelResult>;
|
|
39
40
|
};
|