@cosmicdrift/kumiko-bundled-features 0.350.0 → 0.352.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/audit/handlers/list.query.js +1 -1
- package/dist/auth-email-password/email-templates.js +1 -1
- package/dist/auth-email-password/handlers/invite-create.write.js +1 -1
- package/dist/auth-email-password/handlers/signup-request.write.js +1 -1
- package/dist/auth-email-password/handlers/token-request-handler.d.ts +1 -1
- package/dist/auth-email-password/reset-token.d.ts +1 -1
- package/dist/auth-email-password/unlock-token.d.ts +1 -1
- package/dist/auth-email-password/verification-token.d.ts +1 -1
- package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +1 -1
- package/dist/auth-mfa/handlers/enable-confirm.write.js +1 -1
- package/dist/auth-mfa/hmac-token-codec.d.ts +1 -1
- package/dist/auth-mfa/hmac-token-codec.js +1 -1
- package/dist/auth-mfa/mfa-challenge-token.d.ts +1 -1
- package/dist/auth-mfa/mfa-challenge-token.js +1 -1
- package/dist/auth-mfa/mfa-preauth-setup-token.d.ts +1 -1
- package/dist/auth-mfa/mfa-preauth-setup-token.js +1 -1
- package/dist/auth-mfa/mfa-setup-token.d.ts +1 -1
- package/dist/auth-mfa/mfa-setup-token.js +1 -1
- package/dist/billing-foundation/constants.d.ts +2 -2
- package/dist/billing-foundation/constants.js +3 -12
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +1 -1
- package/dist/billing-foundation/consumer-protection/termination-mail.js +1 -1
- package/dist/billing-foundation/feature.js +2 -9
- package/dist/billing-foundation/handlers/sync-subscription.write.js +3 -6
- package/dist/cap-counter/book-cap-usage.d.ts +5 -1
- package/dist/cap-counter/book-cap-usage.js +66 -45
- 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/enforce-cap.d.ts +1 -1
- package/dist/cap-counter/enforce-cap.js +1 -8
- 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/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 +2 -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/handlers/log.query.d.ts +1 -2
- package/dist/delivery/handlers/log.query.js +1 -1
- 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 +3 -0
- package/dist/delivery/types.d.ts +1 -0
- package/dist/delivery/types.js +6 -2
- package/dist/feature-toggles/handlers/set.write.js +1 -1
- package/dist/form-draft/db/queries/cleanup.d.ts +1 -1
- package/dist/form-draft/db/queries/cleanup.js +1 -1
- package/dist/form-draft/db/queries/owned-file-refs.d.ts +1 -1
- package/dist/form-draft/lookup.d.ts +1 -1
- package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +1 -1
- package/dist/inbound-mail-foundation/oauth-access-token.d.ts +1 -1
- package/dist/inbound-mail-foundation/oauth-access-token.js +1 -1
- package/dist/inbound-mail-foundation/oauth-state.d.ts +1 -1
- package/dist/inbound-mail-foundation/oauth-state.js +1 -1
- package/dist/inbound-mail-foundation/watch-supervisor.js +1 -1
- package/dist/inbound-provider-imap/feature.js +1 -1
- package/dist/inbound-provider-imap/imap-client.js +1 -1
- package/dist/jobs/job-instant.d.ts +1 -1
- package/dist/jobs/job-instant.js +1 -1
- package/dist/ledger/entity.d.ts +3 -0
- package/dist/metrics/compute.d.ts +1 -1
- package/dist/metrics/feature.js +1 -1
- package/dist/metrics/types.d.ts +1 -1
- package/dist/metrics/window.d.ts +1 -1
- package/dist/metrics/window.js +1 -1
- package/dist/personal-access-tokens/expiry.js +1 -1
- package/dist/personal-access-tokens/handlers/create.write.js +1 -1
- package/dist/personal-access-tokens/handlers/revoke.write.js +1 -1
- package/dist/personal-access-tokens/revoke-for-user.js +1 -1
- 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/sessions/handlers/revoke-all-for-user.write.js +1 -1
- package/dist/sessions/handlers/revoke-all-others.write.js +1 -1
- package/dist/sessions/handlers/revoke.write.js +1 -1
- package/dist/sessions/session-callbacks.js +1 -1
- package/dist/shared/grace-period.d.ts +2 -1
- package/dist/shared/grace-period.js +3 -7
- package/dist/shared/row-bound-grant.d.ts +1 -1
- package/dist/shared/signed-token.d.ts +1 -1
- package/dist/shared/signed-token.js +1 -1
- package/dist/subscription-mollie/verify-webhook.js +1 -1
- package/dist/subscription-stripe/verify-webhook.js +1 -1
- package/dist/template-resolver/api.d.ts +1 -1
- package/dist/template-resolver/handlers/by-slug.query.d.ts +1 -1
- package/dist/template-resolver/handlers/by-tenant.query.d.ts +2 -2
- package/dist/template-resolver/handlers/collection-item.query.d.ts +1 -1
- package/dist/template-resolver/handlers/collection-list.query.d.ts +1 -1
- package/dist/template-resolver/handlers/collection-shared.d.ts +2 -2
- package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -1
- package/dist/template-resolver/handlers/list.query.d.ts +1 -1
- package/dist/template-resolver/table.d.ts +9 -1
- package/dist/template-resolver/user-content-table.d.ts +4 -0
- package/dist/tenant/handlers/team-list.query.d.ts +1 -1
- package/dist/tenant/handlers/team-list.query.js +1 -1
- package/dist/tenant/invitation-table.d.ts +2 -0
- package/dist/tenant/invited-membership.d.ts +1 -1
- package/dist/tenant/invited-membership.js +1 -1
- package/dist/tenant/schema/tenant.d.ts +2 -0
- package/dist/tenant-handover/grant.d.ts +1 -1
- 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/user/schema/user.d.ts +2 -0
- package/dist/user-data-rights/db/queries/export-jobs.d.ts +1 -1
- package/dist/user-data-rights/db/queries/forget-cleanup.d.ts +2 -2
- package/dist/user-data-rights/db/queries/forget-cleanup.js +2 -8
- package/dist/user-data-rights/deletion-token.d.ts +1 -1
- package/dist/user-data-rights/email-templates.js +1 -1
- package/dist/user-data-rights/handlers/list-download-attempts.query.js +1 -1
- package/dist/user-data-rights/handlers/my-audit-log.query.js +1 -1
- package/dist/user-data-rights/schema/export-job.d.ts +2 -0
- package/dist/user-data-rights-defaults/hooks/delivery-attempt.userdata-hook.js +25 -6
- package/dist/waitlist/entity.d.ts +2 -0
- package/dist/waitlist/handlers/invite.write.js +1 -1
- package/dist/waitlist/handlers/submit.write.js +1 -1
- package/package.json +10 -10
- 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/legal-pages/changes.json +6 -0
- package/src/renderer-simple/changes.json +6 -0
- package/src/tier-engine/changes.json +6 -0
|
@@ -16,6 +16,12 @@ export const CAP_COUNTER_ROLLING_AGGREGATE_TYPE = "cap-counter-rolling";
|
|
|
16
16
|
// Beide MÜSSEN konsistent sein (drift-pin im feature-test).
|
|
17
17
|
export const ROLLING_INCREMENTED_EVENT_SHORT = "rolling-incremented";
|
|
18
18
|
export const ROLLING_INCREMENTED_EVENT_QN = "cap-counter:event:rolling-incremented";
|
|
19
|
+
// Gives back a reserved rolling amount; the window sum is incremented minus released.
|
|
20
|
+
export const ROLLING_RELEASED_EVENT_SHORT = "rolling-released";
|
|
21
|
+
export const ROLLING_RELEASED_EVENT_QN = "cap-counter:event:rolling-released";
|
|
22
|
+
// A reservation row older than this is treated as a lost release and given back before the next
|
|
23
|
+
// reservation for the same cap, which bounds the over-count a failed release can leave behind.
|
|
24
|
+
export const CAP_RESERVATION_TTL_MINUTES = 60;
|
|
19
25
|
// Qualified write handler names (QN format: scope:type:name).
|
|
20
26
|
export const CapCounterHandlers = {
|
|
21
27
|
increment: "cap-counter:write:increment",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
2
1
|
import { type HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
2
|
import { KumikoError } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
4
4
|
/**
|
|
5
5
|
* Cap-Toleranz-Profile — pro Cap-Typ asymmetrisch. Quelle: Memory
|
|
6
6
|
* `project_pricing_byok_caps` §3 + `docs/plans/marketing/produkt/
|
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
2
1
|
import { createEntityExecutor } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
2
|
import { KumikoError, reraiseAsKumikoError } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
4
4
|
import { markCapSoftWarned, readRollingCapUsage } from "./book-cap-usage.js";
|
|
5
5
|
import { capCounterEntity } from "./entity.js";
|
|
6
|
-
// Temporal globally provided by the framework's polyfill init
|
|
7
|
-
// (ensureTemporalPolyfill() in time/polyfill.ts, called from
|
|
8
|
-
// setupTestStack/boot). Importing from "temporal-polyfill" gives us
|
|
9
|
-
// the polyfill-package types which don't quite match drizzle's
|
|
10
|
-
// `instant()`-customType (temporal-spec narrowing of `until(...).sign`).
|
|
11
|
-
// Mirror the audit-handler pattern: rely on the global ambient
|
|
12
|
-
// declaration from temporal-spec.
|
|
13
6
|
const { table } = createEntityExecutor("cap-counter", capCounterEntity);
|
|
14
7
|
export const CAP_TOLERANCES = {
|
|
15
8
|
/** Mails / Tokens — billig, BYOK ab Pro. Burst-Buffer großzügig. */
|
|
@@ -45,13 +45,14 @@
|
|
|
45
45
|
// config, kein secrets, kein tenant-feature nötig. Tenant-Scoping kommt
|
|
46
46
|
// vom Framework-Default (Base-Column tenantId).
|
|
47
47
|
import { access, defineEntityListHandler, defineFeature, } from "@cosmicdrift/kumiko-framework/engine";
|
|
48
|
-
import { CAP_COUNTER_FEATURE, CAP_COUNTER_LIST_SCREEN_ID, ROLLING_INCREMENTED_EVENT_SHORT, } from "./constants.js";
|
|
48
|
+
import { CAP_COUNTER_FEATURE, CAP_COUNTER_LIST_SCREEN_ID, ROLLING_INCREMENTED_EVENT_SHORT, ROLLING_RELEASED_EVENT_SHORT, } from "./constants.js";
|
|
49
49
|
import { capCounterEntity } from "./entity.js";
|
|
50
50
|
import { getCounterQuery } from "./handlers/get-counter.query.js";
|
|
51
51
|
import { incrementCapHandler } from "./handlers/increment.write.js";
|
|
52
|
-
import { incrementRollingCapHandler, rollingIncrementedSchema, } from "./handlers/increment-rolling.write.js";
|
|
52
|
+
import { incrementRollingCapHandler, rollingIncrementedSchema, rollingReleasedSchema, } from "./handlers/increment-rolling.write.js";
|
|
53
53
|
import { markSoftWarnedHandler } from "./handlers/mark-soft-warned.write.js";
|
|
54
54
|
import { CAP_COUNTER_I18N } from "./i18n.js";
|
|
55
|
+
import { capReservationsTableMeta } from "./tables.js";
|
|
55
56
|
export const capCounterFeature = defineFeature(CAP_COUNTER_FEATURE, (r) => {
|
|
56
57
|
r.describe("Tracks per-tenant usage against configurable limits using two complementary storage models: calendar-period counters (one projection row per tenant/capName/period, reset implicitly by period rollover) and rolling-window counters (append-only event stream, no projection). Use `enforceCap` / `enforceRollingCap` (or the `withCapEnforcement` / `withRollingCapEnforcement` handler wrappers) in your write-handlers to check limits with soft-warn and hard-block tolerances; call `enforceCapAndMaybeNotify` when you also want to trigger a delivery notification on soft-threshold hits.");
|
|
57
58
|
r.uiHints({
|
|
@@ -60,12 +61,16 @@ export const capCounterFeature = defineFeature(CAP_COUNTER_FEATURE, (r) => {
|
|
|
60
61
|
recommended: false,
|
|
61
62
|
});
|
|
62
63
|
r.entity("cap-counter", capCounterEntity);
|
|
64
|
+
r.storeTable(capReservationsTableMeta, {
|
|
65
|
+
reason: "coordination.cap_reservation_ledger",
|
|
66
|
+
});
|
|
63
67
|
// Custom Domain-Event für Rolling-Counter. r.defineEvent registriert
|
|
64
68
|
// das Schema beim Registry; ctx.unsafeAppendEvent im Handler nutzt
|
|
65
69
|
// dasselbe Schema für Append-Time-Validation. QN nach Prefixing:
|
|
66
70
|
// "cap-counter:event:rolling-incremented" (siehe
|
|
67
71
|
// ROLLING_INCREMENTED_EVENT_QN).
|
|
68
72
|
r.defineEvent(ROLLING_INCREMENTED_EVENT_SHORT, rollingIncrementedSchema, { piiFields: "none" });
|
|
73
|
+
r.defineEvent(ROLLING_RELEASED_EVENT_SHORT, rollingReleasedSchema, { piiFields: "none" });
|
|
69
74
|
// Custom write-handlers.
|
|
70
75
|
// - increment: Calendar-Period (CRUD via projection-row).
|
|
71
76
|
// - increment-rolling: Rolling-Window (Custom-Event, no projection).
|
|
@@ -6,4 +6,9 @@ export declare const rollingIncrementedSchema: z.ZodObject<{
|
|
|
6
6
|
capName: z.ZodString;
|
|
7
7
|
amount: z.ZodDefault<z.ZodNumber>;
|
|
8
8
|
}, z.core.$strip>;
|
|
9
|
+
/** Payload of the event that gives a reserved rolling amount back. */
|
|
10
|
+
export declare const rollingReleasedSchema: z.ZodObject<{
|
|
11
|
+
capName: z.ZodString;
|
|
12
|
+
amount: z.ZodNumber;
|
|
13
|
+
}, z.core.$strip>;
|
|
9
14
|
export declare const incrementRollingCapHandler: WriteHandlerDef;
|
|
@@ -31,6 +31,10 @@ const incrementRollingSchema = z.object({
|
|
|
31
31
|
/** Schema des emittierten Custom-Events. Identisch zum Input-Schema:
|
|
32
32
|
* der Caller zahlt amount, wir hängen es 1:1 an den Stream. */
|
|
33
33
|
export const rollingIncrementedSchema = incrementRollingSchema;
|
|
34
|
+
/** Payload of the event that gives a reserved rolling amount back. */
|
|
35
|
+
export const rollingReleasedSchema = incrementRollingSchema.extend({
|
|
36
|
+
amount: z.number().int().positive(),
|
|
37
|
+
});
|
|
34
38
|
// Rolling-Increment-Handler — append-only. Race-frei: zwei parallele
|
|
35
39
|
// Increments für (tenant, cap) hängen sich am selben aggregate-stream
|
|
36
40
|
// in unterschiedlichen versions auf, das event-store ordert.
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export { capCounterAggregateId, rollingCapAggregateId } from "./aggregate-id.js";
|
|
2
2
|
export { type BookCapUsageOptions, bookCapUsage, type MarkCapSoftWarnedOptions, markCapSoftWarned, type ReadRollingCapUsageOptions, readRollingCapUsage, } from "./book-cap-usage.js";
|
|
3
|
-
export { CAP_COUNTER_FEATURE, CAP_COUNTER_ROLLING_AGGREGATE_TYPE, CapCounterHandlers, CapCounterQueries, ROLLING_INCREMENTED_EVENT_QN, ROLLING_INCREMENTED_EVENT_SHORT, } from "./constants.js";
|
|
3
|
+
export { CAP_COUNTER_FEATURE, CAP_COUNTER_ROLLING_AGGREGATE_TYPE, CapCounterHandlers, CapCounterQueries, ROLLING_INCREMENTED_EVENT_QN, ROLLING_INCREMENTED_EVENT_SHORT, ROLLING_RELEASED_EVENT_QN, ROLLING_RELEASED_EVENT_SHORT, } from "./constants.js";
|
|
4
4
|
export { CAP_TOLERANCES, CapExceededError, type CapToleranceProfile, type CapToleranceProfileName, currentCalendarMonthStartIso, type EnforceCapResult, enforceCap, enforceCapAndMaybeNotify, enforceRollingCap, enforceRollingCapAndMaybeNotify, enforceStockCap, type SoftHitNotifier, type StockCapResult, } from "./enforce-cap.js";
|
|
5
5
|
export { capCounterEntity } from "./entity.js";
|
|
6
6
|
export { capCounterFeature } from "./feature.js";
|
|
7
|
-
export {
|
|
7
|
+
export { readPayloadField } from "./payload-field.js";
|
|
8
|
+
export { type CapGuardContext, type CapLimitContext, createStockCapGuard, type StockCapGuard, type StockCapSpec, } from "./stock-cap-guard.js";
|
|
9
|
+
export { createValueCapGuard, type ValueCapGuard, type ValueCapSpec, } from "./value-cap-guard.js";
|
|
8
10
|
export { type CalendarCapDef, type CalendarCapResolver, type RollingCapDef, type RollingCapResolver, withCapEnforcement, withRollingCapEnforcement, } from "./with-cap-enforcement.js";
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// Public API of the cap-counter bundled-feature.
|
|
2
2
|
export { capCounterAggregateId, rollingCapAggregateId } from "./aggregate-id.js";
|
|
3
3
|
export { bookCapUsage, markCapSoftWarned, readRollingCapUsage, } from "./book-cap-usage.js";
|
|
4
|
-
export { CAP_COUNTER_FEATURE, CAP_COUNTER_ROLLING_AGGREGATE_TYPE, CapCounterHandlers, CapCounterQueries, ROLLING_INCREMENTED_EVENT_QN, ROLLING_INCREMENTED_EVENT_SHORT, } from "./constants.js";
|
|
4
|
+
export { CAP_COUNTER_FEATURE, CAP_COUNTER_ROLLING_AGGREGATE_TYPE, CapCounterHandlers, CapCounterQueries, ROLLING_INCREMENTED_EVENT_QN, ROLLING_INCREMENTED_EVENT_SHORT, ROLLING_RELEASED_EVENT_QN, ROLLING_RELEASED_EVENT_SHORT, } from "./constants.js";
|
|
5
5
|
export { CAP_TOLERANCES, CapExceededError, currentCalendarMonthStartIso, enforceCap, enforceCapAndMaybeNotify, enforceRollingCap, enforceRollingCapAndMaybeNotify, enforceStockCap, } from "./enforce-cap.js";
|
|
6
6
|
export { capCounterEntity } from "./entity.js";
|
|
7
7
|
export { capCounterFeature } from "./feature.js";
|
|
8
|
+
export { readPayloadField } from "./payload-field.js";
|
|
8
9
|
export { createStockCapGuard, } from "./stock-cap-guard.js";
|
|
10
|
+
export { createValueCapGuard, } from "./value-cap-guard.js";
|
|
9
11
|
export { withCapEnforcement, withRollingCapEnforcement, } from "./with-cap-enforcement.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function readPayloadField(payload: unknown, field: string): unknown;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-framework/utils";
|
|
2
|
+
// Create payloads are flat, entity update payloads carry the edited fields under `changes`
|
|
3
|
+
// (`{ id, version, changes }`), so a guard that follows one field has to look at both.
|
|
4
|
+
export function readPayloadField(payload, field) {
|
|
5
|
+
if (!isPlainObject(payload))
|
|
6
|
+
return undefined;
|
|
7
|
+
if (field in payload)
|
|
8
|
+
return payload[field];
|
|
9
|
+
const { changes } = payload;
|
|
10
|
+
return isPlainObject(changes) ? changes[field] : undefined;
|
|
11
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function toHandle(outcome) {
|
|
2
|
+
if (outcome === undefined)
|
|
3
|
+
return { release: async () => { }, confirmInTransaction: async () => { } };
|
|
4
|
+
if (typeof outcome === "function") {
|
|
5
|
+
return { release: outcome, confirmInTransaction: async () => { } };
|
|
6
|
+
}
|
|
7
|
+
return outcome;
|
|
8
|
+
}
|
|
9
|
+
// Runs `inner` first, then `own`; a failing `own` gives the inner reservation back before it
|
|
10
|
+
// throws. Release goes in reverse order, confirm covers both.
|
|
11
|
+
export function chainReservations(inner, own) {
|
|
12
|
+
if (!inner)
|
|
13
|
+
return own;
|
|
14
|
+
return async (event, ctx) => {
|
|
15
|
+
const innerHandle = toHandle(await inner(event, ctx));
|
|
16
|
+
let ownHandle;
|
|
17
|
+
try {
|
|
18
|
+
ownHandle = toHandle(await own(event, ctx));
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
try {
|
|
22
|
+
await innerHandle.release();
|
|
23
|
+
}
|
|
24
|
+
catch (releaseError) {
|
|
25
|
+
ctx.log?.error("releasing the inner cap reservation failed", { error: releaseError });
|
|
26
|
+
}
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
release: async () => {
|
|
31
|
+
try {
|
|
32
|
+
await ownHandle.release();
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
await innerHandle.release();
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
confirmInTransaction: async (confirm) => {
|
|
39
|
+
await innerHandle.confirmInTransaction(confirm);
|
|
40
|
+
await ownHandle.confirmInTransaction(confirm);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -4,6 +4,9 @@ import { type WriteFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
|
4
4
|
export type CapLimitContext = {
|
|
5
5
|
readonly config?: ConfigAccessor;
|
|
6
6
|
};
|
|
7
|
+
export type CapGuardContext = CapLimitContext & {
|
|
8
|
+
readonly db: TenantDb;
|
|
9
|
+
};
|
|
7
10
|
export type StockCapSpec<TCaps> = {
|
|
8
11
|
readonly table: SchemaTable | EntityTableMeta;
|
|
9
12
|
readonly limit: (caps: TCaps) => number;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { defineUnmanagedTable, index, instant, integer, table as pgTable, text, uuid, } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
const CAP_RESERVATIONS_TENANT_CAP_EXPIRES_INDEX = "store_cap_reservations_tenant_id_cap_name_expires_at_idx";
|
|
3
|
+
// One row per capacity that was booked before a handler transaction and is not settled yet. The
|
|
4
|
+
// handler transaction deletes it on its own commit (confirm); a release or the TTL sweep deletes it
|
|
5
|
+
// together with the counter decrement. Unmanaged: the rows are a coordination aid, not state with
|
|
6
|
+
// an event history.
|
|
7
|
+
export const capReservationsTable = pgTable("store_cap_reservations", {
|
|
8
|
+
id: uuid("id").primaryKey(),
|
|
9
|
+
tenantId: uuid("tenant_id").notNull(),
|
|
10
|
+
capName: text("cap_name").notNull(),
|
|
11
|
+
kind: text("kind").notNull().$type(),
|
|
12
|
+
// Calendar only: raw string the counter's aggregate id was derived from; a normalized instant could point at another counter.
|
|
13
|
+
periodStartIso: text("period_start_iso"),
|
|
14
|
+
amount: integer("amount").notNull(),
|
|
15
|
+
expiresAt: instant("expires_at").notNull(),
|
|
16
|
+
createdAt: instant("created_at").notNull(),
|
|
17
|
+
}, (t) => [index(CAP_RESERVATIONS_TENANT_CAP_EXPIRES_INDEX).on(t.tenantId, t.capName, t.expiresAt)]);
|
|
18
|
+
export const capReservationsTableMeta = defineUnmanagedTable({
|
|
19
|
+
tableName: "store_cap_reservations",
|
|
20
|
+
indexes: [
|
|
21
|
+
{
|
|
22
|
+
name: CAP_RESERVATIONS_TENANT_CAP_EXPIRES_INDEX,
|
|
23
|
+
columns: ["tenant_id", "cap_name", "expires_at"],
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
columns: [
|
|
27
|
+
{ name: "id", pgType: "uuid", notNull: true, primaryKey: true },
|
|
28
|
+
{ name: "tenant_id", pgType: "uuid", notNull: true },
|
|
29
|
+
{ name: "cap_name", pgType: "text", notNull: true },
|
|
30
|
+
{ name: "kind", pgType: "text", notNull: true },
|
|
31
|
+
{ name: "period_start_iso", pgType: "text", notNull: false },
|
|
32
|
+
{ name: "amount", pgType: "integer", notNull: true },
|
|
33
|
+
{ name: "expires_at", pgType: "timestamptz", notNull: true },
|
|
34
|
+
{ name: "created_at", pgType: "timestamptz", notNull: true },
|
|
35
|
+
],
|
|
36
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import type { WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { type WriteFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
|
+
import type { CapGuardContext, CapLimitContext } from "./stock-cap-guard.js";
|
|
5
|
+
export type ValueCapSpec<TCaps> = {
|
|
6
|
+
readonly field: string;
|
|
7
|
+
/** `undefined` = no lower bound for this tier. */
|
|
8
|
+
readonly min?: (caps: TCaps) => number | undefined;
|
|
9
|
+
/** `undefined` = no upper bound for this tier. */
|
|
10
|
+
readonly max?: (caps: TCaps) => number | undefined;
|
|
11
|
+
readonly code: string;
|
|
12
|
+
readonly i18nKey: string;
|
|
13
|
+
};
|
|
14
|
+
export type ValueCapGuard<TCaps> = {
|
|
15
|
+
readonly checkValueCap: (ctx: CapGuardContext, value: unknown, spec: ValueCapSpec<TCaps>) => Promise<WriteFailure | null>;
|
|
16
|
+
readonly withValueCap: (handler: WriteHandlerDef, spec: ValueCapSpec<TCaps>) => WriteHandlerDef;
|
|
17
|
+
};
|
|
18
|
+
export declare function createValueCapGuard<TCaps>(resolveTierCaps: (db: TenantDb, context: CapLimitContext) => Promise<TCaps>): ValueCapGuard<TCaps>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Per-tier bounds on a single written number (e.g. a retention period or a page size),
|
|
2
|
+
// next to the stock-cap guard, which bounds how many rows may exist.
|
|
3
|
+
import { UnprocessableError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
|
+
import { readPayloadField } from "./payload-field.js";
|
|
5
|
+
function isOutOfBounds(value, min, max) {
|
|
6
|
+
return (min !== undefined && value < min) || (max !== undefined && value > max);
|
|
7
|
+
}
|
|
8
|
+
export function createValueCapGuard(resolveTierCaps) {
|
|
9
|
+
async function checkValueCap(ctx, value, spec) {
|
|
10
|
+
// skip: an absent or non-numeric value is the schema's business, not a cap violation.
|
|
11
|
+
if (typeof value !== "number")
|
|
12
|
+
return null;
|
|
13
|
+
const caps = await resolveTierCaps(ctx.db, ctx.config ? { config: ctx.config } : {});
|
|
14
|
+
const min = spec.min?.(caps);
|
|
15
|
+
const max = spec.max?.(caps);
|
|
16
|
+
if (!isOutOfBounds(value, min, max))
|
|
17
|
+
return null;
|
|
18
|
+
return writeFailure(new UnprocessableError(spec.code, {
|
|
19
|
+
i18nKey: spec.i18nKey,
|
|
20
|
+
details: { field: spec.field, value, min, max },
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
function withValueCap(handler, spec) {
|
|
24
|
+
return {
|
|
25
|
+
...handler,
|
|
26
|
+
handler: async (event, ctx) => {
|
|
27
|
+
const failure = await checkValueCap(ctx, readPayloadField(event.payload, spec.field), spec);
|
|
28
|
+
return failure ?? handler.handler(event, ctx);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return { checkValueCap, withValueCap };
|
|
33
|
+
}
|
|
@@ -44,13 +44,11 @@ export type RollingCapResolver = (event: WriteEvent, ctx: HandlerContext) => Pro
|
|
|
44
44
|
/**
|
|
45
45
|
* Wrap a write-handler with rolling-window cap-enforcement.
|
|
46
46
|
*
|
|
47
|
-
* Same flow as `withCapEnforcement` but uses
|
|
48
|
-
* `
|
|
49
|
-
* `
|
|
47
|
+
* Same flow as `withCapEnforcement` but uses `enforceRollingCapAndMaybeNotify` and reserves by
|
|
48
|
+
* appending a `rolling-incremented` event before the handler transaction; a release appends
|
|
49
|
+
* `rolling-released`.
|
|
50
50
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* der counter im soft-Bereich ist. Caller sollte einen TTL-Cache
|
|
54
|
-
* (`Map<capName, lastNotifiedAt>`) im notify-callback einbauen.
|
|
51
|
+
* Rolling counters track no lastSoftWarnedAt, so the notifier fires on every call while usage
|
|
52
|
+
* sits in the soft range; throttle inside the notify callback (e.g. a per-capName TTL cache).
|
|
55
53
|
*/
|
|
56
54
|
export declare function withRollingCapEnforcement(handler: WriteHandlerDef, capResolver: RollingCapResolver): WriteHandlerDef;
|
|
@@ -6,26 +6,26 @@
|
|
|
6
6
|
// the wrapper keeps the pattern explicit and co-located.
|
|
7
7
|
//
|
|
8
8
|
// **Calendar reservation:** the wrapper only declares the cap; the dispatcher runs the
|
|
9
|
-
// reservation (reserveBeforeTransaction)
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// COMMIT
|
|
14
|
-
//
|
|
15
|
-
// the
|
|
9
|
+
// reservation (reserveBeforeTransaction) after all pre-handler gates and before the handler
|
|
10
|
+
// transaction opens, in its own short committed write (counter increment plus a
|
|
11
|
+
// store_cap_reservations row). The handler transaction deletes the row on its own commit; a release
|
|
12
|
+
// after a rollback, failure result or failed COMMIT only gives back what is still booked, so an
|
|
13
|
+
// unknown COMMIT outcome cannot under-count. No connection is held across the handler. Rows a
|
|
14
|
+
// crashed process left behind expire after CAP_RESERVATION_TTL_MINUTES and are given back before the
|
|
15
|
+
// next reserve of the same cap. Reached through a nested ctx.write the cap is not reserved, so the
|
|
16
|
+
// dispatcher rejects that call unless the top-level batch reserved the same handler.
|
|
16
17
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
18
|
+
// The wrapper spreads the wrapped handler and chains its reserveBeforeTransaction: inner reserves
|
|
19
|
+
// first, release runs in reverse, and an outer failure gives the inner reservation back.
|
|
20
|
+
//
|
|
21
|
+
// Rolling caps reserve the same way: a version-guarded `rolling-incremented` append plus a
|
|
22
|
+
// reservation row, undone by a `rolling-released` event. The window sum is incremented minus released.
|
|
22
23
|
//
|
|
23
24
|
// No automatic markSoftWarned here — that's inside enforceCapAndMaybeNotify
|
|
24
25
|
// (enforce-cap.ts).
|
|
25
|
-
import {
|
|
26
|
-
import { bookCapUsage, releaseCapUsage } from "./book-cap-usage.js";
|
|
27
|
-
import { CapCounterHandlers } from "./constants.js";
|
|
26
|
+
import { releaseExpiredCapReservations, reserveCalendarCap, reserveRollingCap, } from "./cap-reservation.js";
|
|
28
27
|
import { assertBelowHardCap, enforceCapAndMaybeNotify, enforceRollingCapAndMaybeNotify, } from "./enforce-cap.js";
|
|
28
|
+
import { chainReservations } from "./reservation-chain.js";
|
|
29
29
|
/**
|
|
30
30
|
* Wrap a write-handler with calendar-period cap-enforcement.
|
|
31
31
|
*
|
|
@@ -39,8 +39,9 @@ import { assertBelowHardCap, enforceCapAndMaybeNotify, enforceRollingCapAndMaybe
|
|
|
39
39
|
export function withCapEnforcement(handler, capResolver) {
|
|
40
40
|
return {
|
|
41
41
|
...handler,
|
|
42
|
-
reserveBeforeTransaction: async (event, ctx) => {
|
|
42
|
+
reserveBeforeTransaction: chainReservations(handler.reserveBeforeTransaction, async (event, ctx) => {
|
|
43
43
|
const cap = await capResolver(event, ctx);
|
|
44
|
+
await releaseExpiredCapReservations(ctx, cap.capName);
|
|
44
45
|
await enforceCapAndMaybeNotify(ctx, {
|
|
45
46
|
capName: cap.capName,
|
|
46
47
|
periodStartIso: cap.periodStartIso,
|
|
@@ -51,47 +52,31 @@ export function withCapEnforcement(handler, capResolver) {
|
|
|
51
52
|
...(cap.amount !== undefined && { amount: cap.amount }),
|
|
52
53
|
});
|
|
53
54
|
const amount = cap.amount ?? 1;
|
|
54
|
-
|
|
55
|
+
return reserveCalendarCap(ctx, {
|
|
55
56
|
capName: cap.capName,
|
|
56
57
|
amount,
|
|
57
58
|
periodStartIso: cap.periodStartIso,
|
|
58
|
-
outsideTransaction: true,
|
|
59
59
|
guardCurrentValue: (currentValue) => assertBelowHardCap(currentValue + amount - 1, cap),
|
|
60
60
|
});
|
|
61
|
-
|
|
62
|
-
throw reraiseAsKumikoError(reserved.error);
|
|
63
|
-
return async () => {
|
|
64
|
-
const released = await releaseCapUsage(ctx, {
|
|
65
|
-
capName: cap.capName,
|
|
66
|
-
amount,
|
|
67
|
-
periodStartIso: cap.periodStartIso,
|
|
68
|
-
outsideTransaction: true,
|
|
69
|
-
});
|
|
70
|
-
if (!released.isSuccess)
|
|
71
|
-
throw reraiseAsKumikoError(released.error);
|
|
72
|
-
};
|
|
73
|
-
},
|
|
61
|
+
}),
|
|
74
62
|
};
|
|
75
63
|
}
|
|
76
64
|
/**
|
|
77
65
|
* Wrap a write-handler with rolling-window cap-enforcement.
|
|
78
66
|
*
|
|
79
|
-
* Same flow as `withCapEnforcement` but uses
|
|
80
|
-
* `
|
|
81
|
-
* `
|
|
67
|
+
* Same flow as `withCapEnforcement` but uses `enforceRollingCapAndMaybeNotify` and reserves by
|
|
68
|
+
* appending a `rolling-incremented` event before the handler transaction; a release appends
|
|
69
|
+
* `rolling-released`.
|
|
82
70
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* der counter im soft-Bereich ist. Caller sollte einen TTL-Cache
|
|
86
|
-
* (`Map<capName, lastNotifiedAt>`) im notify-callback einbauen.
|
|
71
|
+
* Rolling counters track no lastSoftWarnedAt, so the notifier fires on every call while usage
|
|
72
|
+
* sits in the soft range; throttle inside the notify callback (e.g. a per-capName TTL cache).
|
|
87
73
|
*/
|
|
88
74
|
export function withRollingCapEnforcement(handler, capResolver) {
|
|
89
75
|
return {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
access: handler.access,
|
|
93
|
-
handler: async (event, ctx) => {
|
|
76
|
+
...handler,
|
|
77
|
+
reserveBeforeTransaction: chainReservations(handler.reserveBeforeTransaction, async (event, ctx) => {
|
|
94
78
|
const cap = await capResolver(event, ctx);
|
|
79
|
+
await releaseExpiredCapReservations(ctx, cap.capName);
|
|
95
80
|
await enforceRollingCapAndMaybeNotify(ctx, {
|
|
96
81
|
capName: cap.capName,
|
|
97
82
|
windowDays: cap.windowDays,
|
|
@@ -99,14 +84,13 @@ export function withRollingCapEnforcement(handler, capResolver) {
|
|
|
99
84
|
profile: cap.profile,
|
|
100
85
|
notify: cap.notify,
|
|
101
86
|
});
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
},
|
|
87
|
+
const amount = cap.amount ?? 1;
|
|
88
|
+
return reserveRollingCap(ctx, {
|
|
89
|
+
capName: cap.capName,
|
|
90
|
+
windowDays: cap.windowDays,
|
|
91
|
+
amount,
|
|
92
|
+
guardCurrentValue: (usage) => assertBelowHardCap(usage + amount - 1, cap),
|
|
93
|
+
});
|
|
94
|
+
}),
|
|
111
95
|
};
|
|
112
96
|
}
|
|
@@ -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
|
});
|
|
@@ -18,6 +18,7 @@ export declare const tenantComplianceProfileEntity: import("@cosmicdrift/kumiko-
|
|
|
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
|
};
|
|
@@ -58,6 +59,7 @@ export declare const tenantComplianceProfileTable: import("@cosmicdrift/kumiko-f
|
|
|
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
|
};
|
|
@@ -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({});
|