@cosmicdrift/kumiko-bundled-features 0.333.0 → 0.335.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/admin-shell/overview-panels.js +2 -2
- package/dist/agent-tools/agent-doc-lint.d.ts +1 -0
- package/dist/agent-tools/agent-doc-lint.js +33 -3
- package/dist/agent-tools/agent-manifest.js +47 -6
- package/dist/agent-tools/filter-ops.d.ts +2 -0
- package/dist/agent-tools/filter-ops.js +4 -0
- package/dist/agent-tools/tool-catalog.js +1 -4
- package/dist/agent-tools/tool-dispatch.js +1 -4
- package/dist/audit/constants.d.ts +1 -1
- package/dist/audit/constants.js +1 -1
- package/dist/audit/handlers/list.query.js +4 -3
- package/dist/auth-email-password/handlers/change-password.write.js +2 -0
- package/dist/auth-email-password/handlers/invite-create.write.js +12 -2
- package/dist/auth-email-password/handlers/login.write.js +2 -0
- package/dist/auth-email-password/handlers/self-registration-status.query.js +1 -1
- package/dist/auth-email-password/i18n.js +2 -0
- package/dist/auth-email-password/web/auth-client.d.ts +1 -0
- package/dist/auth-email-password/web/auth-client.js +1 -1
- package/dist/auth-email-password/web/auth-gate.js +6 -8
- package/dist/auth-email-password/web/index.d.ts +1 -1
- package/dist/auth-email-password/web/index.js +1 -1
- package/dist/auth-email-password/web/invite-accept-screen.js +16 -12
- package/dist/auth-email-password/web/login-screen.js +1 -1
- package/dist/auth-email-password/web/session-bootstrap-error.d.ts +5 -1
- package/dist/auth-email-password/web/session-bootstrap-error.js +2 -2
- package/dist/auth-email-password/web/session.d.ts +4 -0
- package/dist/auth-email-password/web/session.js +15 -0
- package/dist/auth-mfa/feature.js +3 -2
- package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +2 -2
- package/dist/auth-mfa/handlers/verify.write.js +2 -0
- package/dist/auth-mfa/hmac-token-codec.js +3 -0
- package/dist/auth-mfa/mfa-challenge-token.js +4 -0
- package/dist/auth-mfa/mfa-preauth-setup-token.js +3 -0
- package/dist/auth-mfa/schema/user-mfa.d.ts +6 -0
- package/dist/auth-mfa/web/mfa-client.d.ts +1 -1
- package/dist/auth-mfa/web/mfa-client.js +4 -10
- package/dist/billing-foundation/billing-info-query.js +1 -1
- package/dist/billing-foundation/checkout-core.d.ts +5 -5
- package/dist/billing-foundation/checkout-core.js +10 -6
- package/dist/billing-foundation/entities.d.ts +8 -0
- package/dist/billing-foundation/handlers/switch-plan.write.js +5 -8
- package/dist/billing-foundation/projection.d.ts +8 -0
- package/dist/cap-counter/book-cap-usage.js +15 -5
- package/dist/cap-counter/enforce-cap.js +9 -3
- package/dist/cap-counter/entity.d.ts +1 -0
- package/dist/cap-counter/feature.js +3 -2
- package/dist/cap-counter/i18n.d.ts +1 -0
- package/dist/cap-counter/i18n.js +6 -5
- package/dist/cap-overview/handlers/caps-usage.query.js +9 -16
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +41 -15
- package/dist/cap-overview/i18n.js +5 -0
- package/dist/cap-overview/web/cap-cards-panel.js +4 -1
- package/dist/cap-overview/web/cap-usage-bar.js +1 -1
- package/dist/channel-email/email-channel.js +12 -1
- package/dist/channel-in-app/tables.js +2 -6
- package/dist/compliance-profiles/handlers/sub-processors.query.js +1 -1
- package/dist/compliance-profiles/schema/profile-selection.d.ts +2 -2
- package/dist/config/db/queries/resolver.js +3 -1
- package/dist/config/handlers/set.write.js +8 -4
- package/dist/config/table.d.ts +6 -0
- package/dist/config/web/i18n.js +13 -4
- package/dist/config/write-helpers.d.ts +1 -1
- package/dist/config/write-helpers.js +11 -5
- package/dist/crypto-shredding/handlers/forget-subject.write.js +15 -8
- package/dist/custom-fields/constants.js +1 -4
- package/dist/custom-fields/entity.d.ts +4 -0
- package/dist/data-retention/resolve-for-tenant.d.ts +2 -3
- package/dist/data-retention/run-retention-cleanup.js +100 -44
- package/dist/data-retention/schema/tenant-retention-override.d.ts +4 -0
- package/dist/delivery/tables.d.ts +6 -0
- package/dist/delivery/types.d.ts +2 -1
- package/dist/delivery/types.js +7 -0
- package/dist/delivery/unsubscribe.js +5 -11
- package/dist/derivatives-sharp/render.js +27 -9
- package/dist/document-ingest-foundation/entity.d.ts +4 -0
- package/dist/document-ingest-foundation/feature.js +5 -5
- package/dist/document-ingest-foundation/providers.d.ts +1 -0
- package/dist/document-ingest-foundation/providers.js +6 -0
- package/dist/document-ingest-foundation/write-document-extract.d.ts +2 -2
- package/dist/document-ingest-foundation/write-document-extract.js +10 -0
- package/dist/feature-toggles/global-feature-state-table.js +2 -7
- package/dist/feature-toggles/toggle-sync-signal.d.ts +2 -1
- package/dist/feature-toggles/toggle-sync-signal.js +3 -2
- package/dist/file-provider-s3/feature.js +1 -1
- package/dist/files-tenant-data/index.js +3 -2
- package/dist/folders/entity.d.ts +8 -0
- package/dist/folders/handlers/clear-folder.write.js +4 -11
- package/dist/folders/handlers/set-folder.write.js +4 -10
- package/dist/form-draft/entity.d.ts +2 -0
- package/dist/form-draft/feature.js +3 -2
- package/dist/form-draft/handlers/discard.write.js +7 -8
- package/dist/form-draft/handlers/get.query.js +7 -1
- package/dist/foundation-shared/host-allowlist.d.ts +4 -0
- package/dist/foundation-shared/host-allowlist.js +14 -0
- package/dist/foundation-shared/index.d.ts +1 -0
- package/dist/foundation-shared/index.js +1 -0
- package/dist/foundation-shared/mail-host-policy.d.ts +0 -2
- package/dist/foundation-shared/mail-host-policy.js +3 -13
- package/dist/inbound-mail-foundation/connect-routes.js +7 -5
- package/dist/inbound-mail-foundation/entities.d.ts +25 -0
- package/dist/inbound-mail-foundation/projection.d.ts +20 -0
- package/dist/inbound-mail-foundation/watch-supervisor.js +47 -43
- package/dist/jobs/handlers/list.query.d.ts +7 -2
- package/dist/jobs/handlers/list.query.js +9 -2
- package/dist/jobs/handlers/retry.write.js +3 -2
- package/dist/jobs/handlers/tenant-failures.query.js +3 -9
- package/dist/jobs/job-run-logger.js +18 -2
- package/dist/jobs/job-run-table.d.ts +6 -0
- package/dist/jobs/tenant-job-failure-table.js +2 -6
- package/dist/ledger/entity.d.ts +16 -3
- package/dist/ledger/handlers/confirm-schedule-period.write.js +2 -2
- package/dist/ledger/handlers/reverse-transaction.write.js +2 -2
- package/dist/ledger/subject-reference.d.ts +4 -0
- package/dist/ledger/subject-reference.js +7 -0
- package/dist/legal-pages/feature.js +8 -1
- package/dist/managed-pages/handlers/branding.query.js +1 -1
- package/dist/managed-pages/handlers/by-slug.query.js +1 -1
- package/dist/managed-pages/handlers/by-tenant-published.query.js +1 -1
- package/dist/managed-pages/table.d.ts +12 -0
- package/dist/metrics/compute.js +7 -1
- package/dist/metrics/window.js +5 -0
- package/dist/notes-history/entity.d.ts +12 -0
- package/dist/notes-history/handlers/add-note.write.js +4 -9
- package/dist/notes-history-user-data/hooks.js +8 -1
- package/dist/personal-access-tokens/constants.d.ts +1 -0
- package/dist/personal-access-tokens/constants.js +1 -0
- package/dist/personal-access-tokens/feature.js +2 -2
- package/dist/personal-access-tokens/handlers/create.write.js +2 -2
- package/dist/personal-access-tokens/handlers/revoke.write.js +2 -2
- package/dist/personal-access-tokens/i18n.js +12 -13
- package/dist/personal-access-tokens/schema/api-token.d.ts +6 -0
- package/dist/personal-access-tokens/schema/api-token.js +2 -1
- package/dist/personal-access-tokens/screens.js +5 -1
- package/dist/rate-limiting/feature.js +2 -0
- package/dist/rate-limiting/handlers/bucket-access.js +15 -7
- package/dist/rate-limiting/i18n.d.ts +10 -0
- package/dist/rate-limiting/i18n.js +10 -0
- package/dist/secrets/feature.js +3 -2
- package/dist/secrets/table.d.ts +1 -0
- package/dist/seo/handlers/seo-config.query.js +1 -1
- package/dist/sessions/constants.d.ts +1 -0
- package/dist/sessions/constants.js +1 -0
- package/dist/sessions/feature.js +5 -3
- package/dist/sessions/handlers/revoke-all-for-user.write.d.ts +1 -0
- package/dist/sessions/handlers/revoke-all-for-user.write.js +12 -2
- package/dist/sessions/handlers/revoke.write.js +2 -2
- package/dist/sessions/schema/user-session.d.ts +4 -0
- package/dist/sessions/schema/user-session.js +2 -1
- package/dist/sessions/screens.js +5 -1
- package/dist/sessions/session-callbacks.js +2 -2
- package/dist/shared/index.d.ts +1 -1
- package/dist/shared/index.js +1 -1
- package/dist/shared/lockout-counter.js +1 -1
- package/dist/shared/parent-visibility.d.ts +5 -0
- package/dist/shared/parent-visibility.js +17 -1
- package/dist/shared/row-bound-grant.js +20 -9
- package/dist/shared/single-use-token-store.js +5 -13
- package/dist/step-dispatcher/webhook-runner.d.ts +0 -2
- package/dist/step-dispatcher/webhook-runner.js +18 -17
- package/dist/subscription-mollie/plugin-methods.d.ts +3 -2
- package/dist/subscription-mollie/plugin-methods.js +7 -1
- package/dist/subscription-mollie/verify-webhook.js +6 -0
- package/dist/subscription-stripe/feature.js +3 -3
- package/dist/subscription-stripe/plugin-methods.js +25 -6
- package/dist/subscription-stripe/verify-webhook.js +13 -6
- package/dist/tags/entity.d.ts +9 -0
- package/dist/tags/handlers/assign-tag.write.js +4 -10
- package/dist/tags/handlers/remove-tag.write.js +4 -11
- package/dist/template-resolver/api.d.ts +2 -1
- package/dist/template-resolver/api.js +1 -1
- package/dist/template-resolver/handlers/by-slug.query.d.ts +1 -1
- package/dist/template-resolver/handlers/by-slug.query.js +2 -2
- package/dist/template-resolver/handlers/by-tenant.query.d.ts +3 -2
- package/dist/template-resolver/handlers/by-tenant.query.js +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 +3 -2
- package/dist/template-resolver/handlers/collection-shared.js +1 -1
- package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -1
- package/dist/template-resolver/handlers/find-by-id.query.js +1 -1
- package/dist/template-resolver/handlers/list.query.d.ts +1 -1
- package/dist/template-resolver/handlers/list.query.js +1 -1
- package/dist/template-resolver/seeding.js +41 -39
- package/dist/template-resolver/table.d.ts +26 -13
- package/dist/template-resolver/testing.js +2 -2
- package/dist/template-resolver/user-content-table.d.ts +16 -9
- package/dist/template-resolver/web/client-plugin.d.ts +1 -1
- package/dist/template-resolver-user-data/hooks.js +1 -1
- package/dist/tenant/cancel-pending-invitation.d.ts +8 -0
- package/dist/tenant/cancel-pending-invitation.js +22 -0
- package/dist/tenant/feature.js +4 -2
- package/dist/tenant/handlers/cancel-invitation.write.js +6 -21
- package/dist/tenant/handlers/member-directory.query.d.ts +1 -0
- package/dist/tenant/handlers/member-directory.query.js +17 -4
- package/dist/tenant/handlers/remove-member.write.js +9 -14
- package/dist/tenant/handlers/team-list.query.js +2 -2
- package/dist/tenant/handlers/tenant-directory.query.d.ts +1 -0
- package/dist/tenant/handlers/tenant-directory.query.js +13 -3
- package/dist/tenant/handlers/update-member-roles.write.d.ts +1 -1
- package/dist/tenant/handlers/update-member-roles.write.js +28 -12
- package/dist/tenant/i18n.js +4 -0
- package/dist/tenant/index.d.ts +1 -1
- package/dist/tenant/index.js +1 -1
- package/dist/tenant/invitation-table.d.ts +8 -2
- package/dist/tenant/last-tenant-admin.d.ts +1 -1
- package/dist/tenant/last-tenant-admin.js +5 -3
- package/dist/tenant/membership-table.d.ts +4 -0
- package/dist/tenant/schema/tenant.d.ts +8 -2
- package/dist/tenant/seeding.d.ts +4 -0
- package/dist/tenant/seeding.js +9 -2
- package/dist/tenant-lifecycle/boot-checks.d.ts +1 -0
- package/dist/tenant-lifecycle/boot-checks.js +27 -1
- package/dist/tenant-lifecycle/feature.js +2 -1
- package/dist/tenant-lifecycle/stages.js +2 -2
- package/dist/tenant-settings/feature.js +9 -6
- package/dist/tier-engine/entity.d.ts +2 -0
- package/dist/tier-engine/feature.js +3 -2
- package/dist/tier-engine/tier-resolver.d.ts +2 -0
- package/dist/user/command-schemas.d.ts +2 -2
- package/dist/user/handlers/create.write.d.ts +1 -1
- package/dist/user/handlers/create.write.js +8 -2
- package/dist/user/handlers/update.write.d.ts +1 -1
- package/dist/user/handlers/update.write.js +8 -2
- package/dist/user/schema/user.d.ts +14 -2
- package/dist/user-data-rights/db/queries/export-jobs.d.ts +2 -0
- package/dist/user-data-rights/feature.js +22 -9
- package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +4 -7
- package/dist/user-data-rights/handlers/deletion-grace-period.d.ts +3 -2
- package/dist/user-data-rights/handlers/deletion-grace-period.js +30 -8
- package/dist/user-data-rights/handlers/download-by-token.query.js +1 -1
- package/dist/user-data-rights/handlers/request-deletion-by-email.write.js +1 -1
- package/dist/user-data-rights/handlers/request-deletion.write.js +3 -6
- package/dist/user-data-rights/lib/update-user-lifecycle.js +7 -14
- package/dist/user-data-rights/run-export-jobs.d.ts +1 -0
- package/dist/user-data-rights/run-export-jobs.js +74 -41
- package/dist/user-data-rights/run-forget-cleanup.js +2 -2
- package/dist/user-data-rights/run-user-export.js +8 -4
- 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 +8 -2
- package/dist/user-data-rights-defaults/hooks/file-ref.userdata-hook.js +4 -1
- package/dist/user-profile/feature.js +6 -1
- package/dist/user-profile/handlers/change-email.write.js +2 -0
- package/dist/workflow-runner/db/queries/due-runs.js +6 -9
- package/dist/workflow-runner/event-subscriber.js +29 -9
- package/dist/workflow-runner/handlers/resume-run.write.js +8 -5
- package/dist/workflow-runner/pending-projection.js +8 -1
- package/package.json +11 -9
- package/src/agent-tools/changes.json +12 -0
- package/src/auth-email-password/changes.json +18 -0
- package/src/auth-mfa/changes.json +6 -0
- package/src/billing-foundation/changes.json +7 -0
- package/src/cap-counter/changes.json +7 -0
- package/src/cap-overview/changes.json +12 -0
- package/src/channel-email/changes.json +6 -0
- package/src/config/changes.json +6 -0
- package/src/crypto-shredding/changes.json +6 -0
- package/src/data-retention/changes.json +12 -0
- package/src/delivery/changes.json +7 -0
- package/src/derivatives-sharp/changes.json +11 -0
- package/src/document-ingest-foundation/changes.json +12 -0
- package/src/inbound-mail-foundation/changes.json +12 -0
- package/src/jobs/changes.json +13 -0
- package/src/legal-pages/changes.json +8 -1
- package/src/managed-pages/changes.json +5 -0
- package/src/metrics/changes.json +6 -0
- package/src/rate-limiting/changes.json +5 -0
- package/src/secrets/changes.json +7 -0
- package/src/sessions/changes.json +5 -0
- package/src/shared/changes.json +6 -0
- package/src/step-dispatcher/changes.json +5 -0
- package/src/subscription-mollie/changes.json +7 -0
- package/src/subscription-stripe/changes.json +11 -0
- package/src/template-resolver/changes.json +7 -0
- package/src/tenant/changes.json +29 -0
- package/src/user-data-rights/changes.json +22 -0
- package/src/workflow-runner/changes.json +6 -0
|
@@ -121,7 +121,8 @@ export function createInboundMailSupervisor(deps) {
|
|
|
121
121
|
return null; // korrupter Cursor = wie kein Cursor → Backfill, Dedup fängt Dubletten
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
|
-
async function saveCursor(
|
|
124
|
+
async function saveCursor(account, cursor) {
|
|
125
|
+
const accountId = account.id;
|
|
125
126
|
const now = Temporal.Now.instant().toString();
|
|
126
127
|
const serialized = JSON.stringify(cursor);
|
|
127
128
|
const existing = await fetchOne(deps.db, syncCursorTable, {
|
|
@@ -135,6 +136,8 @@ export function createInboundMailSupervisor(deps) {
|
|
|
135
136
|
}
|
|
136
137
|
await insertOne(deps.db, syncCursorTable, {
|
|
137
138
|
id: crypto.randomUUID(),
|
|
139
|
+
// Direct-write store: no executor stamps the NOT NULL tenant_id for us.
|
|
140
|
+
tenantId: account.tenantId,
|
|
138
141
|
accountId,
|
|
139
142
|
scope: CURSOR_SCOPE,
|
|
140
143
|
cursor: serialized,
|
|
@@ -227,7 +230,7 @@ export function createInboundMailSupervisor(deps) {
|
|
|
227
230
|
maxMessages: budget,
|
|
228
231
|
});
|
|
229
232
|
await ingestBatch(account, result.messages, JSON.stringify(result.nextCursor));
|
|
230
|
-
await saveCursor(account
|
|
233
|
+
await saveCursor(account, result.nextCursor);
|
|
231
234
|
cursor = result.nextCursor;
|
|
232
235
|
budget -= result.messages.length;
|
|
233
236
|
if (!result.hasMore || budget <= 0)
|
|
@@ -267,51 +270,52 @@ export function createInboundMailSupervisor(deps) {
|
|
|
267
270
|
// lifecycle — it keeps renewing across reconnect backoff, not just while
|
|
268
271
|
// `plugin.watch()` is actually connected, so a flaky IMAP link doesn't
|
|
269
272
|
// make this worker lose the account to a peer mid-backoff.
|
|
270
|
-
// kumiko-lint-ignore complexity-budget watch lease renew backoff/re-acquire loop
|
|
271
273
|
function scheduleRenew(account, state, generation) {
|
|
272
274
|
state.renewTimer = setTimeout(() => {
|
|
273
|
-
|
|
274
|
-
void (async () => {
|
|
275
|
-
state.renewTimer = null;
|
|
276
|
-
// skip: supervisor stopped, generation superseded, or lease already lost — stale timer fire, nothing to renew.
|
|
277
|
-
if (!running || state.generation !== generation || !state.lockToken || !deps.lock)
|
|
278
|
-
return;
|
|
279
|
-
let renewed;
|
|
280
|
-
try {
|
|
281
|
-
renewed = await deps.lock.renew(account.id, state.lockToken, leaseTtlSeconds);
|
|
282
|
-
}
|
|
283
|
-
catch (err) {
|
|
284
|
-
log(`inbound-mail: watch lease renew for account ${account.id} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
285
|
-
state.renewFailures += 1;
|
|
286
|
-
// After failures spanning a full lease TTL, tear down — otherwise a
|
|
287
|
-
// peer can acquire the expired key while we still hold an IDLE conn.
|
|
288
|
-
if (state.renewFailures * renewIntervalMs >= leaseTtlSeconds * 1000) {
|
|
289
|
-
log(`inbound-mail: watch lease renew for account ${account.id} failed across TTL — tearing down local watcher`);
|
|
290
|
-
state.lockToken = null;
|
|
291
|
-
await stopWatcher(account.id);
|
|
292
|
-
// skip: local watcher torn down after renew failures spanning a full lease TTL.
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
if (running && state.generation === generation && state.lockToken) {
|
|
296
|
-
scheduleRenew(account, state, generation);
|
|
297
|
-
}
|
|
298
|
-
// skip: renew already rescheduled above (or conditions no longer hold) — nothing left to do this tick.
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
state.renewFailures = 0;
|
|
302
|
-
if (!renewed) {
|
|
303
|
-
log(`inbound-mail: watch lease for account ${account.id} lost — tearing down local watcher`);
|
|
304
|
-
state.lockToken = null;
|
|
305
|
-
await stopWatcher(account.id);
|
|
306
|
-
// skip: watcher already torn down by stopWatcher() above — nothing left to do.
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
if (running && state.generation === generation && state.lockToken) {
|
|
310
|
-
scheduleRenew(account, state, generation);
|
|
311
|
-
}
|
|
312
|
-
})();
|
|
275
|
+
void renewLeaseOnce(account, state, generation);
|
|
313
276
|
}, renewIntervalMs);
|
|
314
277
|
}
|
|
278
|
+
async function tearDownAfterRenewFailures(account, state, reason) {
|
|
279
|
+
log(`inbound-mail: watch lease ${reason} for account ${account.id} — tearing down local watcher`);
|
|
280
|
+
state.lockToken = null;
|
|
281
|
+
await stopWatcher(account.id);
|
|
282
|
+
}
|
|
283
|
+
async function renewLeaseOnce(account, state, generation) {
|
|
284
|
+
state.renewTimer = null;
|
|
285
|
+
// skip: supervisor stopped, generation superseded, or lease already lost — stale timer fire, nothing to renew.
|
|
286
|
+
if (!running || state.generation !== generation || !state.lockToken || !deps.lock)
|
|
287
|
+
return;
|
|
288
|
+
let renewed;
|
|
289
|
+
try {
|
|
290
|
+
renewed = await deps.lock.renew(account.id, state.lockToken, leaseTtlSeconds);
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
log(`inbound-mail: watch lease renew for account ${account.id} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
294
|
+
state.renewFailures += 1;
|
|
295
|
+
// After failures spanning a full lease TTL, tear down — otherwise a
|
|
296
|
+
// peer can acquire the expired key while we still hold an IDLE conn.
|
|
297
|
+
if (state.renewFailures * renewIntervalMs >= leaseTtlSeconds * 1000) {
|
|
298
|
+
await tearDownAfterRenewFailures(account, state, "renew failed across TTL");
|
|
299
|
+
// skip: connection torn down, no reschedule
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
rescheduleIfStillHeld(account, state, generation);
|
|
303
|
+
// skip: renew failed but TTL not yet exhausted, retry already scheduled
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
state.renewFailures = 0;
|
|
307
|
+
if (!renewed) {
|
|
308
|
+
await tearDownAfterRenewFailures(account, state, "lost");
|
|
309
|
+
// skip: lease lost and torn down, nothing to reschedule
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
rescheduleIfStillHeld(account, state, generation);
|
|
313
|
+
}
|
|
314
|
+
function rescheduleIfStillHeld(account, state, generation) {
|
|
315
|
+
if (running && state.generation === generation && state.lockToken) {
|
|
316
|
+
scheduleRenew(account, state, generation);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
315
319
|
// kumiko-lint-ignore complexity-budget yield-point stale-map check after acquire is intentional (#2460)
|
|
316
320
|
async function acquireWatchLease(account, state) {
|
|
317
321
|
if (!deps.lock || state.lockToken)
|
|
@@ -8,9 +8,14 @@ export declare const listQuery: import("@cosmicdrift/kumiko-types/define-handler
|
|
|
8
8
|
running: "running";
|
|
9
9
|
}>>;
|
|
10
10
|
filters: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
11
|
-
field: z.
|
|
11
|
+
field: z.ZodLiteral<"status">;
|
|
12
12
|
op: z.ZodLiteral<"in">;
|
|
13
|
-
value: z.ZodArray<z.
|
|
13
|
+
value: z.ZodArray<z.ZodEnum<{
|
|
14
|
+
completed: "completed";
|
|
15
|
+
failed: "failed";
|
|
16
|
+
queued: "queued";
|
|
17
|
+
running: "running";
|
|
18
|
+
}>>;
|
|
14
19
|
}, z.core.$strip>>>;
|
|
15
20
|
sort: z.ZodOptional<z.ZodEnum<{
|
|
16
21
|
duration: "duration";
|
|
@@ -5,6 +5,7 @@ import * as z from "zod";
|
|
|
5
5
|
import { decryptStoredPii, mapWithConcurrency } from "../../shared/index.js";
|
|
6
6
|
import { jobRunsTable } from "../job-run-table.js";
|
|
7
7
|
const KMS_POOL_CONCURRENCY = 4;
|
|
8
|
+
const jobRunStatusSchema = z.enum(["queued", "running", "completed", "failed"]);
|
|
8
9
|
async function decryptRunRow(row) {
|
|
9
10
|
let result = row;
|
|
10
11
|
if (typeof result["payload"] === "string") {
|
|
@@ -23,9 +24,13 @@ export const listQuery = defineQueryHandler({
|
|
|
23
24
|
description: "Lists job runs across all tenants newest-first, optionally filtered by job name and status; use it to check whether a job ran and whether it succeeded.",
|
|
24
25
|
schema: z.object({
|
|
25
26
|
jobName: z.string().optional(),
|
|
26
|
-
status:
|
|
27
|
+
status: jobRunStatusSchema.optional(),
|
|
27
28
|
filters: z
|
|
28
|
-
.array(z.object({
|
|
29
|
+
.array(z.object({
|
|
30
|
+
field: z.literal("status"),
|
|
31
|
+
op: z.literal("in"),
|
|
32
|
+
value: z.array(jobRunStatusSchema),
|
|
33
|
+
}))
|
|
29
34
|
.optional(),
|
|
30
35
|
sort: z.enum(["jobName", "status", "startedAt", "duration"]).optional(),
|
|
31
36
|
sortDirection: z.enum(["asc", "desc"]).optional(),
|
|
@@ -47,6 +52,8 @@ export const listQuery = defineQueryHandler({
|
|
|
47
52
|
const statuses = statusFilter?.value ?? (query.payload.status ? [query.payload.status] : []);
|
|
48
53
|
if (statuses.length === 1)
|
|
49
54
|
where["status"] = statuses[0];
|
|
55
|
+
else if (statuses.length > 1)
|
|
56
|
+
where["status"] = { in: statuses };
|
|
50
57
|
const sortColumn = query.payload.sort ?? "startedAt";
|
|
51
58
|
const rows = await selectMany(db, jobRunsTable, where, {
|
|
52
59
|
orderBy: { col: sortColumn, direction: query.payload.sortDirection ?? "desc" },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
import { PII_ERASED_SENTINEL } from "@cosmicdrift/kumiko-framework/crypto";
|
|
2
|
+
import { isPiiCiphertext, PII_ERASED_SENTINEL } from "@cosmicdrift/kumiko-framework/crypto";
|
|
3
3
|
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
import { InternalError, NotFoundError, UnprocessableError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
5
5
|
import { parseJsonOrThrow } from "@cosmicdrift/kumiko-framework/utils";
|
|
@@ -44,7 +44,8 @@ export const retryWrite = defineWriteHandler({
|
|
|
44
44
|
let decryptedPayloadJson = null;
|
|
45
45
|
if (run.payload) {
|
|
46
46
|
const decryptedPayload = await decryptStoredPii(run.payload, "payload", `job run ${event.payload.runId} payload`);
|
|
47
|
-
|
|
47
|
+
// Still-ciphertext means no KMS could decrypt it (rollout mode); not JSON either.
|
|
48
|
+
if (decryptedPayload === PII_ERASED_SENTINEL || isPiiCiphertext(decryptedPayload)) {
|
|
48
49
|
return writeFailure(new UnprocessableError(JobErrors.payloadErased, {
|
|
49
50
|
i18nKey: "jobs.errors.payloadErased",
|
|
50
51
|
}));
|
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { access, defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
|
+
import { serializeJobSubject } from "@cosmicdrift/kumiko-framework/jobs";
|
|
4
5
|
import { parseJsonSafe } from "@cosmicdrift/kumiko-framework/utils";
|
|
5
6
|
import * as z from "zod";
|
|
6
7
|
import { tenantJobFailuresTable } from "../tenant-job-failure-table.js";
|
|
7
8
|
const DEFAULT_LIMIT = 50;
|
|
8
|
-
// Mirrors job-runner.ts's jobSubjectKey: sorted field names, so a caller that
|
|
9
|
-
// passes the same subject values gets the same string the writer stored.
|
|
10
|
-
function subjectKey(subject) {
|
|
11
|
-
return JSON.stringify(Object.keys(subject)
|
|
12
|
-
.sort()
|
|
13
|
-
.map((field) => [field, subject[field] ?? null]));
|
|
14
|
-
}
|
|
15
9
|
function isSubjectEntry(value) {
|
|
16
10
|
return Array.isArray(value) && typeof value[0] === "string";
|
|
17
11
|
}
|
|
@@ -28,7 +22,7 @@ function parseSubject(stored) {
|
|
|
28
22
|
}
|
|
29
23
|
export const tenantFailuresQuery = defineQueryHandler({
|
|
30
24
|
name: "failures",
|
|
31
|
-
description: "Lists the calling tenant's own failed jobs — one record per job and subject, newest first, each carrying a translation key for the reason, never the provider's own error message; use it to tell a tenant that their asynchronous job failed instead of leaving the screen waiting.",
|
|
25
|
+
description: "Lists the calling tenant's own failed jobs — one record per job and subject, newest first, each carrying a translation key for the reason, never the provider's own error message; the optional subject filter matches exactly: all declared subjectFields with the same JSON types as the job payload; use it to tell a tenant that their asynchronous job failed instead of leaving the screen waiting.",
|
|
32
26
|
schema: z.object({
|
|
33
27
|
jobName: z.string().optional(),
|
|
34
28
|
subject: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
@@ -52,7 +46,7 @@ export const tenantFailuresQuery = defineQueryHandler({
|
|
|
52
46
|
if (query.payload.jobName)
|
|
53
47
|
where["jobName"] = query.payload.jobName;
|
|
54
48
|
if (query.payload.subject)
|
|
55
|
-
where["subject"] =
|
|
49
|
+
where["subject"] = serializeJobSubject(query.payload.subject);
|
|
56
50
|
const rows = await selectMany(db, tenantJobFailuresTable, where, {
|
|
57
51
|
orderBy: { col: "failedAt", direction: "desc" },
|
|
58
52
|
limit: query.payload.limit ?? DEFAULT_LIMIT,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { deleteMany, fetchOne, insertMany, insertOne, updateMany, } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { configuredPiiSubjectKms, encryptPiiValueForSubject, KeyErasedError, PII_ERASED_SENTINEL, } from "@cosmicdrift/kumiko-framework/crypto";
|
|
3
3
|
import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
4
5
|
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
5
6
|
import { mapWithConcurrency } from "../shared/index.js";
|
|
6
7
|
import { runCompletedSchema, runFailedSchema, runStartedSchema } from "./events.js";
|
|
@@ -128,6 +129,21 @@ async function clearTenantJobFailure(db, jobName, outcome) {
|
|
|
128
129
|
return;
|
|
129
130
|
await deleteMany(db, tenantJobFailuresTable, where);
|
|
130
131
|
}
|
|
132
|
+
const log = createFallbackLogger("job-run-logger");
|
|
133
|
+
// The tenant failure record is an optional side write: a throw here must not
|
|
134
|
+
// skip the run-row update or turn a completed run into a BullMQ retry.
|
|
135
|
+
async function isolateTenantFailureWrite(write, jobName, bullJobId) {
|
|
136
|
+
try {
|
|
137
|
+
await write();
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
log.error("tenant job failure record write failed", {
|
|
141
|
+
jobName,
|
|
142
|
+
bullJobId,
|
|
143
|
+
error: e instanceof Error ? e.message : String(e),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
131
147
|
export function createJobRunLogger(opts) {
|
|
132
148
|
const { db } = opts;
|
|
133
149
|
const runIdByBullJobId = new Map();
|
|
@@ -210,7 +226,7 @@ export function createJobRunLogger(opts) {
|
|
|
210
226
|
// Before the run-row write and independent of it: a successful run
|
|
211
227
|
// clears the tenant's failure record even when the run row itself is
|
|
212
228
|
// unreachable (the state-loss return below).
|
|
213
|
-
await clearTenantJobFailure(db, jobName, outcome);
|
|
229
|
+
await isolateTenantFailureWrite(() => clearTenantJobFailure(db, jobName, outcome), jobName, bullJobId);
|
|
214
230
|
const resolved = await resolveRun(bullJobId);
|
|
215
231
|
// skip: state loss between start + complete (worker restart, cache
|
|
216
232
|
// evicted AND DB has no matching bull_job_id). Rare edge case; we
|
|
@@ -255,7 +271,7 @@ export function createJobRunLogger(opts) {
|
|
|
255
271
|
onJobFailed: async (jobName, bullJobId, error, logs, outcome) => {
|
|
256
272
|
// Mirror of onJobComplete: recorded independently of the run row, so a
|
|
257
273
|
// tenant still learns their job failed if the row is unreachable.
|
|
258
|
-
await recordTenantJobFailure(db, jobName, outcome);
|
|
274
|
+
await isolateTenantFailureWrite(() => recordTenantJobFailure(db, jobName, outcome), jobName, bullJobId);
|
|
259
275
|
const resolved = await resolveRun(bullJobId);
|
|
260
276
|
// skip: same rare state-loss case as in onJobComplete — drop the
|
|
261
277
|
// failure write rather than forge a run row from scratch.
|
|
@@ -12,6 +12,7 @@ export declare const jobRunEntity: import("@cosmicdrift/kumiko-types/fields").En
|
|
|
12
12
|
readonly filterable?: boolean;
|
|
13
13
|
readonly encrypted?: boolean;
|
|
14
14
|
readonly sensitive?: boolean;
|
|
15
|
+
readonly writeOnly?: boolean;
|
|
15
16
|
readonly readAsInstruction?: boolean;
|
|
16
17
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
17
18
|
readonly default?: string;
|
|
@@ -32,6 +33,7 @@ export declare const jobRunEntity: import("@cosmicdrift/kumiko-types/fields").En
|
|
|
32
33
|
readonly filterable?: boolean;
|
|
33
34
|
readonly encrypted?: boolean;
|
|
34
35
|
readonly sensitive?: boolean;
|
|
36
|
+
readonly writeOnly?: boolean;
|
|
35
37
|
readonly readAsInstruction?: boolean;
|
|
36
38
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
37
39
|
readonly default?: string;
|
|
@@ -52,6 +54,7 @@ export declare const jobRunEntity: import("@cosmicdrift/kumiko-types/fields").En
|
|
|
52
54
|
readonly filterable?: boolean;
|
|
53
55
|
readonly encrypted?: boolean;
|
|
54
56
|
readonly sensitive?: boolean;
|
|
57
|
+
readonly writeOnly?: boolean;
|
|
55
58
|
readonly readAsInstruction?: boolean;
|
|
56
59
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
57
60
|
readonly default?: string;
|
|
@@ -72,6 +75,7 @@ export declare const jobRunEntity: import("@cosmicdrift/kumiko-types/fields").En
|
|
|
72
75
|
readonly filterable?: boolean;
|
|
73
76
|
readonly encrypted?: boolean;
|
|
74
77
|
readonly sensitive?: boolean;
|
|
78
|
+
readonly writeOnly?: boolean;
|
|
75
79
|
readonly readAsInstruction?: boolean;
|
|
76
80
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
77
81
|
readonly default?: string;
|
|
@@ -92,6 +96,7 @@ export declare const jobRunEntity: import("@cosmicdrift/kumiko-types/fields").En
|
|
|
92
96
|
readonly filterable?: boolean;
|
|
93
97
|
readonly encrypted?: boolean;
|
|
94
98
|
readonly sensitive?: boolean;
|
|
99
|
+
readonly writeOnly?: boolean;
|
|
95
100
|
readonly readAsInstruction?: boolean;
|
|
96
101
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
97
102
|
readonly default?: string;
|
|
@@ -180,6 +185,7 @@ export declare const jobRunEntity: import("@cosmicdrift/kumiko-types/fields").En
|
|
|
180
185
|
readonly filterable?: boolean;
|
|
181
186
|
readonly encrypted?: boolean;
|
|
182
187
|
readonly sensitive?: boolean;
|
|
188
|
+
readonly writeOnly?: boolean;
|
|
183
189
|
readonly readAsInstruction?: boolean;
|
|
184
190
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
185
191
|
readonly default?: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { requireEntityTableMeta } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { instant, table as pgTable, serial, sql, text, uuid, } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
// The tenant's own view of a failed job (fw#3079). Direct-write store like
|
|
4
4
|
// store_job_runs: job-run-logger.ts writes it from the BullMQ callbacks,
|
|
@@ -22,8 +22,4 @@ export const tenantJobFailuresTable = pgTable("store_tenant_job_failures", {
|
|
|
22
22
|
messageKey: text("message_key").notNull(),
|
|
23
23
|
failedAt: instant("failed_at").default(sql `now()`).notNull(),
|
|
24
24
|
});
|
|
25
|
-
const
|
|
26
|
-
if (!derivedTenantJobFailuresTableMeta) {
|
|
27
|
-
throw new Error("tenantJobFailuresTable: table carries no EntityTableMeta — built via table()?");
|
|
28
|
-
}
|
|
29
|
-
export const tenantJobFailuresTableMeta = derivedTenantJobFailuresTableMeta;
|
|
25
|
+
export const tenantJobFailuresTableMeta = requireEntityTableMeta(tenantJobFailuresTable, "tenantJobFailuresTable");
|
package/dist/ledger/entity.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export declare const accountEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
9
9
|
readonly filterable?: boolean;
|
|
10
10
|
readonly encrypted?: boolean;
|
|
11
11
|
readonly sensitive?: boolean;
|
|
12
|
+
readonly writeOnly?: boolean;
|
|
12
13
|
readonly readAsInstruction?: boolean;
|
|
13
14
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
14
15
|
readonly default?: string;
|
|
@@ -38,7 +39,7 @@ export declare const accountEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
38
39
|
readonly liability?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
|
|
39
40
|
} | undefined;
|
|
40
41
|
readonly optionsQuery?: string;
|
|
41
|
-
readonly optionsQueryPayload?:
|
|
42
|
+
readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
|
|
42
43
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
43
44
|
required: true;
|
|
44
45
|
};
|
|
@@ -52,6 +53,7 @@ export declare const accountEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
52
53
|
readonly filterable?: boolean;
|
|
53
54
|
readonly encrypted?: boolean;
|
|
54
55
|
readonly sensitive?: boolean;
|
|
56
|
+
readonly writeOnly?: boolean;
|
|
55
57
|
readonly readAsInstruction?: boolean;
|
|
56
58
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
57
59
|
readonly default?: string;
|
|
@@ -72,6 +74,7 @@ export declare const accountEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
72
74
|
readonly filterable?: boolean;
|
|
73
75
|
readonly encrypted?: boolean;
|
|
74
76
|
readonly sensitive?: boolean;
|
|
77
|
+
readonly writeOnly?: boolean;
|
|
75
78
|
readonly readAsInstruction?: boolean;
|
|
76
79
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
77
80
|
readonly default?: string;
|
|
@@ -108,6 +111,7 @@ export declare const transactionEntity: import("@cosmicdrift/kumiko-types/fields
|
|
|
108
111
|
readonly filterable?: boolean;
|
|
109
112
|
readonly encrypted?: boolean;
|
|
110
113
|
readonly sensitive?: boolean;
|
|
114
|
+
readonly writeOnly?: boolean;
|
|
111
115
|
readonly readAsInstruction?: boolean;
|
|
112
116
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
113
117
|
readonly default?: string;
|
|
@@ -128,6 +132,7 @@ export declare const transactionEntity: import("@cosmicdrift/kumiko-types/fields
|
|
|
128
132
|
readonly filterable?: boolean;
|
|
129
133
|
readonly encrypted?: boolean;
|
|
130
134
|
readonly sensitive?: boolean;
|
|
135
|
+
readonly writeOnly?: boolean;
|
|
131
136
|
readonly readAsInstruction?: boolean;
|
|
132
137
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
133
138
|
readonly default?: string;
|
|
@@ -154,7 +159,7 @@ export declare const transactionEntity: import("@cosmicdrift/kumiko-types/fields
|
|
|
154
159
|
readonly posted?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
|
|
155
160
|
} | undefined;
|
|
156
161
|
readonly optionsQuery?: string;
|
|
157
|
-
readonly optionsQueryPayload?:
|
|
162
|
+
readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
|
|
158
163
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
159
164
|
required: true;
|
|
160
165
|
};
|
|
@@ -184,6 +189,7 @@ export declare const transactionEntity: import("@cosmicdrift/kumiko-types/fields
|
|
|
184
189
|
readonly filterable?: boolean;
|
|
185
190
|
readonly encrypted?: boolean;
|
|
186
191
|
readonly sensitive?: boolean;
|
|
192
|
+
readonly writeOnly?: boolean;
|
|
187
193
|
readonly readAsInstruction?: boolean;
|
|
188
194
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
189
195
|
readonly default?: string;
|
|
@@ -204,6 +210,7 @@ export declare const transactionEntity: import("@cosmicdrift/kumiko-types/fields
|
|
|
204
210
|
readonly filterable?: boolean;
|
|
205
211
|
readonly encrypted?: boolean;
|
|
206
212
|
readonly sensitive?: boolean;
|
|
213
|
+
readonly writeOnly?: boolean;
|
|
207
214
|
readonly readAsInstruction?: boolean;
|
|
208
215
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
209
216
|
readonly default?: string;
|
|
@@ -226,6 +233,7 @@ export declare const scheduleEntity: import("@cosmicdrift/kumiko-types/fields").
|
|
|
226
233
|
readonly filterable?: boolean;
|
|
227
234
|
readonly encrypted?: boolean;
|
|
228
235
|
readonly sensitive?: boolean;
|
|
236
|
+
readonly writeOnly?: boolean;
|
|
229
237
|
readonly readAsInstruction?: boolean;
|
|
230
238
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
231
239
|
readonly default?: string;
|
|
@@ -279,7 +287,7 @@ export declare const scheduleEntity: import("@cosmicdrift/kumiko-types/fields").
|
|
|
279
287
|
readonly monthly?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
|
|
280
288
|
} | undefined;
|
|
281
289
|
readonly optionsQuery?: string;
|
|
282
|
-
readonly optionsQueryPayload?:
|
|
290
|
+
readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
|
|
283
291
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
284
292
|
required: true;
|
|
285
293
|
};
|
|
@@ -312,6 +320,7 @@ export declare const scheduleEntity: import("@cosmicdrift/kumiko-types/fields").
|
|
|
312
320
|
readonly filterable?: boolean;
|
|
313
321
|
readonly encrypted?: boolean;
|
|
314
322
|
readonly sensitive?: boolean;
|
|
323
|
+
readonly writeOnly?: boolean;
|
|
315
324
|
readonly readAsInstruction?: boolean;
|
|
316
325
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
317
326
|
readonly default?: string;
|
|
@@ -332,6 +341,7 @@ export declare const scheduleEntity: import("@cosmicdrift/kumiko-types/fields").
|
|
|
332
341
|
readonly filterable?: boolean;
|
|
333
342
|
readonly encrypted?: boolean;
|
|
334
343
|
readonly sensitive?: boolean;
|
|
344
|
+
readonly writeOnly?: boolean;
|
|
335
345
|
readonly readAsInstruction?: boolean;
|
|
336
346
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
337
347
|
readonly default?: string;
|
|
@@ -352,6 +362,7 @@ export declare const scheduleEntity: import("@cosmicdrift/kumiko-types/fields").
|
|
|
352
362
|
readonly filterable?: boolean;
|
|
353
363
|
readonly encrypted?: boolean;
|
|
354
364
|
readonly sensitive?: boolean;
|
|
365
|
+
readonly writeOnly?: boolean;
|
|
355
366
|
readonly readAsInstruction?: boolean;
|
|
356
367
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
357
368
|
readonly default?: string;
|
|
@@ -372,6 +383,7 @@ export declare const scheduleEntity: import("@cosmicdrift/kumiko-types/fields").
|
|
|
372
383
|
readonly filterable?: boolean;
|
|
373
384
|
readonly encrypted?: boolean;
|
|
374
385
|
readonly sensitive?: boolean;
|
|
386
|
+
readonly writeOnly?: boolean;
|
|
375
387
|
readonly readAsInstruction?: boolean;
|
|
376
388
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
377
389
|
readonly default?: string;
|
|
@@ -392,6 +404,7 @@ export declare const scheduleEntity: import("@cosmicdrift/kumiko-types/fields").
|
|
|
392
404
|
readonly filterable?: boolean;
|
|
393
405
|
readonly encrypted?: boolean;
|
|
394
406
|
readonly sensitive?: boolean;
|
|
407
|
+
readonly writeOnly?: boolean;
|
|
395
408
|
readonly readAsInstruction?: boolean;
|
|
396
409
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
397
410
|
readonly default?: string;
|
|
@@ -4,6 +4,7 @@ import { DEFAULT_LEDGER_ACCESS } from "../constants.js";
|
|
|
4
4
|
import { accountExecutor, scheduleExecutor, transactionExecutor, transactionTable, } from "../executor.js";
|
|
5
5
|
import { findReversedIds, isoMonth, scheduleReference } from "../recurring.js";
|
|
6
6
|
import { confirmSchedulePeriodPayloadSchema, } from "../schemas.js";
|
|
7
|
+
import { readSubjectReference } from "../subject-reference.js";
|
|
7
8
|
// confirm-schedule-period — turn ONE projected period of a schedule into a posted,
|
|
8
9
|
// balanced transaction (debit +amount / credit −amount), tagged with
|
|
9
10
|
// scheduleReference(scheduleId, period) so the host can merge Soll vs. Ist.
|
|
@@ -70,8 +71,7 @@ export function createConfirmSchedulePeriodHandler(access = DEFAULT_LEDGER_ACCES
|
|
|
70
71
|
const amount = payload.amount ?? Number(schedule["amount"]);
|
|
71
72
|
// The subject is a property of the schedule, not the confirm call — a
|
|
72
73
|
// period without one stays without one, no backfill/error.
|
|
73
|
-
const
|
|
74
|
-
const subjectId = typeof schedule["subjectId"] === "string" ? schedule["subjectId"] : null;
|
|
74
|
+
const { subjectType, subjectId } = readSubjectReference(schedule);
|
|
75
75
|
const created = await transactionExecutor.create({
|
|
76
76
|
id: generateId(),
|
|
77
77
|
date: payload.date ?? `${payload.period}-01`,
|
|
@@ -4,6 +4,7 @@ import { DEFAULT_LEDGER_ACCESS } from "../constants.js";
|
|
|
4
4
|
import { transactionExecutor, transactionTable } from "../executor.js";
|
|
5
5
|
import { normalizeLines } from "../reports.js";
|
|
6
6
|
import { reverseTransactionPayloadSchema } from "../schemas.js";
|
|
7
|
+
import { readSubjectReference } from "../subject-reference.js";
|
|
7
8
|
// reverse-transaction (Storno) — the ONLY correction path for a posted entry.
|
|
8
9
|
// Books the mirror image (every amount negated) as a new posted entry that
|
|
9
10
|
// references the original, leaving the original untouched. This is why
|
|
@@ -51,8 +52,7 @@ export function createReverseTransactionHandler(access = DEFAULT_LEDGER_ACCESS)
|
|
|
51
52
|
}));
|
|
52
53
|
// The reversal shares its subject with the original so filtering by
|
|
53
54
|
// subjectId still finds the Storno; unset on the original stays unset.
|
|
54
|
-
const
|
|
55
|
-
const subjectId = typeof original["subjectId"] === "string" ? original["subjectId"] : null;
|
|
55
|
+
const { subjectType, subjectId } = readSubjectReference(original);
|
|
56
56
|
return transactionExecutor.create({
|
|
57
57
|
id: generateId(),
|
|
58
58
|
// Pass the original's date through untyped — create takes Record<string,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Subject columns are nullable; anything that is not a string reads as unset.
|
|
2
|
+
export function readSubjectReference(row) {
|
|
3
|
+
return {
|
|
4
|
+
subjectType: typeof row["subjectType"] === "string" ? row["subjectType"] : null,
|
|
5
|
+
subjectId: typeof row["subjectId"] === "string" ? row["subjectId"] : null,
|
|
6
|
+
};
|
|
7
|
+
}
|
|
@@ -36,6 +36,12 @@ export function createLegalPagesFeature(opts = {}) {
|
|
|
36
36
|
const routes = opts.routes ?? LEGAL_ROUTES;
|
|
37
37
|
const requiredBlocks = opts.requiredBlocks ?? LEGAL_REQUIRED_BLOCKS;
|
|
38
38
|
validateRoutes(routes);
|
|
39
|
+
if (opts.routes !== undefined && opts.routes.length > 0 && opts.requiredBlocks === undefined) {
|
|
40
|
+
const coversDefaultBlock = LEGAL_REQUIRED_BLOCKS.some((block) => opts.routes?.some((route) => route.slug === block.slug && route.lang === block.lang));
|
|
41
|
+
if (!coversDefaultBlock) {
|
|
42
|
+
throw new Error("legal-pages: custom `routes` without `requiredBlocks` — pass `requiredBlocks` explicitly (or `[]`)");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
39
45
|
return defineFeature("legal-pages", (r) => {
|
|
40
46
|
r.describe("Opt-in wrapper around `template-resolver` text-blocks that registers public HTML routes (default: `/legal/impressum`, `/legal/datenschutz`, `/legal/imprint`, `/legal/privacy`) with Markdown-to-HTML rendering and a boot-time job that hard-fails in production when the required blocks (default: `imprint/de`, `privacy/de`) are not seeded in `SYSTEM_TENANT`. Both are configurable via `routes`/`requiredBlocks` for apps with a different default language or additional pages. Requires `anonymousAccess: { defaultTenantId: SYSTEM_TENANT_ID }` and `extraContext.templateResolver` to be wired at app bootstrap; for per-tenant imprints or a custom layout call `template-resolver:query:by-slug` directly.");
|
|
41
47
|
r.uiHints({
|
|
@@ -75,9 +81,10 @@ export function createLegalPagesFeature(opts = {}) {
|
|
|
75
81
|
}
|
|
76
82
|
const etag = computeRevisionEtag([
|
|
77
83
|
SYSTEM_TENANT_ID,
|
|
84
|
+
route.path,
|
|
78
85
|
route.slug,
|
|
79
86
|
route.lang,
|
|
80
|
-
data.
|
|
87
|
+
String(data.modifiedAt ?? ""),
|
|
81
88
|
]);
|
|
82
89
|
const extra = { "content-type": "text/html; charset=utf-8" };
|
|
83
90
|
// 304 (Revision unverändert) und HEAD überspringen beide das
|
|
@@ -18,7 +18,7 @@ export function createBrandingQuery(opts) {
|
|
|
18
18
|
name: "branding",
|
|
19
19
|
schema: z.object({}),
|
|
20
20
|
access: { roles: ["anonymous", "User", "TenantAdmin", "SystemAdmin"] },
|
|
21
|
-
rateLimit: { per: "ip", limit: 60, windowSeconds: 60 },
|
|
21
|
+
rateLimit: { per: "ip+handler", limit: 60, windowSeconds: 60 },
|
|
22
22
|
description: "Reads the tenant's public branding values for the rendered page shell, adding the raw custom CSS only when the app opted in and the per-tenant CSS toggle is on; anonymous callers may read it because it dresses public pages.",
|
|
23
23
|
handler: async (_query, ctx) => {
|
|
24
24
|
const base = await readBranding(ctx.config);
|
|
@@ -15,7 +15,7 @@ export const bySlugQuery = defineQueryHandler({
|
|
|
15
15
|
lang: z.string().min(2).max(8),
|
|
16
16
|
}),
|
|
17
17
|
access: { roles: ["anonymous", "User", "TenantAdmin", "SystemAdmin"] },
|
|
18
|
-
rateLimit: { per: "ip", limit: 60, windowSeconds: 60 },
|
|
18
|
+
rateLimit: { per: "ip+handler", limit: 60, windowSeconds: 60 },
|
|
19
19
|
description: "Reads the body and metadata of one published managed page of the caller's tenant by slug and language, returning null for drafts and bodyless pages; use it to render a public page, not to load one for editing.",
|
|
20
20
|
handler: async (query, ctx) => {
|
|
21
21
|
const row = await fetchOne(ctx.db, pagesTable, {
|
|
@@ -11,7 +11,7 @@ export const byTenantPublishedQuery = defineQueryHandler({
|
|
|
11
11
|
tenantIdOverride: z.string().min(1).optional(),
|
|
12
12
|
}),
|
|
13
13
|
access: { roles: ["anonymous", "User", "TenantAdmin", "SystemAdmin"] },
|
|
14
|
-
rateLimit: { per: "ip", limit: 60, windowSeconds: 60 },
|
|
14
|
+
rateLimit: { per: "ip+handler", limit: 60, windowSeconds: 60 },
|
|
15
15
|
description: "Lists every published page of a tenant with slug, language, title and last-change time but no body; use it to enumerate the public pages for sitemap.xml or llms.txt, rather than by-slug which fetches one page's content.",
|
|
16
16
|
handler: async (query, ctx) => {
|
|
17
17
|
const override = query.payload.tenantIdOverride;
|