@cosmicdrift/kumiko-bundled-features 0.403.0 → 0.404.1
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/feature.js +1 -1
- package/dist/auth-email-password/web/auth-gate.js +4 -0
- package/dist/data-retention/feature.js +1 -1
- package/dist/delivery/feature.js +1 -1
- package/dist/feature-toggles/toggle-sync-signal.d.ts +1 -1
- package/dist/feature-toggles/toggle-sync-signal.js +7 -4
- package/dist/form-draft/feature.js +1 -1
- package/dist/inbound-mail-foundation/handlers/thread-rollup.js +1 -0
- package/dist/inbound-mail-foundation/retention-job.js +1 -1
- package/dist/jobs/feature.js +5 -6
- package/dist/template-resolver/document-sanitizer.js +3 -2
- package/dist/template-resolver/web/collection-entry-create-form.js +1 -0
- package/dist/user-data-rights/feature.js +3 -3
- package/dist/user-data-rights/web/public-deletion-gate.js +1 -0
- package/package.json +35 -35
- package/src/documents/changes.json +8 -0
- package/src/feature-toggles/changes.json +6 -0
- package/src/jobs/changes.json +6 -0
package/dist/audit/feature.js
CHANGED
|
@@ -43,7 +43,7 @@ export function createAuditFeature() {
|
|
|
43
43
|
}));
|
|
44
44
|
r.job({
|
|
45
45
|
name: "escape-hatch-retention",
|
|
46
|
-
trigger: {
|
|
46
|
+
trigger: { maintenance: true },
|
|
47
47
|
concurrency: "skip",
|
|
48
48
|
// pruneEvents needs a transaction-capable connection, which only unsafeRaw() hands a job.
|
|
49
49
|
escapeHatch: {
|
|
@@ -29,6 +29,7 @@ export function createLoginRoute(opts = {}) {
|
|
|
29
29
|
const LoginComponent = opts.loginScreen ?? LoginScreen;
|
|
30
30
|
const MfaVerifyComponent = opts.mfaVerifyScreen;
|
|
31
31
|
const MfaSetupComponent = opts.mfaSetupScreen;
|
|
32
|
+
// biome-ignore lint/correctness/noComponentHookFactories: setup-time factory (module scope or makeAuthGate); the route takes no props, so opts are bound here
|
|
32
33
|
function LoginRoute() {
|
|
33
34
|
const { status, refresh, bootstrapFailure, logout } = useSession();
|
|
34
35
|
const { onAuthenticated } = opts;
|
|
@@ -107,6 +108,7 @@ function resolveLoginRedirectOrNull(loginUrl, locale, location) {
|
|
|
107
108
|
return resolveLoginRedirect(loginUrl, locale, location);
|
|
108
109
|
}
|
|
109
110
|
catch (error) {
|
|
111
|
+
// biome-ignore lint/suspicious/noConsole: the browser console is the only place a broken loginUrl builder shows up, the visitor just sees the built-in login
|
|
110
112
|
console.error("Auth gate: loginUrl could not be resolved, showing the built-in login", error);
|
|
111
113
|
return null;
|
|
112
114
|
}
|
|
@@ -116,6 +118,7 @@ export function makeAuthGate(opts = {}) {
|
|
|
116
118
|
if (typeof loginUrl === "string")
|
|
117
119
|
assertNavigableUrl(loginUrl, "loginUrl");
|
|
118
120
|
const LoginRoute = createLoginRoute(opts);
|
|
121
|
+
// biome-ignore lint/correctness/noComponentHookFactories: setup-time factory (makeSessionAuthGate or an app's mount function); gates only receive children, so opts are bound here
|
|
119
122
|
function AuthGate({ children }) {
|
|
120
123
|
const { status } = useSession();
|
|
121
124
|
const locale = useLocale().locale();
|
|
@@ -145,6 +148,7 @@ export function makeSessionAuthGate(opts = {}) {
|
|
|
145
148
|
if (postLogoutUrl !== undefined)
|
|
146
149
|
assertNavigableUrl(postLogoutUrl, "postLogoutUrl");
|
|
147
150
|
const AuthGate = makeAuthGate(opts);
|
|
151
|
+
// biome-ignore lint/correctness/noComponentHookFactories: setup-time factory, called once per emailPasswordClient(); gates only receive children, so opts are bound here
|
|
148
152
|
function SessionAuthGate({ children }) {
|
|
149
153
|
return (_jsx(SessionProvider, { postLogoutUrl: postLogoutUrl, children: _jsx(AuthGate, { children: children }) }));
|
|
150
154
|
}
|
|
@@ -41,7 +41,7 @@ export function createDataRetentionFeature() {
|
|
|
41
41
|
// retention rules are configured but never enforced.
|
|
42
42
|
r.job({
|
|
43
43
|
name: "retention-cleanup",
|
|
44
|
-
trigger: {
|
|
44
|
+
trigger: { maintenance: true },
|
|
45
45
|
perTenant: true,
|
|
46
46
|
concurrency: "skip",
|
|
47
47
|
escapeHatch: {
|
package/dist/delivery/feature.js
CHANGED
|
@@ -125,7 +125,7 @@ export function createDeliveryFeature(options) {
|
|
|
125
125
|
if (attemptLogRetentionDays !== undefined) {
|
|
126
126
|
r.job({
|
|
127
127
|
name: DeliveryJobNames.attemptLogRetention,
|
|
128
|
-
trigger: {
|
|
128
|
+
trigger: { maintenance: true },
|
|
129
129
|
concurrency: "skip",
|
|
130
130
|
escapeHatch: { reason: ATTEMPT_LOG_RETENTION_ESCAPE_HATCH_REASON },
|
|
131
131
|
handler: async (_payload, ctx) => {
|
|
@@ -3,5 +3,5 @@ import type { ToggleSyncSignal } from "./toggle-runtime.ts";
|
|
|
3
3
|
export type RedisToggleSyncSignal = ToggleSyncSignal & {
|
|
4
4
|
close(): Promise<void>;
|
|
5
5
|
};
|
|
6
|
-
export declare function createRedisToggleSyncSignal(redisUrl: string, clientOptions?: RedisClientOptions, channelPrefix?: string): RedisToggleSyncSignal;
|
|
6
|
+
export declare function createRedisToggleSyncSignal(redisUrl: string, clientOptions?: RedisClientOptions, channelPrefix?: string, channelSubscribePattern?: string | undefined): RedisToggleSyncSignal;
|
|
7
7
|
export declare function createCacheSyncToggleSignal(bus: CacheSyncBus): ToggleSyncSignal;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CACHE_SYNC_TOPICS, createRedisPubSubSignal, redisChannelPrefixFromEnv, } from "@cosmicdrift/kumiko-framework/redis";
|
|
1
|
+
import { CACHE_SYNC_TOPICS, createRedisPubSubSignal, redisChannelPrefixFromEnv, redisNamespaceFromEnv, } from "@cosmicdrift/kumiko-framework/redis";
|
|
2
2
|
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
3
3
|
// Single fixed channel — unlike the SSE broker there's no per-tenant/
|
|
4
4
|
// per-user variance to multiplex, every toggle flip is global.
|
|
@@ -13,11 +13,11 @@ function isTogglePayload(value) {
|
|
|
13
13
|
// the shared cursor's toggle-set event would ever learn about a flip.
|
|
14
14
|
// Standalone Redis transport for callers that want their own connections;
|
|
15
15
|
// createFeatureToggleRuntime builds a cache-sync-bus signal by default.
|
|
16
|
-
export function createRedisToggleSyncSignal(redisUrl, clientOptions, channelPrefix = redisChannelPrefixFromEnv()) {
|
|
16
|
+
export function createRedisToggleSyncSignal(redisUrl, clientOptions, channelPrefix = redisChannelPrefixFromEnv(), channelSubscribePattern = redisNamespaceFromEnv().channelSubscribePattern) {
|
|
17
17
|
const toggleSyncChannel = `${channelPrefix}${TOGGLE_SYNC_CHANNEL_NAME}`;
|
|
18
18
|
const signal = createRedisPubSubSignal({
|
|
19
19
|
redisUrl,
|
|
20
|
-
channelPattern: toggleSyncChannel,
|
|
20
|
+
channelPattern: channelSubscribePattern ?? toggleSyncChannel,
|
|
21
21
|
label: "feature-toggles",
|
|
22
22
|
...(clientOptions ? { clientOptions } : {}),
|
|
23
23
|
});
|
|
@@ -26,7 +26,10 @@ export function createRedisToggleSyncSignal(redisUrl, clientOptions, channelPref
|
|
|
26
26
|
signal.publish(toggleSyncChannel, { featureName, enabled });
|
|
27
27
|
},
|
|
28
28
|
onMessage(listener) {
|
|
29
|
-
signal.onMessage((
|
|
29
|
+
signal.onMessage((channel, payload) => {
|
|
30
|
+
// skip: a wide subscribe pattern also delivers other features' channels
|
|
31
|
+
if (channel !== toggleSyncChannel)
|
|
32
|
+
return;
|
|
30
33
|
if (!isTogglePayload(payload)) {
|
|
31
34
|
// biome-ignore lint/suspicious/noConsole: ops-visible fallback — no ctx.obs.log in a raw PubSubSignal handler.
|
|
32
35
|
console.error(`[kumiko:feature-toggles] dropping malformed cache-sync message on "${toggleSyncChannel}"`);
|
|
@@ -34,7 +34,7 @@ function registerFormDraft(r) {
|
|
|
34
34
|
r.config({ keys: { retentionDays: formDraftRetentionDaysConfig } });
|
|
35
35
|
r.job({
|
|
36
36
|
name: "cleanup",
|
|
37
|
-
trigger: {
|
|
37
|
+
trigger: { maintenance: true },
|
|
38
38
|
concurrency: "skip",
|
|
39
39
|
escapeHatch: { reason: CLEANUP_ESCAPE_HATCH_REASON },
|
|
40
40
|
handler: (payload, ctx) => cleanupDraftsJob(payload, ctx, ctx.db.unsafeRaw()),
|
|
@@ -70,6 +70,7 @@ export async function appendThreadRollup(ctx, input) {
|
|
|
70
70
|
// @cast-boundary engine-payload — eigene events, shape via defineEvent
|
|
71
71
|
const previousThread = lastThreadEvent?.payload;
|
|
72
72
|
const previousLastAt = previousThread?.lastMessageAtIso ?? "";
|
|
73
|
+
// biome-ignore lint/complexity/useMathMinMax: ISO strings compare lexicographically; Math.max would coerce them to NaN
|
|
73
74
|
const newLastAt = receivedAtIso > previousLastAt ? receivedAtIso : previousLastAt;
|
|
74
75
|
const messageCount = await countWhere(threadRollupRunner, inboundMessagesProjectionTable, {
|
|
75
76
|
tenantId,
|
|
@@ -10,7 +10,7 @@ export function registerInboundMailRetentionJob(r) {
|
|
|
10
10
|
const INBOUND_MAIL_RETENTION_REASON = "inbound-mail retention runs raw deletes; every statement filters by this run's tenantId";
|
|
11
11
|
r.job({
|
|
12
12
|
name: "inbound-mail-retention",
|
|
13
|
-
trigger: {
|
|
13
|
+
trigger: { maintenance: true },
|
|
14
14
|
perTenant: true,
|
|
15
15
|
concurrency: "skip",
|
|
16
16
|
escapeHatch: { reason: INBOUND_MAIL_RETENTION_REASON },
|
package/dist/jobs/feature.js
CHANGED
|
@@ -65,13 +65,12 @@ export function createJobsFeature(options = {}) {
|
|
|
65
65
|
}, reindexEntityJob);
|
|
66
66
|
// store_job_runs/store_job_run_logs are direct-write, unbounded-growth
|
|
67
67
|
// stores — nothing else purges them.
|
|
68
|
-
r.job("retention-cleanup", { trigger: {
|
|
68
|
+
r.job("retention-cleanup", { trigger: { maintenance: true }, concurrency: "skip" }, createRetentionCleanupJob(retentionDays));
|
|
69
69
|
// Sweeps store_job_runs for rows stuck at status "running" past the
|
|
70
|
-
// timeout.
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
r.job("stale-run-sweep", { trigger: { cron: "0 * * * *" }, concurrency: "skip" }, createStaleRunSweepJob(staleRunTimeoutHours, retentionDays));
|
|
70
|
+
// timeout. Runs in the maintenance window: the timeout is
|
|
71
|
+
// hours, so a daily pass finds a stuck run at most a day late and a sleeping
|
|
72
|
+
// app is not woken every hour for it.
|
|
73
|
+
r.job("stale-run-sweep", { trigger: { maintenance: true }, concurrency: "skip" }, createStaleRunSweepJob(staleRunTimeoutHours, retentionDays));
|
|
75
74
|
const handlers = {
|
|
76
75
|
trigger: r.writeHandler(triggerWrite),
|
|
77
76
|
retry: r.writeHandler(retryWrite),
|
|
@@ -131,8 +131,9 @@ export function withParsedDocument(html, read) {
|
|
|
131
131
|
return read(parsed.body);
|
|
132
132
|
}
|
|
133
133
|
finally {
|
|
134
|
-
window.
|
|
135
|
-
window.happyDOM.
|
|
134
|
+
// The window is never reused, so this sync API need not await the teardown promises.
|
|
135
|
+
void window.happyDOM.abort();
|
|
136
|
+
void window.happyDOM.close();
|
|
136
137
|
}
|
|
137
138
|
}
|
|
138
139
|
function scanDocumentHtml(html) {
|
|
@@ -11,6 +11,7 @@ const SLUG_INVALID_ISSUE = {
|
|
|
11
11
|
// `tenantId` is the plugin's SystemAdmin tenant, the one the collection tree lists, so a new entry
|
|
12
12
|
// lands where the tree looks. Same rule as the tree: per-user collections stay on the session tenant.
|
|
13
13
|
export function createCollectionEntryCreateForm(tenantId) {
|
|
14
|
+
// biome-ignore lint/correctness/noComponentHookFactories: setup-time factory, called once per templateResolverClient(); the resolver slot only passes target/onClose, so tenantId is bound here
|
|
14
15
|
return function BoundCollectionEntryCreateForm(props) {
|
|
15
16
|
return _jsx(CollectionEntryCreateForm, { ...props, tenantId: tenantId });
|
|
16
17
|
};
|
|
@@ -416,13 +416,13 @@ export function createUserDataRightsFeature(opts = {}) {
|
|
|
416
416
|
r.metric(EXPORT_CLEANUP_LAST_RUN_TIMESTAMP_METRIC, {
|
|
417
417
|
type: "gauge",
|
|
418
418
|
unit: "seconds",
|
|
419
|
-
description: "Unix timestamp of the last export storage-cleanup pass that completed. Alert on time() minus this value exceeding
|
|
419
|
+
description: "Unix timestamp of the last export storage-cleanup pass that completed. Alert on time() minus this value exceeding one day plus a buffer; the backlog-age gauge alone stays at its last value when the cron stops.",
|
|
420
420
|
});
|
|
421
421
|
// Worker for the asynchronous export pipeline, triggered by a cron job.
|
|
422
422
|
const RUN_EXPORT_JOBS_REASON = "processes pending export jobs across every tenant";
|
|
423
423
|
r.job({
|
|
424
424
|
name: "run-export-jobs",
|
|
425
|
-
trigger: {
|
|
425
|
+
trigger: { maintenance: true },
|
|
426
426
|
concurrency: "skip",
|
|
427
427
|
escapeHatch: { reason: RUN_EXPORT_JOBS_REASON },
|
|
428
428
|
handler: async (_payload, ctx) => {
|
|
@@ -509,7 +509,7 @@ export function createUserDataRightsFeature(opts = {}) {
|
|
|
509
509
|
const RUN_FORGET_CLEANUP_REASON = "executes overdue Art.17 forget requests across every tenant";
|
|
510
510
|
r.job({
|
|
511
511
|
name: "run-forget-cleanup",
|
|
512
|
-
trigger: {
|
|
512
|
+
trigger: { maintenance: true },
|
|
513
513
|
concurrency: "skip",
|
|
514
514
|
escapeHatch: { reason: RUN_FORGET_CLEANUP_REASON },
|
|
515
515
|
handler: async (_payload, ctx) => {
|
|
@@ -4,6 +4,7 @@ import { RequestAccountDeletionScreen } from "./request-deletion-screen.js";
|
|
|
4
4
|
const centeredShell = (screen) => (_jsx("div", { className: "min-h-screen flex items-center justify-center bg-background px-4", children: screen }));
|
|
5
5
|
export function makePublicDeletionGate(routes) {
|
|
6
6
|
const shell = routes.shell ?? centeredShell;
|
|
7
|
+
// biome-ignore lint/correctness/noComponentHookFactories: setup-time factory, called once per userDataRightsClient(); gates only receive children, so routes are bound here
|
|
7
8
|
function PublicDeletionGate({ children }) {
|
|
8
9
|
const path = window.location.pathname;
|
|
9
10
|
if (path === routes.requestPath)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.404.1",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -570,41 +570,41 @@
|
|
|
570
570
|
}
|
|
571
571
|
},
|
|
572
572
|
"dependencies": {
|
|
573
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
574
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
575
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
576
|
-
"@mollie/api-client": "^4.
|
|
577
|
-
"@node-rs/argon2": "^2.
|
|
578
|
-
"@tiptap/core": "^3.
|
|
579
|
-
"@tiptap/extension-link": "^3.
|
|
580
|
-
"@tiptap/extension-list": "^3.
|
|
581
|
-
"@tiptap/extension-table": "^3.
|
|
582
|
-
"@tiptap/html": "^3.
|
|
583
|
-
"@tiptap/extensions": "^3.
|
|
584
|
-
"@tiptap/pm": "^3.
|
|
585
|
-
"@tiptap/react": "^3.
|
|
586
|
-
"@tiptap/starter-kit": "^3.
|
|
587
|
-
"@types/mailparser": "^3.
|
|
588
|
-
"@types/qrcode": "^1.5.
|
|
573
|
+
"@cosmicdrift/kumiko-framework": "0.404.1",
|
|
574
|
+
"@cosmicdrift/kumiko-renderer": "0.404.1",
|
|
575
|
+
"@cosmicdrift/kumiko-types": "0.404.1",
|
|
576
|
+
"@mollie/api-client": "^4.6.0",
|
|
577
|
+
"@node-rs/argon2": "^2.2.2",
|
|
578
|
+
"@tiptap/core": "^3.32.1",
|
|
579
|
+
"@tiptap/extension-link": "^3.32.1",
|
|
580
|
+
"@tiptap/extension-list": "^3.32.1",
|
|
581
|
+
"@tiptap/extension-table": "^3.32.1",
|
|
582
|
+
"@tiptap/html": "^3.32.1",
|
|
583
|
+
"@tiptap/extensions": "^3.32.1",
|
|
584
|
+
"@tiptap/pm": "^3.32.1",
|
|
585
|
+
"@tiptap/react": "^3.32.1",
|
|
586
|
+
"@tiptap/starter-kit": "^3.32.1",
|
|
587
|
+
"@types/mailparser": "^3.9.0",
|
|
588
|
+
"@types/qrcode": "^1.5.6",
|
|
589
589
|
"clsx": "^2.1.1",
|
|
590
590
|
"happy-dom": "^21.0.4",
|
|
591
591
|
"hono": "^4.13.14",
|
|
592
592
|
"imapflow": "^1.7.8",
|
|
593
|
-
"ioredis": "^5.
|
|
594
|
-
"jose": "^6.2.
|
|
595
|
-
"lucide-react": "^1.
|
|
596
|
-
"mailparser": "^3.
|
|
597
|
-
"marked": "^18.
|
|
598
|
-
"nodemailer": "^10.
|
|
593
|
+
"ioredis": "^5.11.1",
|
|
594
|
+
"jose": "^6.2.14",
|
|
595
|
+
"lucide-react": "^1.65.0",
|
|
596
|
+
"mailparser": "^3.14.1",
|
|
597
|
+
"marked": "^18.1.2",
|
|
598
|
+
"nodemailer": "^10.3.1",
|
|
599
599
|
"pdf-lib": "^1.17.1",
|
|
600
600
|
"qrcode": "^1.5.4",
|
|
601
|
-
"react": "^19.
|
|
602
|
-
"react-dom": "^19.
|
|
601
|
+
"react": "^19.3.1",
|
|
602
|
+
"react-dom": "^19.3.1",
|
|
603
603
|
"sharp": "^0.35.5",
|
|
604
604
|
"stripe": "^22.6.2",
|
|
605
|
-
"tailwind-merge": "^3.
|
|
605
|
+
"tailwind-merge": "^3.7.0",
|
|
606
606
|
"temporal-polyfill": "^0.3.2",
|
|
607
|
-
"uuid": "^14.0.
|
|
607
|
+
"uuid": "^14.0.3",
|
|
608
608
|
"zod": "^4.6.7"
|
|
609
609
|
},
|
|
610
610
|
"publishConfig": {
|
|
@@ -1166,17 +1166,17 @@
|
|
|
1166
1166
|
],
|
|
1167
1167
|
"devDependencies": {
|
|
1168
1168
|
"@testing-library/user-event": "^14.6.8",
|
|
1169
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
1170
|
-
"@cosmicdrift/kumiko-locale-es": "0.
|
|
1171
|
-
"@cosmicdrift/kumiko-locale-fr": "0.
|
|
1172
|
-
"@cosmicdrift/kumiko-locale-it": "0.
|
|
1173
|
-
"@cosmicdrift/kumiko-renderer-mail-html": "0.
|
|
1169
|
+
"@cosmicdrift/kumiko-locale-de": "0.404.1",
|
|
1170
|
+
"@cosmicdrift/kumiko-locale-es": "0.404.1",
|
|
1171
|
+
"@cosmicdrift/kumiko-locale-fr": "0.404.1",
|
|
1172
|
+
"@cosmicdrift/kumiko-locale-it": "0.404.1",
|
|
1173
|
+
"@cosmicdrift/kumiko-renderer-mail-html": "0.404.1",
|
|
1174
1174
|
"jsqr": "^1.4.0",
|
|
1175
|
-
"unpdf": "^1.
|
|
1176
|
-
"@cosmicdrift/kumiko-testing": "0.
|
|
1175
|
+
"unpdf": "^1.9.1",
|
|
1176
|
+
"@cosmicdrift/kumiko-testing": "0.404.1"
|
|
1177
1177
|
},
|
|
1178
1178
|
"peerDependencies": {
|
|
1179
|
-
"@cosmicdrift/kumiko-testing": "0.
|
|
1179
|
+
"@cosmicdrift/kumiko-testing": "0.404.1"
|
|
1180
1180
|
},
|
|
1181
1181
|
"peerDependenciesMeta": {
|
|
1182
1182
|
"@cosmicdrift/kumiko-testing": {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.404.1",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Documents recipe has a README and gen-sample-index rejects a primarySample without one",
|
|
6
|
+
"detail": "feature-reference linked to /en/samples/recipes-documents/, a page the docs site never generated because the recipe had no README.md. The README is added, and gen-sample-index aborts with the feature, the sample and the fix when a primarySample from sample-index.overrides.json has no README.md."
|
|
7
|
+
}
|
|
8
|
+
]
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.404.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Feature-toggle Redis signal follows KUMIKO_REDIS_PREFIX",
|
|
6
|
+
"detail": "createRedisToggleSyncSignal takes an optional channelSubscribePattern (default from KUMIKO_REDIS_PREFIX) and ignores messages from other channels, which a prefix-wide subscription delivers."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.365.0",
|
|
4
10
|
"type": "fix",
|
package/src/jobs/changes.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.404.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Cleanup jobs run once a day in the maintenance window",
|
|
6
|
+
"detail": "jobs (stale-run-sweep, hourly before, and retention-cleanup), user-data-rights (export and forget), data-retention, audit escape-hatch-retention, inbound-mail retention, delivery attempt-log-retention and form-draft cleanup use trigger { maintenance: true } instead of 03:00 to 03:30 and hourly crons. Without KUMIKO_MAINTENANCE_WINDOW they all run at 03:00 UTC. A stuck job run is now marked failed at most a day after it crosses the stale timeout."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.394.0",
|
|
4
10
|
"type": "fix",
|