@cosmicdrift/kumiko-bundled-features 0.337.1 → 0.339.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-tools/tool-catalog.js +37 -17
- package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +3 -2
- package/dist/auth-email-password/handlers/invite-accept.write.js +3 -4
- package/dist/auth-email-password/handlers/invite-info.query.js +4 -1
- package/dist/auth-email-password/handlers/invite-signup-complete.write.js +3 -4
- package/dist/auth-email-password/handlers/signup-confirm.write.js +2 -1
- package/dist/auth-email-password/handlers/signup-request.write.js +2 -2
- package/dist/auth-email-password/web/auth-gate.d.ts +12 -2
- package/dist/auth-email-password/web/auth-gate.js +25 -1
- package/dist/auth-email-password/web/auth-redirect.d.ts +5 -0
- package/dist/auth-email-password/web/auth-redirect.js +58 -0
- package/dist/auth-email-password/web/client-plugin.d.ts +8 -1
- package/dist/auth-email-password/web/client-plugin.js +2 -0
- package/dist/auth-email-password/web/index.d.ts +2 -1
- package/dist/auth-email-password/web/index.js +1 -0
- package/dist/auth-email-password/web/session.d.ts +3 -1
- package/dist/auth-email-password/web/session.js +8 -2
- package/dist/auth-mfa/feature.js +1 -1
- package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +1 -1
- package/dist/auth-mfa/handlers/enable-start-preauth.write.js +2 -1
- package/dist/auth-mfa/handlers/verify.write.js +1 -1
- package/dist/auth-mfa/mfa-code-verifier.js +1 -1
- package/dist/auth-mfa/mfa-status-checker.js +1 -1
- package/dist/billing-foundation/aggregate-id.d.ts +2 -0
- package/dist/billing-foundation/aggregate-id.js +8 -0
- package/dist/billing-foundation/checkout-core.d.ts +3 -0
- package/dist/billing-foundation/checkout-core.js +3 -0
- package/dist/billing-foundation/constants.d.ts +38 -0
- package/dist/billing-foundation/constants.js +43 -0
- package/dist/billing-foundation/consumer-protection/confirmation-mail.d.ts +21 -0
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +125 -0
- package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +32 -0
- package/dist/billing-foundation/consumer-protection/consent-gate.js +96 -0
- package/dist/billing-foundation/consumer-protection/consent-locale.d.ts +5 -0
- package/dist/billing-foundation/consumer-protection/consent-locale.js +17 -0
- package/dist/billing-foundation/consumer-protection/consent-text.d.ts +11 -0
- package/dist/billing-foundation/consumer-protection/consent-text.js +28 -0
- package/dist/billing-foundation/consumer-protection/issue-confirmation.d.ts +15 -0
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +149 -0
- package/dist/billing-foundation/consumer-protection/register-consent.d.ts +3 -0
- package/dist/billing-foundation/consumer-protection/register-consent.js +63 -0
- package/dist/billing-foundation/consumer-protection/register-termination.d.ts +3 -0
- package/dist/billing-foundation/consumer-protection/register-termination.js +40 -0
- package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +45 -0
- package/dist/billing-foundation/consumer-protection/termination-mail.js +76 -0
- package/dist/billing-foundation/consumer-protection/termination-notify.d.ts +18 -0
- package/dist/billing-foundation/consumer-protection/termination-notify.js +21 -0
- package/dist/billing-foundation/consumer-protection/termination-pages.d.ts +8 -0
- package/dist/billing-foundation/consumer-protection/termination-pages.js +268 -0
- package/dist/billing-foundation/consumer-protection/termination-record.d.ts +73 -0
- package/dist/billing-foundation/consumer-protection/termination-record.js +230 -0
- package/dist/billing-foundation/consumer-protection/termination-request.d.ts +45 -0
- package/dist/billing-foundation/consumer-protection/termination-request.js +231 -0
- package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +62 -0
- package/dist/billing-foundation/consumer-protection/termination-texts.js +168 -0
- package/dist/billing-foundation/events.d.ts +105 -2
- package/dist/billing-foundation/events.js +88 -2
- package/dist/billing-foundation/feature.js +6 -2
- package/dist/billing-foundation/handlers/billing-plans.query.js +19 -1
- package/dist/billing-foundation/handlers/create-checkout-session.write.js +33 -2
- package/dist/billing-foundation/handlers/process-event.write.d.ts +2 -2
- package/dist/billing-foundation/handlers/process-event.write.js +43 -15
- package/dist/billing-foundation/handlers/process-payment-event.write.d.ts +2 -1
- package/dist/billing-foundation/handlers/process-payment-event.write.js +6 -2
- package/dist/billing-foundation/handlers/start-plan-checkout.write.js +17 -1
- package/dist/billing-foundation/handlers/sync-subscription.write.js +0 -1
- package/dist/billing-foundation/i18n.js +61 -0
- package/dist/billing-foundation/index.d.ts +3 -2
- package/dist/billing-foundation/index.js +2 -1
- package/dist/billing-foundation/tenant-destroy-hook.js +4 -4
- package/dist/billing-foundation/types.d.ts +54 -9
- package/dist/billing-foundation/validate-options.js +51 -10
- package/dist/billing-foundation/web/billing-plans-panel.js +40 -3
- package/dist/billing-foundation/web/cancel-contract-dialog.d.ts +7 -0
- package/dist/billing-foundation/web/cancel-contract-dialog.js +57 -0
- package/dist/billing-foundation/web/checkout-consent-dialog.d.ts +28 -0
- package/dist/billing-foundation/web/checkout-consent-dialog.js +38 -0
- package/dist/billing-foundation/webhook-handler.js +5 -2
- package/dist/cap-counter/with-cap-enforcement.js +7 -3
- package/dist/cap-overview/access.d.ts +2 -1
- package/dist/cap-overview/access.js +26 -5
- package/dist/cap-overview/feature.d.ts +4 -0
- package/dist/cap-overview/feature.js +8 -6
- package/dist/cap-overview/handlers/caps-usage.query.d.ts +1 -1
- package/dist/cap-overview/handlers/caps-usage.query.js +6 -4
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +12 -7
- package/dist/cap-overview/index.d.ts +2 -2
- package/dist/cap-overview/index.js +2 -2
- package/dist/cap-overview/screens.d.ts +1 -1
- package/dist/cap-overview/screens.js +17 -16
- package/dist/channel-discord/discord-channel.d.ts +6 -0
- package/dist/channel-discord/discord-channel.js +19 -0
- package/dist/channel-discord/feature.d.ts +3 -0
- package/dist/channel-discord/feature.js +30 -0
- package/dist/channel-discord/index.d.ts +2 -0
- package/dist/channel-discord/index.js +2 -0
- package/dist/channel-slack/feature.d.ts +3 -0
- package/dist/channel-slack/feature.js +30 -0
- package/dist/channel-slack/index.d.ts +2 -0
- package/dist/channel-slack/index.js +2 -0
- package/dist/channel-slack/slack-channel.d.ts +5 -0
- package/dist/channel-slack/slack-channel.js +16 -0
- package/dist/channel-teams/feature.d.ts +3 -0
- package/dist/channel-teams/feature.js +34 -0
- package/dist/channel-teams/index.d.ts +2 -0
- package/dist/channel-teams/index.js +2 -0
- package/dist/channel-teams/teams-channel.d.ts +5 -0
- package/dist/channel-teams/teams-channel.js +38 -0
- package/dist/channel-telegram/feature.d.ts +3 -0
- package/dist/channel-telegram/feature.js +28 -0
- package/dist/channel-telegram/index.d.ts +2 -0
- package/dist/channel-telegram/index.js +2 -0
- package/dist/channel-telegram/telegram-channel.d.ts +8 -0
- package/dist/channel-telegram/telegram-channel.js +54 -0
- package/dist/config/handlers/set.write.js +18 -2
- package/dist/crypto-shredding/feature.js +3 -2
- package/dist/crypto-shredding/handlers/forget-subject.write.d.ts +1 -1
- package/dist/crypto-shredding/handlers/forget-subject.write.js +30 -19
- package/dist/custom-fields/feature.js +9 -6
- package/dist/custom-fields/handlers/clear-custom-field.write.js +2 -1
- package/dist/custom-fields/handlers/define-tenant-field.write.js +2 -1
- package/dist/custom-fields/handlers/set-custom-field.write.js +2 -1
- package/dist/custom-fields/wire-user-data-rights.js +1 -1
- package/dist/data-retention/feature.js +1 -1
- package/dist/delivery/channel-context.d.ts +2 -1
- package/dist/delivery/channel-context.js +2 -2
- package/dist/delivery/chat-webhook-channel.d.ts +19 -0
- package/dist/delivery/chat-webhook-channel.js +48 -0
- package/dist/delivery/chat-webhook-sender.d.ts +21 -0
- package/dist/delivery/chat-webhook-sender.js +102 -0
- package/dist/delivery/delivery-service.js +12 -7
- package/dist/delivery/index.d.ts +2 -0
- package/dist/delivery/index.js +2 -0
- package/dist/delivery/jobs.js +1 -1
- package/dist/delivery/testing.d.ts +2 -0
- package/dist/delivery/types.d.ts +3 -1
- package/dist/delivery/types.js +1 -2
- package/dist/document-ingest-foundation/feature.js +3 -2
- package/dist/file-derivatives/handlers/public-variant-by-file-ref.query.js +1 -1
- package/dist/files-tenant-data/index.js +1 -1
- package/dist/form-draft/feature.js +1 -1
- package/dist/form-draft/handlers/discard.write.js +2 -1
- package/dist/form-draft/handlers/save.write.js +2 -1
- package/dist/inbound-mail-foundation/connect-routes.d.ts +7 -0
- package/dist/inbound-mail-foundation/connect-routes.js +13 -8
- package/dist/inbound-mail-foundation/feature.js +1 -1
- package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +2 -1
- package/dist/inbound-mail-foundation/index.d.ts +1 -0
- package/dist/inbound-mail-foundation/index.js +1 -0
- package/dist/inbound-mail-foundation/oauth-access-token.d.ts +28 -0
- package/dist/inbound-mail-foundation/oauth-access-token.js +131 -0
- package/dist/inbound-mail-foundation/tenant-destroy-hook.js +1 -1
- package/dist/inbound-mail-foundation/types.d.ts +18 -1
- package/dist/inbound-mail-foundation/watch-supervisor.d.ts +5 -0
- package/dist/inbound-mail-foundation/watch-supervisor.js +42 -14
- package/dist/managed-pages/handlers/set.write.js +2 -1
- package/dist/notes-history/executor.d.ts +2 -0
- package/dist/notes-history/executor.js +9 -1
- package/dist/notes-history/feature.d.ts +2 -3
- package/dist/notes-history/feature.js +2 -1
- package/dist/notes-history/handlers/add-note.write.d.ts +2 -1
- package/dist/notes-history/handlers/add-note.write.js +21 -4
- package/dist/notes-history-user-data/hooks.js +26 -8
- package/dist/renderer-simple/feature.d.ts +8 -2
- package/dist/renderer-simple/feature.js +8 -5
- package/dist/renderer-simple/index.d.ts +2 -2
- package/dist/renderer-simple/index.js +1 -1
- package/dist/renderer-simple/simple-renderer.d.ts +15 -0
- package/dist/renderer-simple/simple-renderer.js +74 -27
- package/dist/secrets/feature.js +1 -1
- package/dist/sessions/feature.js +1 -1
- package/dist/sessions/handlers/revoke-all-for-user.write.js +2 -1
- package/dist/subscription-mollie/plugin-methods.js +1 -0
- package/dist/subscription-mollie/verify-webhook.js +5 -3
- package/dist/subscription-stripe/plugin-methods.d.ts +3 -1
- package/dist/subscription-stripe/plugin-methods.js +78 -13
- package/dist/subscription-stripe/verify-webhook.js +7 -3
- package/dist/tags/executor.d.ts +2 -0
- package/dist/tags/executor.js +9 -1
- package/dist/tags/feature.d.ts +4 -3
- package/dist/tags/feature.js +4 -2
- package/dist/tags/handlers/assign-tag.write.d.ts +2 -1
- package/dist/tags/handlers/assign-tag.write.js +4 -4
- package/dist/tags/handlers/remove-tag.write.d.ts +2 -1
- package/dist/tags/handlers/remove-tag.write.js +3 -3
- package/dist/tenant-handover/handlers/claim.write.js +2 -2
- package/dist/tenant-lifecycle/feature.js +1 -1
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +4 -1
- package/dist/tenant-lifecycle/handlers/request-destruction.write.js +2 -1
- package/dist/tier-engine/feature.js +1 -1
- package/dist/tier-engine/handlers/get-tenant-tier.query.js +2 -1
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +2 -1
- package/dist/user/handlers/detail.query.js +1 -0
- package/dist/user/handlers/list.query.js +1 -0
- package/dist/user-data-rights/feature.js +2 -2
- package/dist/user-data-rights/handlers/cancel-deletion.write.js +2 -1
- package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +1 -1
- package/dist/user-data-rights/handlers/download-attempt-list.query.js +1 -0
- package/dist/user-data-rights/handlers/download-by-job.query.js +2 -1
- package/dist/user-data-rights/handlers/download-by-token.query.js +2 -1
- package/dist/user-data-rights/handlers/export-status.query.js +2 -1
- package/dist/user-data-rights/handlers/lift-restriction.write.js +3 -4
- package/dist/user-data-rights/handlers/my-audit-log.query.js +2 -1
- package/dist/user-data-rights/handlers/request-deletion-by-email.write.js +2 -1
- package/dist/user-data-rights/handlers/request-deletion.write.js +1 -1
- package/dist/user-data-rights/handlers/request-export.write.js +2 -1
- package/dist/user-data-rights/handlers/restrict-account.write.js +2 -2
- package/dist/user-data-rights/handlers/run-forget-cleanup.write.js +2 -1
- package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
- package/dist/user-data-rights/run-forget-cleanup.js +30 -15
- package/dist/workflow-runner/event-subscriber.js +6 -2
- package/dist/workflow-runner/event-trigger.js +5 -0
- package/dist/workflow-runner/handlers/resume-run.write.js +147 -59
- package/dist/workflow-runner/pending-projection.js +1 -0
- package/dist/workflow-runner/runner.d.ts +7 -1
- package/dist/workflow-runner/runner.js +1 -1
- package/dist/workflow-runner/tables.js +8 -1
- package/package.json +41 -9
- package/src/agent-tools/changes.json +14 -0
- package/src/auth-email-password/changes.json +7 -0
- package/src/billing-foundation/changes.json +83 -0
- package/src/cap-counter/changes.json +7 -0
- package/src/cap-overview/changes.json +14 -0
- package/src/config/changes.json +7 -0
- package/src/crypto-shredding/changes.json +13 -0
- package/src/custom-fields/changes.json +7 -0
- package/src/delivery/changes.json +14 -0
- package/src/inbound-mail-foundation/changes.json +7 -0
- package/src/notes-history/changes.json +7 -0
- package/src/renderer-simple/changes.json +9 -1
- package/src/subscription-mollie/changes.json +13 -0
- package/src/subscription-stripe/changes.json +13 -0
- package/src/tags/changes.json +7 -0
- package/src/user-data-rights/changes.json +6 -0
- package/src/workflow-runner/changes.json +7 -0
package/dist/tags/feature.d.ts
CHANGED
|
@@ -28,9 +28,10 @@ export type TagsFeatureOptions = {
|
|
|
28
28
|
* `tag-assignment`; the `tag` catalog stays tenant-wide by design.
|
|
29
29
|
*
|
|
30
30
|
* `ownership.write` is separate and does NOT affect list/read. It's
|
|
31
|
-
* consulted
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* consulted on every assignment write: assign-tag (create/restore),
|
|
32
|
+
* remove-tag (delete) and the framework's generic forget path. The
|
|
33
|
+
* delete-tag cascade stays ungated, since deleting a catalog tag detaches
|
|
34
|
+
* it everywhere. A `from()` rule there also gates GDPR erasure
|
|
34
35
|
* (`forget`): if the rule's role map doesn't cover whatever role the
|
|
35
36
|
* erasure/retention pipeline runs as, `forget` denies instead of
|
|
36
37
|
* crypto-shredding — a silent Art.17 failure, not a thrown error. Make
|
package/dist/tags/feature.js
CHANGED
|
@@ -21,6 +21,7 @@ import { defineEntityDetailHandler, defineEntityListHandler, defineEntityUpdateH
|
|
|
21
21
|
import { hasWhereRule } from "../shared/index.js";
|
|
22
22
|
import { DEFAULT_TAG_ACCESS, TAGS_FEATURE_NAME } from "./constants.js";
|
|
23
23
|
import { createTagAssignmentEntity, tagEntity } from "./entity.js";
|
|
24
|
+
import { createTagAssignmentExecutor } from "./executor.js";
|
|
24
25
|
import { createAssignTagHandler } from "./handlers/assign-tag.write.js";
|
|
25
26
|
import { createCreateTagHandler } from "./handlers/create-tag.write.js";
|
|
26
27
|
import { createDeleteTagHandler } from "./handlers/delete-tag.write.js";
|
|
@@ -45,8 +46,9 @@ function registerTags(r, access, toggleable, ownership, parents) {
|
|
|
45
46
|
r.writeHandler(createCreateTagHandler(access));
|
|
46
47
|
r.writeHandler(createUpdateTagHandler(access));
|
|
47
48
|
r.writeHandler(createDeleteTagHandler(access));
|
|
48
|
-
|
|
49
|
-
r.writeHandler(
|
|
49
|
+
const assignmentExecutor = createTagAssignmentExecutor(ownership);
|
|
50
|
+
r.writeHandler(createAssignTagHandler(access, assignmentExecutor));
|
|
51
|
+
r.writeHandler(createRemoveTagHandler(access, assignmentExecutor));
|
|
50
52
|
// Convention aliases for entityList/entityEdit — create stays flat-payload
|
|
51
53
|
// (payloadMode=values); update must accept the {id,version,changes} envelope
|
|
52
54
|
// entityEdit sends (legacy update-tag stays for TagManager). delete stays
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { AccessRule, WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
|
|
2
|
+
import { tagAssignmentExecutor } from "../executor.js";
|
|
3
|
+
export declare function createAssignTagHandler(access?: AccessRule, assignmentExecutor?: typeof tagAssignmentExecutor): WriteHandlerDef;
|
|
3
4
|
export declare const assignTagHandler: WriteHandlerDef;
|
|
@@ -25,7 +25,7 @@ import { assignTagPayloadSchema } from "../schemas.js";
|
|
|
25
25
|
// the aggregate-id is tenant-scoped, so this is not a cross-tenant hole, but
|
|
26
26
|
// without the check any dispatch-eligible tenant user could tag an object they
|
|
27
27
|
// are not allowed to read. See shared/parent-visibility.ts.
|
|
28
|
-
export function createAssignTagHandler(access = DEFAULT_TAG_ACCESS) {
|
|
28
|
+
export function createAssignTagHandler(access = DEFAULT_TAG_ACCESS, assignmentExecutor = tagAssignmentExecutor) {
|
|
29
29
|
return {
|
|
30
30
|
name: "assign-tag",
|
|
31
31
|
schema: assignTagPayloadSchema,
|
|
@@ -37,11 +37,11 @@ export function createAssignTagHandler(access = DEFAULT_TAG_ACCESS) {
|
|
|
37
37
|
if (denied)
|
|
38
38
|
return denied;
|
|
39
39
|
const id = tagAssignmentAggregateId(event.user.tenantId, payload.tagId, payload.entityType, payload.entityId);
|
|
40
|
-
const existing = await
|
|
40
|
+
const existing = await assignmentExecutor.detail({ id }, event.user, ctx.db);
|
|
41
41
|
if (existing) {
|
|
42
42
|
return { isSuccess: true, data: { id } };
|
|
43
43
|
}
|
|
44
|
-
const restored = await
|
|
44
|
+
const restored = await assignmentExecutor.restore({ id }, event.user, ctx.db);
|
|
45
45
|
if (restored.isSuccess)
|
|
46
46
|
return { isSuccess: true, data: { id } };
|
|
47
47
|
// A concurrent first-time assign can also land here: the other caller's
|
|
@@ -57,7 +57,7 @@ export function createAssignTagHandler(access = DEFAULT_TAG_ACCESS) {
|
|
|
57
57
|
const tag = await tagExecutor.detail({ id: payload.tagId }, event.user, ctx.db);
|
|
58
58
|
if (!tag)
|
|
59
59
|
return writeFailure(new NotFoundError("tag", payload.tagId));
|
|
60
|
-
const created = await
|
|
60
|
+
const created = await assignmentExecutor.create({
|
|
61
61
|
id,
|
|
62
62
|
tagId: payload.tagId,
|
|
63
63
|
entityType: payload.entityType,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { AccessRule, WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
|
|
2
|
+
import { tagAssignmentExecutor } from "../executor.js";
|
|
3
|
+
export declare function createRemoveTagHandler(access?: AccessRule, assignmentExecutor?: typeof tagAssignmentExecutor): WriteHandlerDef;
|
|
3
4
|
export declare const removeTagHandler: WriteHandlerDef;
|
|
@@ -10,7 +10,7 @@ import { removeTagPayloadSchema } from "../schemas.js";
|
|
|
10
10
|
// Host reference: entityType/entityId are client input, so the caller must be
|
|
11
11
|
// able to see the host row before detaching anything from it — see
|
|
12
12
|
// shared/parent-visibility.ts.
|
|
13
|
-
export function createRemoveTagHandler(access = DEFAULT_TAG_ACCESS) {
|
|
13
|
+
export function createRemoveTagHandler(access = DEFAULT_TAG_ACCESS, assignmentExecutor = tagAssignmentExecutor) {
|
|
14
14
|
return {
|
|
15
15
|
name: "remove-tag",
|
|
16
16
|
schema: removeTagPayloadSchema,
|
|
@@ -22,11 +22,11 @@ export function createRemoveTagHandler(access = DEFAULT_TAG_ACCESS) {
|
|
|
22
22
|
if (denied)
|
|
23
23
|
return denied;
|
|
24
24
|
const id = tagAssignmentAggregateId(event.user.tenantId, payload.tagId, payload.entityType, payload.entityId);
|
|
25
|
-
const existing = await
|
|
25
|
+
const existing = await assignmentExecutor.detail({ id }, event.user, ctx.db);
|
|
26
26
|
if (!existing) {
|
|
27
27
|
return { isSuccess: true, data: { id } };
|
|
28
28
|
}
|
|
29
|
-
return
|
|
29
|
+
return assignmentExecutor.delete({ id }, event.user, ctx.db);
|
|
30
30
|
},
|
|
31
31
|
};
|
|
32
32
|
}
|
|
@@ -28,6 +28,7 @@ export function createClaimTenantHandoverHandler(opts = {}) {
|
|
|
28
28
|
schema: z.object({ token: z.string().min(1), entityType: z.string().min(1) }),
|
|
29
29
|
access: { roles: ["Member", "User", "TenantAdmin", "SystemAdmin"] },
|
|
30
30
|
escapeHatch: {
|
|
31
|
+
grants: ["unsafeRaw"],
|
|
31
32
|
reason: "the ownership change moves rows OUT of a source tenant this caller has no membership " +
|
|
32
33
|
"in — the framework's declared cross-tenant operation for try-before-signup, not a " +
|
|
33
34
|
"per-consumer acknowledgeCrossTenant workaround",
|
|
@@ -35,8 +36,7 @@ export function createClaimTenantHandoverHandler(opts = {}) {
|
|
|
35
36
|
agent: { expose: false },
|
|
36
37
|
rateLimit: { per: "user", limit: 10, windowSeconds: 60 },
|
|
37
38
|
handler: async (event, ctx) => {
|
|
38
|
-
const db = ctx.db.unsafeRaw(
|
|
39
|
-
"row-bound grant redeemed below");
|
|
39
|
+
const db = ctx.db.unsafeRaw();
|
|
40
40
|
const rootLocation = resolveRootAnchorLocation(ctx.registry, db, event.payload.entityType);
|
|
41
41
|
if (!rootLocation)
|
|
42
42
|
return notTransferable(event.payload.entityType);
|
|
@@ -74,7 +74,7 @@ export function createTenantLifecycleFeature() {
|
|
|
74
74
|
}
|
|
75
75
|
const T = (await import("@cosmicdrift/kumiko-framework/time")).getTemporal();
|
|
76
76
|
await runTenantDestructionSweep({
|
|
77
|
-
db: ctx.db.unsafeRaw(
|
|
77
|
+
db: ctx.db.unsafeRaw(), // @cast-boundary db-operator — jobs never run inside a DbTx
|
|
78
78
|
registry: ctx.registry,
|
|
79
79
|
now: T.Now.instant(),
|
|
80
80
|
log: (message) => ctx.log?.warn(message),
|
|
@@ -15,11 +15,14 @@ export const cancelDestructionWrite = defineWriteHandler({
|
|
|
15
15
|
access: { roles: ["TenantOwner", "Admin"] },
|
|
16
16
|
description: "Returns a tenant whose destruction was requested back to active and clears the grace period; use it to undo a close-account request while the grace period is still running.",
|
|
17
17
|
escapeHatch: {
|
|
18
|
+
grants: ["unsafeRaw"],
|
|
18
19
|
reason: CANCEL_DESTRUCTION_TENANT_ROW_REASON,
|
|
19
20
|
},
|
|
20
21
|
handler: async (event, ctx) => {
|
|
21
22
|
const tenantId = event.user.tenantId;
|
|
22
|
-
const row = await fetchOne(ctx.db.unsafeRaw(
|
|
23
|
+
const row = await fetchOne(ctx.db.unsafeRaw(), tenantTable, {
|
|
24
|
+
id: tenantId,
|
|
25
|
+
});
|
|
23
26
|
if (!row) {
|
|
24
27
|
return writeFailure(new UnprocessableError("tenant_not_found", { details: { tenantId } }));
|
|
25
28
|
}
|
|
@@ -19,11 +19,12 @@ export const requestDestructionWrite = defineWriteHandler({
|
|
|
19
19
|
description: "Puts the caller's own tenant into destroyRequested, starts the compliance-profile grace period after which its data is erased, and revokes every session in the tenant; use it when an account owner asks to close their account.",
|
|
20
20
|
agent: { risk: "high" },
|
|
21
21
|
escapeHatch: {
|
|
22
|
+
grants: ["unsafeRaw"],
|
|
22
23
|
reason: REQUEST_DESTRUCTION_TENANT_ROW_REASON,
|
|
23
24
|
},
|
|
24
25
|
handler: async (event, ctx) => {
|
|
25
26
|
const tenantId = event.user.tenantId;
|
|
26
|
-
const runner = ctx.db.unsafeRaw(
|
|
27
|
+
const runner = ctx.db.unsafeRaw();
|
|
27
28
|
const row = await fetchOne(runner, tenantTable, { id: tenantId });
|
|
28
29
|
if (!row) {
|
|
29
30
|
return writeFailure(new UnprocessableError("tenant_not_found", { details: { tenantId } }));
|
|
@@ -303,7 +303,7 @@ export function createTierEngineFeature(opts = {}) {
|
|
|
303
303
|
const rawDb = ctx.systemDb
|
|
304
304
|
? ctx.systemDb.unsafeRaw(autoDefaultTierHookReason)
|
|
305
305
|
: ctx.db && "unsafeRaw" in ctx.db
|
|
306
|
-
? ctx.db.unsafeRaw(
|
|
306
|
+
? ctx.db.unsafeRaw()
|
|
307
307
|
: undefined;
|
|
308
308
|
// skip: defensive — inTransaction phase always sets db, but AppContext's
|
|
309
309
|
// type makes it optional. Throwing would be overreach (lifecycle
|
|
@@ -15,11 +15,12 @@ export const getTenantTierQuery = defineQueryHandler({
|
|
|
15
15
|
schema: z.object({ tenantId: z.string().min(1) }),
|
|
16
16
|
access: { roles: ["SystemAdmin"] },
|
|
17
17
|
escapeHatch: {
|
|
18
|
+
grants: ["unsafeRaw"],
|
|
18
19
|
reason: GET_TENANT_TIER_REASON,
|
|
19
20
|
},
|
|
20
21
|
handler: async (query, ctx) => {
|
|
21
22
|
const tenantId = query.payload.tenantId; // @cast-boundary engine-bridge
|
|
22
|
-
const tdb = createTenantDb(ctx.db.unsafeRaw(
|
|
23
|
+
const tdb = createTenantDb(ctx.db.unsafeRaw(), tenantId, "system");
|
|
23
24
|
const row = await fetchOne(tdb, tierAssignmentTable, { tenantId });
|
|
24
25
|
return row ?? null;
|
|
25
26
|
},
|
|
@@ -46,11 +46,12 @@ export function createSetTenantTierWrite(opts = {}) {
|
|
|
46
46
|
}),
|
|
47
47
|
access: { roles: ["SystemAdmin"] },
|
|
48
48
|
escapeHatch: {
|
|
49
|
+
grants: ["unsafeRaw"],
|
|
49
50
|
reason: SET_TENANT_TIER_REASON,
|
|
50
51
|
},
|
|
51
52
|
handler: async (event, ctx) => {
|
|
52
53
|
const tenantId = event.payload.tenantId; // @cast-boundary engine-bridge
|
|
53
|
-
const rawDb = ctx.db.unsafeRaw(
|
|
54
|
+
const rawDb = ctx.db.unsafeRaw();
|
|
54
55
|
const tdb = createTenantDb(rawDb, tenantId, "system");
|
|
55
56
|
const systemUser = { ...event.user, tenantId };
|
|
56
57
|
const tier = event.payload.tier;
|
|
@@ -6,6 +6,7 @@ import { attachTenantLabels, dbForList } from "./list.query.js";
|
|
|
6
6
|
// bound to their own tenant's memberships (served by the tenant feature).
|
|
7
7
|
const baseDetail = defineEntityDetailHandler("user", userEntity, {
|
|
8
8
|
access: { roles: access.systemAdmin },
|
|
9
|
+
agent: { risk: "high" },
|
|
9
10
|
description: "Loads one identity record by id for a SystemAdmin, enriched with the same derived `tenants` membership label as the user list; tenant-level admins do not reach it.",
|
|
10
11
|
});
|
|
11
12
|
// Same tenants enrichment as user:list — derived field placeholder is "".
|
|
@@ -6,6 +6,7 @@ import { tenantMembershipsTable, tenantTable } from "../../tenant/index.js";
|
|
|
6
6
|
import { userEntity } from "../schema/user.js";
|
|
7
7
|
const baseList = defineEntityListHandler("user", userEntity, {
|
|
8
8
|
access: { roles: access.systemAdmin },
|
|
9
|
+
agent: { risk: "high" },
|
|
9
10
|
description: "Lists identity records across every tenant for a SystemAdmin roster, each row carrying a derived `tenants` label naming the tenants the user belongs to and their roles there.",
|
|
10
11
|
});
|
|
11
12
|
async function loadMemberships(db, userIds) {
|
|
@@ -411,7 +411,7 @@ export function createUserDataRightsFeature(opts = {}) {
|
|
|
411
411
|
// Discriminator wird via handlerName="user-data-rights:run-export-
|
|
412
412
|
// jobs" im Secret-Read-Audit erfasst.
|
|
413
413
|
const exportUserId = ctx._userId ?? SYSTEM_USER_ID;
|
|
414
|
-
const exportDb = ctx.db.unsafeRaw(
|
|
414
|
+
const exportDb = ctx.db.unsafeRaw(); // @cast-boundary db-operator — jobs never run inside a DbTx
|
|
415
415
|
const exportRegistry = ctx.registry;
|
|
416
416
|
// JobContext carries ctx.meter (raw), not ctx.metrics — that bound
|
|
417
417
|
// handle only exists on HandlerContext (built per write/query call
|
|
@@ -496,7 +496,7 @@ export function createUserDataRightsFeature(opts = {}) {
|
|
|
496
496
|
}
|
|
497
497
|
const T = (await import("@cosmicdrift/kumiko-framework/time")).getTemporal();
|
|
498
498
|
const forgetUserId = ctx._userId ?? SYSTEM_USER_ID;
|
|
499
|
-
const forgetDb = ctx.db.unsafeRaw(
|
|
499
|
+
const forgetDb = ctx.db.unsafeRaw(); // @cast-boundary db-operator — jobs never run inside a DbTx
|
|
500
500
|
const forgetRegistry = ctx.registry;
|
|
501
501
|
const tenantModel = await resolveAppTenantModel({
|
|
502
502
|
registry: forgetRegistry,
|
|
@@ -27,6 +27,7 @@ export const cancelDeletionWrite = defineWriteHandler({
|
|
|
27
27
|
},
|
|
28
28
|
description: "Withdraws the calling user's own pending account-deletion request and puts the account back to active, accepted only while the grace period is still running.",
|
|
29
29
|
escapeHatch: {
|
|
30
|
+
grants: ["unsafeRaw", "globalWrites"],
|
|
30
31
|
reason: APPEND_LIFECYCLE_EVENT_REASON,
|
|
31
32
|
},
|
|
32
33
|
handler: async (event, ctx) => {
|
|
@@ -44,7 +45,7 @@ export const cancelDeletionWrite = defineWriteHandler({
|
|
|
44
45
|
if (!isWithinGracePeriod(row.gracePeriodEnd)) {
|
|
45
46
|
return writeFailure(new UnprocessableError("grace_period_expired"));
|
|
46
47
|
}
|
|
47
|
-
await updateUserLifecycle(ctx.db.unsafeRaw(
|
|
48
|
+
await updateUserLifecycle(ctx.db.unsafeRaw(), event.user.id, {
|
|
48
49
|
status: USER_STATUS.Active,
|
|
49
50
|
gracePeriodEnd: null,
|
|
50
51
|
// #354/1: closes the replay-after-cancel window — a still-TTL-valid email
|
|
@@ -68,7 +68,7 @@ export function createConfirmDeletionByTokenHandler(opts = {}) {
|
|
|
68
68
|
secret: opts.deletionTokenSecret,
|
|
69
69
|
loadPendingRequestId: (userId) => readPendingDeletionRequestId(ctx, userId),
|
|
70
70
|
commitDeletion: async (userId, requestId) => {
|
|
71
|
-
const res = await startDeletionGracePeriod(ctx, userId, () => resolveGracePeriod(ctx, event.user.tenantId), ctx.db.unsafeRaw(
|
|
71
|
+
const res = await startDeletionGracePeriod(ctx, userId, () => resolveGracePeriod(ctx, event.user.tenantId), ctx.db.unsafeRaw(), { pendingDeletionRequestId: requestId });
|
|
72
72
|
if (!res.ok)
|
|
73
73
|
return false;
|
|
74
74
|
gracePeriodEndIso = res.gracePeriodEnd.toString();
|
|
@@ -5,6 +5,7 @@ import { downloadAttemptEntity } from "../schema/download-attempt.js";
|
|
|
5
5
|
// non-convention QN; entityList needs the convention `download-attempt:list`.
|
|
6
6
|
export const downloadAttemptListQuery = defineEntityListHandler("download-attempt", downloadAttemptEntity, {
|
|
7
7
|
access: { roles: access.systemAdmin },
|
|
8
|
+
agent: { risk: "high" },
|
|
8
9
|
escapeHatch: {
|
|
9
10
|
reason: "SystemAdmin DPO triage of invalid export-download attempts needs every tenant's rows to spot brute-force patterns across tenants",
|
|
10
11
|
},
|
|
@@ -47,6 +47,7 @@ export const downloadByJobQuery = defineQueryHandler({
|
|
|
47
47
|
}, // openToAll = auth-required, kein anonymous
|
|
48
48
|
description: "Returns a short-lived signed download URL for the calling user's own finished data-export job named by job id, backing the download button in the privacy center once export-status reports the job done.",
|
|
49
49
|
escapeHatch: {
|
|
50
|
+
grants: ["unsafeRaw"],
|
|
50
51
|
reason: DOWNLOAD_BY_JOB_REASON,
|
|
51
52
|
},
|
|
52
53
|
handler: async (query, ctx) => {
|
|
@@ -60,7 +61,7 @@ export const downloadByJobQuery = defineQueryHandler({
|
|
|
60
61
|
// Wert (603/2).
|
|
61
62
|
const auditIp = requestContext.get()?.ip ?? null;
|
|
62
63
|
const auditUa = requestContext.get()?.userAgent ?? null;
|
|
63
|
-
const runner = ctx.db.unsafeRaw(
|
|
64
|
+
const runner = ctx.db.unsafeRaw();
|
|
64
65
|
// Step 1-2: job-lookup + cross-user-isolation
|
|
65
66
|
const jobRow = await fetchOne(runner, exportJobsTable, { id: jobId });
|
|
66
67
|
if (!jobRow || jobRow.userId !== userId) {
|
|
@@ -58,12 +58,13 @@ export const downloadByTokenQuery = defineQueryHandler({
|
|
|
58
58
|
// Memory `feedback_security_default_on`.
|
|
59
59
|
rateLimit: { per: "ip+handler", limit: 30, windowSeconds: 60 },
|
|
60
60
|
escapeHatch: {
|
|
61
|
+
grants: ["unsafeRaw"],
|
|
61
62
|
reason: DOWNLOAD_BY_TOKEN_REASON,
|
|
62
63
|
},
|
|
63
64
|
handler: async (query, ctx) => {
|
|
64
65
|
const T = getTemporal();
|
|
65
66
|
const now = T.Now.instant();
|
|
66
|
-
const runner = ctx.db.unsafeRaw(
|
|
67
|
+
const runner = ctx.db.unsafeRaw();
|
|
67
68
|
// Step 1: hash + lookup
|
|
68
69
|
const hash = await hashDownloadToken(query.payload.token);
|
|
69
70
|
const tokenRow = await fetchOne(runner, exportDownloadTokensTable, {
|
|
@@ -26,10 +26,11 @@ export const exportStatusQuery = defineQueryHandler({
|
|
|
26
26
|
},
|
|
27
27
|
description: "Returns the calling user's own most recent data-export job with its status, expiry and error, or hasJob false, for polling after a request-export while the job is still running.",
|
|
28
28
|
escapeHatch: {
|
|
29
|
+
grants: ["unsafeRaw"],
|
|
29
30
|
reason: EXPORT_STATUS_REASON,
|
|
30
31
|
},
|
|
31
32
|
handler: async (query, ctx) => {
|
|
32
|
-
const rows = await selectMany(ctx.db.unsafeRaw(
|
|
33
|
+
const rows = await selectMany(ctx.db.unsafeRaw(), exportJobsTable, { userId: query.user.id }, { limit: 1, orderBy: { col: "requestedAt", direction: "desc" } });
|
|
33
34
|
const latest = rows[0];
|
|
34
35
|
if (!latest)
|
|
35
36
|
return { hasJob: false };
|
|
@@ -5,8 +5,6 @@ import { USER_STATUS, userTable } from "../../user/index.js";
|
|
|
5
5
|
import { denyIfTargetOutsideAdminTenant } from "../lib/deny-if-target-outside-admin-tenant.js";
|
|
6
6
|
import { updateUserLifecycle } from "../lib/update-user-lifecycle.js";
|
|
7
7
|
const LIFT_RESTRICTION_ESCAPE_HATCH_REASON = "checks the target user's membership in the admin's tenant and appends the user lifecycle status change on the SYSTEM_TENANT_ID user stream, both via DbRunner helpers outside the admin's own tenant scope.";
|
|
8
|
-
const CHECK_TARGET_MEMBERSHIP_REASON = "checks the target user's membership in the admin's tenant via the DbRunner helper";
|
|
9
|
-
const APPEND_LIFECYCLE_EVENT_REASON = "appends the user lifecycle event on the SYSTEM_TENANT_ID user stream";
|
|
10
8
|
// POST /api/user/lift-restriction (S2.U6) — DSGVO Art. 18 Reverse.
|
|
11
9
|
//
|
|
12
10
|
// Operator-only. A Restricted user's own session is unconditionally
|
|
@@ -33,11 +31,12 @@ export const liftRestrictionWrite = defineWriteHandler({
|
|
|
33
31
|
access: { roles: access.admin },
|
|
34
32
|
description: "Lifts a GDPR Art. 18 processing restriction on the named user and returns the account to active; operator-only, because a restricted user's own session is rejected and cannot reach this endpoint.",
|
|
35
33
|
escapeHatch: {
|
|
34
|
+
grants: ["unsafeRaw", "globalWrites"],
|
|
36
35
|
reason: LIFT_RESTRICTION_ESCAPE_HATCH_REASON,
|
|
37
36
|
},
|
|
38
37
|
handler: async (event, ctx) => {
|
|
39
38
|
const targetUserId = event.payload.userId;
|
|
40
|
-
const outside = await denyIfTargetOutsideAdminTenant(ctx.db.unsafeRaw(
|
|
39
|
+
const outside = await denyIfTargetOutsideAdminTenant(ctx.db.unsafeRaw(), event.user, targetUserId);
|
|
41
40
|
if (outside)
|
|
42
41
|
return outside;
|
|
43
42
|
const userRow = await ctx.db.global(userTable).fetchOne({
|
|
@@ -54,7 +53,7 @@ export const liftRestrictionWrite = defineWriteHandler({
|
|
|
54
53
|
details: { currentStatus },
|
|
55
54
|
}));
|
|
56
55
|
}
|
|
57
|
-
await updateUserLifecycle(ctx.db.unsafeRaw(
|
|
56
|
+
await updateUserLifecycle(ctx.db.unsafeRaw(), targetUserId, {
|
|
58
57
|
status: USER_STATUS.Active,
|
|
59
58
|
});
|
|
60
59
|
return {
|
|
@@ -33,6 +33,7 @@ export const myAuditLogQuery = defineQueryHandler({
|
|
|
33
33
|
},
|
|
34
34
|
description: "Returns the calling user's own event-store entries across all their tenant memberships, paged and filterable by aggregate type, event type and time range, for the GDPR Art. 15 self-disclosure; it can never read another user's history.",
|
|
35
35
|
escapeHatch: {
|
|
36
|
+
grants: ["unsafeRaw"],
|
|
36
37
|
reason: MY_AUDIT_LOG_REASON,
|
|
37
38
|
},
|
|
38
39
|
handler: async (query, ctx) => {
|
|
@@ -52,7 +53,7 @@ export const myAuditLogQuery = defineQueryHandler({
|
|
|
52
53
|
}
|
|
53
54
|
if (p.before)
|
|
54
55
|
where["id"] = { lt: BigInt(p.before) };
|
|
55
|
-
const rows = await selectMany(ctx.db.unsafeRaw(
|
|
56
|
+
const rows = await selectMany(ctx.db.unsafeRaw(), eventsTable, where, {
|
|
56
57
|
orderBy: { col: "id", direction: "desc" },
|
|
57
58
|
limit: p.limit,
|
|
58
59
|
});
|
|
@@ -36,6 +36,7 @@ export function createRequestDeletionByEmailHandler(opts = {}) {
|
|
|
36
36
|
// Defense-in-depth gegen Email-Probing auf dem anonymen Endpoint.
|
|
37
37
|
rateLimit: { per: "ip+handler", limit: 10, windowSeconds: 60 },
|
|
38
38
|
escapeHatch: {
|
|
39
|
+
grants: ["unsafeRaw", "globalWrites"],
|
|
39
40
|
reason: APPEND_LIFECYCLE_EVENT_REASON,
|
|
40
41
|
},
|
|
41
42
|
handler: async (event, ctx) => {
|
|
@@ -56,7 +57,7 @@ export function createRequestDeletionByEmailHandler(opts = {}) {
|
|
|
56
57
|
// user-Row landet und in die Token-HMAC-Purpose gefaltet wird. cancel
|
|
57
58
|
// nullt sie → ein nach Cancel nachgespieltes Token verifiziert nicht mehr.
|
|
58
59
|
const requestId = crypto.randomUUID();
|
|
59
|
-
await updateUserLifecycle(ctx.db.unsafeRaw(
|
|
60
|
+
await updateUserLifecycle(ctx.db.unsafeRaw(), userRow["id"], {
|
|
60
61
|
pendingDeletionRequestId: requestId,
|
|
61
62
|
});
|
|
62
63
|
const { token, expiresAt } = signDeletionToken(userRow["id"], requestId, DELETION_VERIFY_TTL_MINUTES, opts.deletionTokenSecret);
|
|
@@ -29,7 +29,7 @@ export function createRequestDeletionHandler(opts = {}) {
|
|
|
29
29
|
description: "Starts the GDPR Art. 17 deletion of the calling user's own account by arming the grace period from the tenant compliance profile and mailing a confirmation, after which only cancel-deletion can stop the erasure.",
|
|
30
30
|
agent: { risk: "high" },
|
|
31
31
|
handler: async (event, ctx) => {
|
|
32
|
-
const res = await startDeletionGracePeriod(ctx, event.user.id, () => resolveGracePeriod(ctx, event.user.tenantId), ctx.db.unsafeRaw(
|
|
32
|
+
const res = await startDeletionGracePeriod(ctx, event.user.id, () => resolveGracePeriod(ctx, event.user.tenantId), ctx.db.unsafeRaw());
|
|
33
33
|
if (!res.ok)
|
|
34
34
|
return writeFailure(res.error);
|
|
35
35
|
const { gracePeriodEnd, userEmail, userLocale } = res;
|
|
@@ -51,13 +51,14 @@ export const requestExportWrite = defineWriteHandler({
|
|
|
51
51
|
},
|
|
52
52
|
description: "Queues a GDPR Art. 15 and 20 data export for the calling user and returns its job id, handing back the running job with isExisting true instead of a second one when an export is already pending.",
|
|
53
53
|
escapeHatch: {
|
|
54
|
+
grants: ["unsafeRaw"],
|
|
54
55
|
reason: REQUEST_EXPORT_REASON,
|
|
55
56
|
},
|
|
56
57
|
handler: async (event, ctx) => {
|
|
57
58
|
const userId = event.user.id;
|
|
58
59
|
const T = getTemporal();
|
|
59
60
|
const now = T.Now.instant();
|
|
60
|
-
const exportJobRunner = ctx.db.unsafeRaw(
|
|
61
|
+
const exportJobRunner = ctx.db.unsafeRaw();
|
|
61
62
|
// Pre-Check: exportJobRunner bypasses the TenantDb-Filter — the
|
|
62
63
|
// TenantDb-Wrapper would hide cross-tenant jobs.
|
|
63
64
|
const existing = await findActiveJob(exportJobRunner, userId);
|
|
@@ -59,7 +59,7 @@ export const restrictAccountWrite = defineWriteHandler({
|
|
|
59
59
|
details: { reason: "admin_required_for_other_user" },
|
|
60
60
|
}));
|
|
61
61
|
}
|
|
62
|
-
const outside = await denyIfTargetOutsideAdminTenant(ctx.db.unsafeRaw(
|
|
62
|
+
const outside = await denyIfTargetOutsideAdminTenant(ctx.db.unsafeRaw(), event.user, targetUserId);
|
|
63
63
|
if (outside)
|
|
64
64
|
return outside;
|
|
65
65
|
}
|
|
@@ -82,7 +82,7 @@ export const restrictAccountWrite = defineWriteHandler({
|
|
|
82
82
|
details: { currentStatus },
|
|
83
83
|
}));
|
|
84
84
|
}
|
|
85
|
-
await updateUserLifecycle(ctx.db.unsafeRaw(
|
|
85
|
+
await updateUserLifecycle(ctx.db.unsafeRaw(), targetUserId, { status: USER_STATUS.Restricted });
|
|
86
86
|
// Cross-Feature: alle live sessions revoken — sonst koennte der User
|
|
87
87
|
// mit existierendem JWT bis zur Token-Expiry weiter schreiben.
|
|
88
88
|
// ctx.writeAs(systemUser, ...) damit der privileged-Handler die
|
|
@@ -26,6 +26,7 @@ export function createRunForgetCleanupHandler(opts = {}) {
|
|
|
26
26
|
// records without softDelete — an agent must never be able to trigger it.
|
|
27
27
|
agent: { expose: false, risk: "high" },
|
|
28
28
|
escapeHatch: {
|
|
29
|
+
grants: ["unsafeRaw"],
|
|
29
30
|
reason: RUN_FORGET_CLEANUP_REASON,
|
|
30
31
|
},
|
|
31
32
|
handler: async (_event, ctx) => {
|
|
@@ -35,7 +36,7 @@ export function createRunForgetCleanupHandler(opts = {}) {
|
|
|
35
36
|
}));
|
|
36
37
|
}
|
|
37
38
|
const T = getTemporal();
|
|
38
|
-
const runner = ctx.db.unsafeRaw(
|
|
39
|
+
const runner = ctx.db.unsafeRaw();
|
|
39
40
|
// Operator-triggered forget must also erase binaries, not just rows —
|
|
40
41
|
// it flips users to Deleted, after which the cron never re-processes
|
|
41
42
|
// them, so a row-only delete here would permanently leak the binaries.
|
|
@@ -18,7 +18,7 @@ import { USER_STATUS, userEntity, userTable } from "../../user/index.js";
|
|
|
18
18
|
// NICHT `event.user.tenantId` (das ist der aktive Tenant zur Lifecycle-Zeit und
|
|
19
19
|
// kann abweichen).
|
|
20
20
|
const userExecutor = createEventStoreExecutor(userTable, userEntity, { entityName: "user" });
|
|
21
|
-
// `conn` is ctx.db.unsafeRaw(
|
|
21
|
+
// `conn` is ctx.db.unsafeRaw() (regular handlers) or the open tx
|
|
22
22
|
// (forget-cleanup sub-tx) — keeps the event append atomic with the write.
|
|
23
23
|
//
|
|
24
24
|
// Returns `{ applied: false }` instead of throwing only when `options.expect`
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// Forget-Cleanup-Runner (S2.U5b) — pure-Function Pipeline.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// After the grace period (S2.U5a sets gracePeriodEnd) this runner iterates
|
|
4
|
+
// over all users in DeletionRequested state and triggers the EXT_USER_DATA
|
|
5
|
+
// delete hooks in every tenant the user is or was a member of (membership
|
|
6
|
+
// history from kumiko_events).
|
|
6
7
|
//
|
|
7
|
-
// **Cross-
|
|
8
|
-
//
|
|
8
|
+
// **Cross-tenant iteration:** a forget request in tenant A must remove the
|
|
9
|
+
// user's data in ALL of their tenants, including tenants they left before
|
|
10
|
+
// the request — see
|
|
9
11
|
// docs/plans/architecture/user-data-rights.md "Cross-Tenant-Semantik".
|
|
10
12
|
//
|
|
11
13
|
// **Strategy-Dispatch:** Pro Entity entscheidet die data-retention-
|
|
@@ -177,15 +179,16 @@ async function processUser(args) {
|
|
|
177
179
|
// memberships in der Tx loeschen. Orphan-User (0 memberships) liefert
|
|
178
180
|
// [] in Email-args; App-Author-Template kann das case-handlen.
|
|
179
181
|
const tenantIdsBeforeDelete = memberships.map((m) => m.tenantId);
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
|
|
182
|
+
// Departed tenants: removeMember deletes only the projection row, so data
|
|
183
|
+
// the user left behind there (e.g. note mentions) must still be reached.
|
|
184
|
+
const historicalTenantIds = await readHistoricalMembershipTenantIds(db, userId);
|
|
185
|
+
const hookTenantIds = [...new Set([...tenantIdsBeforeDelete, ...historicalTenantIds])];
|
|
186
|
+
// Edge case "0 tenants" (neither membership nor history): the hook loop still
|
|
187
|
+
// runs with a pseudo tenant. The user hook is tenant-agnostic and MUST run so
|
|
188
|
+
// email/displayName/passwordHash get anonymized; tenant-scoped hooks find
|
|
189
|
+
// nothing there and no-op. Without this path status=Deleted would be set
|
|
190
|
+
// while the original PII stays — compliant-looking, but not compliant.
|
|
191
|
+
const tenantList = hookTenantIds.length > 0 ? hookTenantIds : [SYSTEM_TENANT_ID_FOR_ORPHANS];
|
|
189
192
|
// Per-User-Sub-Tx: hooks + status-flip atomar. Bei Hook-Throw rollt
|
|
190
193
|
// nur dieser User zurueck, andere User bleiben commit-fest. Die Sub-Tx
|
|
191
194
|
// nestet korrekt: eine Top-Level-Connection oeffnet sie via `.begin`
|
|
@@ -342,7 +345,7 @@ async function resolveEffectiveTenantModel(db, tenantId, appTenantModel) {
|
|
|
342
345
|
// rows.
|
|
343
346
|
//
|
|
344
347
|
// Filter by payload->>'tenantId' (not the event column tenant_id): cross-
|
|
345
|
-
// tenant SystemAdmin adds land under the actor's tenant_id
|
|
348
|
+
// tenant SystemAdmin adds land under the actor's tenant_id.
|
|
346
349
|
//
|
|
347
350
|
// Counting created-ROWS against distinct userIds latched a tenant to
|
|
348
351
|
// multi-user as soon as its sole member was removed and re-added (#2608) —
|
|
@@ -369,6 +372,18 @@ async function everHadMultipleMembers(db, tenantId) {
|
|
|
369
372
|
// could name a second member.
|
|
370
373
|
return row.event_count > row.readable_users;
|
|
371
374
|
}
|
|
375
|
+
// Filter by payload (not the event column tenant_id) for the same reason as
|
|
376
|
+
// everHadMultipleMembers: cross-tenant SystemAdmin adds are billed to the
|
|
377
|
+
// actor's tenant.
|
|
378
|
+
async function readHistoricalMembershipTenantIds(db, userId) {
|
|
379
|
+
const rows = await executeRawQuery(db, `SELECT DISTINCT payload->>'tenantId' AS tenant_id
|
|
380
|
+
FROM kumiko_events
|
|
381
|
+
WHERE aggregate_type = 'tenant-membership'
|
|
382
|
+
AND type = $1
|
|
383
|
+
AND payload->>'userId' = $2
|
|
384
|
+
AND NULLIF(payload->>'tenantId', '') IS NOT NULL`, [entityEventName("tenant-membership", "created"), userId]);
|
|
385
|
+
return rows.map((row) => row.tenant_id); // @cast-boundary db-row
|
|
386
|
+
}
|
|
372
387
|
// Mapping retention.strategy → user-data-rights.UserDataDeleteStrategy.
|
|
373
388
|
// - "anonymize" / "blockDelete" → "anonymize" (Aufbewahrungs-Pflicht
|
|
374
389
|
// blockDelete: Daten muessen physisch bleiben, nur Personen-Bezug raus)
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// This subscriber never resumes a run itself (D3, workflow-resume-loop.md):
|
|
21
21
|
// the MultiStreamApplyContext it runs in has no callFeature/runStepList
|
|
22
22
|
// access, only unsafeAppendEvent/loadAggregate. It writes triggerEventType
|
|
23
|
-
// +
|
|
23
|
+
// + triggerEventRef + wakeAt=now() onto the matching pending row(s); the
|
|
24
24
|
// existing resume-due-runs job (Phase 2) picks the row up on its next
|
|
25
25
|
// cron tick — the one resume path stays exactly the one Phase 2 built.
|
|
26
26
|
import { selectMany, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
@@ -66,7 +66,11 @@ const wakeupApply = async (event, tx) => {
|
|
|
66
66
|
continue;
|
|
67
67
|
await updateMany(tx, workflowRunPendingTable, {
|
|
68
68
|
triggerEventType: event.type,
|
|
69
|
-
|
|
69
|
+
triggerEventRef: {
|
|
70
|
+
eventId: event.id,
|
|
71
|
+
aggregateId: event.aggregateId,
|
|
72
|
+
version: event.version,
|
|
73
|
+
},
|
|
70
74
|
wakeAt: getTemporal().Now.instant().toString(),
|
|
71
75
|
}, { tenantId: row.tenantId, runId: row.runId, stepIndex: row.stepIndex });
|
|
72
76
|
}
|
|
@@ -69,6 +69,11 @@ export function registerEventTrigger(r, workflow) {
|
|
|
69
69
|
runId,
|
|
70
70
|
workflow,
|
|
71
71
|
triggerEvent,
|
|
72
|
+
triggerEventRef: {
|
|
73
|
+
eventId: event.id,
|
|
74
|
+
aggregateId: event.aggregateId,
|
|
75
|
+
version: event.version,
|
|
76
|
+
},
|
|
72
77
|
...(idempotencyKey && { idempotencyKey }),
|
|
73
78
|
// @cast-boundary msp-to-handler-ctx — MultiStreamApplyContext only
|
|
74
79
|
// exposes unsafeAppendEvent/loadAggregate, a subset of
|