@cosmicdrift/kumiko-samples 0.350.0 → 0.352.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/bundled-features/package.json +2 -2
- package/packages/bundled-features/src/audit/handlers/list.query.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/email-templates.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept.write.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/reset-token.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/unlock-token.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/verification-token.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm.write.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/constants.ts +3 -17
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/feature.ts +2 -9
- package/packages/bundled-features/src/billing-foundation/handlers/sync-subscription.write.ts +3 -11
- package/packages/bundled-features/src/cap-counter/book-cap-usage.ts +96 -55
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +247 -0
- package/packages/bundled-features/src/cap-counter/changes.json +28 -0
- package/packages/bundled-features/src/cap-counter/constants.ts +8 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +1 -9
- package/packages/bundled-features/src/cap-counter/feature.ts +7 -0
- package/packages/bundled-features/src/cap-counter/handlers/increment-rolling.write.ts +5 -0
- package/packages/bundled-features/src/cap-counter/index.ts +9 -0
- package/packages/bundled-features/src/cap-counter/payload-field.ts +10 -0
- package/packages/bundled-features/src/cap-counter/reservation-chain.ts +51 -0
- package/packages/bundled-features/src/cap-counter/stock-cap-guard.ts +3 -0
- package/packages/bundled-features/src/cap-counter/tables.ts +53 -0
- package/packages/bundled-features/src/cap-counter/value-cap-guard.ts +70 -0
- package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +63 -70
- package/packages/bundled-features/src/channel-discord/feature.ts +1 -0
- package/packages/bundled-features/src/channel-slack/feature.ts +1 -0
- package/packages/bundled-features/src/channel-teams/feature.ts +1 -0
- package/packages/bundled-features/src/delivery/attempt-log-retention.ts +40 -0
- package/packages/bundled-features/src/delivery/attempt-log.ts +26 -3
- package/packages/bundled-features/src/delivery/changes.json +12 -0
- package/packages/bundled-features/src/delivery/constants.ts +1 -0
- package/packages/bundled-features/src/delivery/events.ts +3 -0
- package/packages/bundled-features/src/delivery/feature.ts +50 -2
- package/packages/bundled-features/src/delivery/handlers/log.query.ts +1 -1
- package/packages/bundled-features/src/delivery/index.ts +6 -0
- package/packages/bundled-features/src/delivery/mask-recipient-address.ts +40 -0
- package/packages/bundled-features/src/delivery/public-names.ts +5 -0
- package/packages/bundled-features/src/delivery/types.ts +12 -2
- package/packages/bundled-features/src/derivatives-sharp/changes.json +6 -0
- package/packages/bundled-features/src/feature-toggles/handlers/set.write.ts +1 -1
- package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +1 -1
- package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +1 -1
- package/packages/bundled-features/src/form-draft/lookup.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +1 -1
- package/packages/bundled-features/src/inbound-provider-imap/feature.ts +1 -1
- package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +1 -1
- package/packages/bundled-features/src/jobs/job-instant.ts +1 -1
- package/packages/bundled-features/src/legal-pages/README.md +3 -3
- package/packages/bundled-features/src/legal-pages/changes.json +6 -0
- package/packages/bundled-features/src/metrics/compute.ts +1 -1
- package/packages/bundled-features/src/metrics/feature.ts +1 -1
- package/packages/bundled-features/src/metrics/types.ts +1 -1
- package/packages/bundled-features/src/metrics/window.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/expiry.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/handlers/create.write.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/handlers/revoke.write.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +1 -1
- package/packages/bundled-features/src/renderer-foundation/types.ts +1 -1
- package/packages/bundled-features/src/renderer-simple/changes.json +6 -0
- package/packages/bundled-features/src/renderer-simple/feature.ts +4 -6
- package/packages/bundled-features/src/renderer-simple/index.ts +1 -0
- package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +138 -0
- package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +33 -4
- package/packages/bundled-features/src/sessions/handlers/revoke-all-for-user.write.ts +1 -1
- package/packages/bundled-features/src/sessions/handlers/revoke-all-others.write.ts +1 -1
- package/packages/bundled-features/src/sessions/handlers/revoke.write.ts +1 -1
- package/packages/bundled-features/src/sessions/session-callbacks.ts +1 -1
- package/packages/bundled-features/src/shared/grace-period.ts +3 -10
- package/packages/bundled-features/src/shared/row-bound-grant.ts +1 -1
- package/packages/bundled-features/src/shared/signed-token.ts +1 -1
- package/packages/bundled-features/src/subscription-mollie/verify-webhook.ts +1 -1
- package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +1 -1
- package/packages/bundled-features/src/template-resolver/api.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/by-tenant.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/collection-shared.ts +1 -1
- package/packages/bundled-features/src/template-resolver/table.ts +1 -1
- package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +1 -1
- package/packages/bundled-features/src/tenant/invited-membership.ts +1 -1
- package/packages/bundled-features/src/tenant-handover/grant.ts +1 -1
- package/packages/bundled-features/src/tier-engine/changes.json +6 -0
- package/packages/bundled-features/src/tier-engine/index.ts +7 -0
- package/packages/bundled-features/src/tier-engine/tier-option-gate.ts +148 -0
- package/packages/bundled-features/src/user-data-rights/db/queries/export-jobs.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +3 -9
- package/packages/bundled-features/src/user-data-rights/deletion-token.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/email-templates.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/list-download-attempts.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/my-audit-log.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights-defaults/hooks/delivery-attempt.userdata-hook.ts +36 -6
- package/packages/bundled-features/src/waitlist/handlers/invite.write.ts +1 -1
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +1 -1
- package/samples/apps/marketing-demo/package.json +1 -0
- package/samples/apps/marketing-demo/src/app/seed-data.ts +2 -2
- package/samples/apps/marketing-demo/src/app/seed.ts +1 -1
- package/samples/apps/styleguide/src/app/shell.tsx +1 -1
- package/samples/apps/styleguide/src/features/gallery/web/Gallery.tsx +9 -3
- package/samples/apps/ui-walkthrough/src/app/assistant-action.tsx +6 -5
- package/samples/apps/ui-walkthrough/src/app/shell.tsx +3 -2
- package/samples/apps/ui-walkthrough/src/features/tasks/i18n.ts +8 -0
- package/samples/apps/use-all-bundled/package.json +1 -0
- package/samples/apps/use-all-bundled/src/app/seed.ts +1 -1
- package/samples/recipes/files-post-processing/package.json +1 -1
- package/samples/recipes/seed-migration/README.md +13 -1
|
@@ -29,6 +29,13 @@ export {
|
|
|
29
29
|
createTierEngineFeature,
|
|
30
30
|
tierEngineFeature,
|
|
31
31
|
} from "./feature.js";
|
|
32
|
+
export {
|
|
33
|
+
createTierOptionGate,
|
|
34
|
+
type TierOptionAvailability,
|
|
35
|
+
type TierOptionGate,
|
|
36
|
+
type TierOptionGateDeps,
|
|
37
|
+
type TierOptionSpec,
|
|
38
|
+
} from "./tier-option-gate.js";
|
|
32
39
|
export {
|
|
33
40
|
createTierResolver,
|
|
34
41
|
type TierResolver,
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Tier-gated options of a select field: one spec drives the availability query the
|
|
2
|
+
// renderer shows (disabled option + "from <tier>" hint) and the write-path check that
|
|
3
|
+
// actually enforces it, so the two cannot drift apart.
|
|
4
|
+
|
|
5
|
+
import type { EntityTableMeta, SchemaTable, TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
6
|
+
import type { WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
+
import {
|
|
8
|
+
UnprocessableError,
|
|
9
|
+
type WriteFailure,
|
|
10
|
+
writeFailure,
|
|
11
|
+
} from "@cosmicdrift/kumiko-framework/errors";
|
|
12
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-framework/utils";
|
|
13
|
+
import {
|
|
14
|
+
type CapGuardContext,
|
|
15
|
+
type CapLimitContext,
|
|
16
|
+
readPayloadField,
|
|
17
|
+
} from "../cap-counter/index.js";
|
|
18
|
+
|
|
19
|
+
export type TierOptionSpec<TCaps> = {
|
|
20
|
+
readonly field: string;
|
|
21
|
+
readonly options: readonly string[];
|
|
22
|
+
readonly isOptionAllowed: (option: string, caps: TCaps) => boolean;
|
|
23
|
+
readonly code: string;
|
|
24
|
+
readonly i18nKey: string;
|
|
25
|
+
/** Lets an update that resends the unchanged stored value of a no longer allowed
|
|
26
|
+
* option pass (edit forms submit the whole row). Without it the stored value is not looked up. */
|
|
27
|
+
readonly table?: SchemaTable | EntityTableMeta;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type TierOptionAvailability = {
|
|
31
|
+
readonly value: string;
|
|
32
|
+
readonly disabled: boolean;
|
|
33
|
+
readonly hint?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type TierOptionGateDeps<TTier extends string, TCaps> = {
|
|
37
|
+
/** Ascending: the first tier that allows an option is the one named as required. */
|
|
38
|
+
readonly tierOrder: readonly TTier[];
|
|
39
|
+
readonly capsForTier: (tier: TTier, context: CapLimitContext) => TCaps | Promise<TCaps>;
|
|
40
|
+
readonly resolveTier: (db: TenantDb) => Promise<TTier>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type TierOptionGate<TTier extends string, TCaps> = {
|
|
44
|
+
readonly optionAvailability: (
|
|
45
|
+
db: TenantDb,
|
|
46
|
+
context: CapLimitContext,
|
|
47
|
+
spec: Pick<TierOptionSpec<TCaps>, "options" | "isOptionAllowed">,
|
|
48
|
+
hintFor: (requiredTier: TTier) => string,
|
|
49
|
+
) => Promise<readonly TierOptionAvailability[]>;
|
|
50
|
+
/** `recordId` is the id of the edited row; with `spec.table` it enables the unchanged-value pass. */
|
|
51
|
+
readonly checkTierOption: (
|
|
52
|
+
ctx: CapGuardContext,
|
|
53
|
+
value: unknown,
|
|
54
|
+
spec: TierOptionSpec<TCaps>,
|
|
55
|
+
recordId?: string,
|
|
56
|
+
) => Promise<WriteFailure | null>;
|
|
57
|
+
readonly withTierOptionGate: (
|
|
58
|
+
handler: WriteHandlerDef,
|
|
59
|
+
spec: TierOptionSpec<TCaps>,
|
|
60
|
+
) => WriteHandlerDef;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function limitContextOf(ctx: CapGuardContext): CapLimitContext {
|
|
64
|
+
return ctx.config ? { config: ctx.config } : {};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function recordIdOf(payload: unknown): string | undefined {
|
|
68
|
+
return isPlainObject(payload) && typeof payload["id"] === "string" ? payload["id"] : undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function createTierOptionGate<TTier extends string, TCaps>(
|
|
72
|
+
deps: TierOptionGateDeps<TTier, TCaps>,
|
|
73
|
+
): TierOptionGate<TTier, TCaps> {
|
|
74
|
+
async function capsPerTier(
|
|
75
|
+
context: CapLimitContext,
|
|
76
|
+
): Promise<readonly { readonly tier: TTier; readonly caps: TCaps }[]> {
|
|
77
|
+
return Promise.all(
|
|
78
|
+
deps.tierOrder.map(async (tier) => ({ tier, caps: await deps.capsForTier(tier, context) })),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function optionAvailability(
|
|
83
|
+
db: TenantDb,
|
|
84
|
+
context: CapLimitContext,
|
|
85
|
+
spec: Pick<TierOptionSpec<TCaps>, "options" | "isOptionAllowed">,
|
|
86
|
+
hintFor: (requiredTier: TTier) => string,
|
|
87
|
+
): Promise<readonly TierOptionAvailability[]> {
|
|
88
|
+
const currentCaps = await deps.capsForTier(await deps.resolveTier(db), context);
|
|
89
|
+
const tiers = await capsPerTier(context);
|
|
90
|
+
return spec.options.map((value) => {
|
|
91
|
+
if (spec.isOptionAllowed(value, currentCaps)) return { value, disabled: false };
|
|
92
|
+
const required = tiers.find(({ caps }) => spec.isOptionAllowed(value, caps));
|
|
93
|
+
return required === undefined
|
|
94
|
+
? { value, disabled: true }
|
|
95
|
+
: { value, disabled: true, hint: hintFor(required.tier) };
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function storedValueMatches(
|
|
100
|
+
ctx: CapGuardContext,
|
|
101
|
+
spec: TierOptionSpec<TCaps>,
|
|
102
|
+
value: string,
|
|
103
|
+
recordId: string | undefined,
|
|
104
|
+
): Promise<boolean> {
|
|
105
|
+
if (spec.table === undefined || recordId === undefined) return false;
|
|
106
|
+
const stored = await ctx.db.fetchOne<Record<string, unknown>>(spec.table, { id: recordId });
|
|
107
|
+
return stored?.[spec.field] === value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function checkTierOption(
|
|
111
|
+
ctx: CapGuardContext,
|
|
112
|
+
value: unknown,
|
|
113
|
+
spec: TierOptionSpec<TCaps>,
|
|
114
|
+
recordId?: string,
|
|
115
|
+
): Promise<WriteFailure | null> {
|
|
116
|
+
// skip: an absent or non-string value is the schema's business, not a tier violation.
|
|
117
|
+
if (typeof value !== "string") return null;
|
|
118
|
+
const context = limitContextOf(ctx);
|
|
119
|
+
const currentCaps = await deps.capsForTier(await deps.resolveTier(ctx.db), context);
|
|
120
|
+
if (spec.isOptionAllowed(value, currentCaps)) return null;
|
|
121
|
+
if (await storedValueMatches(ctx, spec, value, recordId)) return null;
|
|
122
|
+
const required = (await capsPerTier(context)).find(({ caps }) =>
|
|
123
|
+
spec.isOptionAllowed(value, caps),
|
|
124
|
+
);
|
|
125
|
+
return writeFailure(
|
|
126
|
+
new UnprocessableError(spec.code, {
|
|
127
|
+
i18nKey: spec.i18nKey,
|
|
128
|
+
details: { field: spec.field, value, requiredTier: required?.tier },
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function withTierOptionGate(
|
|
134
|
+
handler: WriteHandlerDef,
|
|
135
|
+
spec: TierOptionSpec<TCaps>,
|
|
136
|
+
): WriteHandlerDef {
|
|
137
|
+
return {
|
|
138
|
+
...handler,
|
|
139
|
+
handler: async (event, ctx) => {
|
|
140
|
+
const value = readPayloadField(event.payload, spec.field);
|
|
141
|
+
const failure = await checkTierOption(ctx, value, spec, recordIdOf(event.payload));
|
|
142
|
+
return failure ?? handler.handler(event, ctx);
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { optionAvailability, checkTierOption, withTierOptionGate };
|
|
148
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
-
import type { Temporal } from "temporal
|
|
4
|
+
import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
5
5
|
import { exportJobsTable } from "../../schema/export-job.js";
|
|
6
6
|
|
|
7
7
|
export type ExportJobCleanupCandidate = {
|
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
|
|
4
|
-
// `Temporal` TYPE that gracePeriodEndCutoff's parameter type resolves
|
|
5
|
-
// against (same #1438 dual-package-hazard pattern as event-store.ts).
|
|
6
|
-
import { Temporal as TemporalPolyfill } from "temporal-polyfill";
|
|
3
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
7
4
|
import { userTable } from "../../../user/index.js";
|
|
8
5
|
|
|
9
6
|
export async function selectUsersDueForForgetCleanup(
|
|
10
7
|
db: DbConnection,
|
|
11
8
|
status: string,
|
|
12
|
-
gracePeriodEndCutoff:
|
|
9
|
+
gracePeriodEndCutoff: Temporal.Instant | string,
|
|
13
10
|
): Promise<readonly { id: string }[]> {
|
|
14
|
-
// @cast-boundary temporal-polyfill-vs-ambient: same TC39 Temporal.Instant
|
|
15
|
-
// at runtime, distinct nominal types across the two .d.ts sources (see
|
|
16
|
-
// event-store.ts). Only ever consumed via the `lte` comparison below.
|
|
17
11
|
const cutoff =
|
|
18
12
|
typeof gracePeriodEndCutoff === "string"
|
|
19
|
-
?
|
|
13
|
+
? Temporal.Instant.from(gracePeriodEndCutoff)
|
|
20
14
|
: gracePeriodEndCutoff;
|
|
21
15
|
return selectMany<{ id: string }>(db, userTable, {
|
|
22
16
|
status,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// The purpose string and the anchoring are unchanged from the hand-rolled
|
|
8
8
|
// version this replaced, so tokens stay byte-compatible.
|
|
9
9
|
|
|
10
|
-
import type { Temporal } from "temporal
|
|
10
|
+
import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
11
11
|
import {
|
|
12
12
|
type RowBoundGrantResult,
|
|
13
13
|
redeemRowBoundGrant,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { mailT, registerMailTranslations } from "@cosmicdrift/kumiko-framework/i18n";
|
|
7
7
|
import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
|
|
8
|
-
import { Temporal } from "temporal
|
|
8
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
9
9
|
|
|
10
10
|
export type GdprMailLocale = string;
|
|
11
11
|
|
package/packages/bundled-features/src/user-data-rights/handlers/list-download-attempts.query.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { selectMany, type WhereObject } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
-
import { Temporal } from "temporal
|
|
3
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
4
4
|
import * as z from "zod";
|
|
5
5
|
import { downloadAttemptsTable } from "../schema/download-attempt.js";
|
|
6
6
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { selectMany, type WhereObject } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
4
|
-
import { Temporal } from "temporal
|
|
4
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
5
5
|
import * as z from "zod";
|
|
6
6
|
|
|
7
7
|
// DSGVO Art. 15 Selbstauskunft — User reads HIS OWN audit-log.
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { UserDataDeleteHook, UserDataExportHook } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
appendEventInTenantDb,
|
|
4
|
+
getStreamVersionInTenantDb,
|
|
5
|
+
} from "@cosmicdrift/kumiko-framework/event-store";
|
|
6
|
+
import {
|
|
7
|
+
DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT,
|
|
8
|
+
deliveryAttemptsTable,
|
|
9
|
+
} from "../../delivery/index.js";
|
|
3
10
|
import { featureMounted } from "./feature-mounted.js";
|
|
4
11
|
|
|
5
12
|
// userData-Hooks for delivery's attempt log (deferred from #797, closed by
|
|
@@ -28,9 +35,32 @@ export const deliveryAttemptExportHook: UserDataExportHook = async (ctx) => {
|
|
|
28
35
|
};
|
|
29
36
|
};
|
|
30
37
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
//
|
|
38
|
+
// Both strategies erase the address. The DEK shredding of a forget covers KMS deployments; the
|
|
39
|
+
// erase event also covers plaintext mode, and unlike a read-side UPDATE it survives a projection
|
|
40
|
+
// rebuild. Rows without an address are skipped, which makes a second run a no-op.
|
|
41
|
+
export const deliveryAttemptDeleteHook: UserDataDeleteHook = async (ctx) => {
|
|
42
|
+
// skip: delivery not mounted — its table doesn't exist, nothing to erase.
|
|
43
|
+
if (!featureMounted(ctx, "delivery")) return;
|
|
44
|
+
const rows = await ctx.db.selectMany<{ id: string; recipientAddress: string | null }>(
|
|
45
|
+
deliveryAttemptsTable,
|
|
46
|
+
{
|
|
47
|
+
tenantId: ctx.tenantId,
|
|
48
|
+
recipientId: ctx.userId,
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
for (const row of rows) {
|
|
52
|
+
if (row.recipientAddress === null) continue;
|
|
53
|
+
await appendEventInTenantDb(
|
|
54
|
+
ctx.db,
|
|
55
|
+
{
|
|
56
|
+
aggregateType: "deliveryAttempt",
|
|
57
|
+
aggregateId: row.id,
|
|
58
|
+
expectedVersion: await getStreamVersionInTenantDb(ctx.db, row.id),
|
|
59
|
+
type: DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT,
|
|
60
|
+
payload: {},
|
|
61
|
+
metadata: { userId: "system" },
|
|
62
|
+
},
|
|
63
|
+
{ registry: ctx.registry },
|
|
64
|
+
);
|
|
65
|
+
}
|
|
36
66
|
};
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
type WriteFailure,
|
|
14
14
|
writeFailure,
|
|
15
15
|
} from "@cosmicdrift/kumiko-framework/errors";
|
|
16
|
-
import { Temporal } from "temporal
|
|
16
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
17
17
|
import { AuthErrors, AuthHandlers } from "../../auth-email-password/index.js";
|
|
18
18
|
import { TenantHandlers, tenantTable } from "../../tenant/index.js";
|
|
19
19
|
import { DEFAULT_OWN_TENANT_INVITE_ROLE, WAITLIST_STATUS, WaitlistErrors } from "../constants.js";
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type WriteResult,
|
|
7
7
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
8
8
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
9
|
-
import { Temporal } from "temporal
|
|
9
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
10
10
|
import { WAITLIST_NOTIFICATION_TYPES, WAITLIST_STATUS } from "../constants.js";
|
|
11
11
|
import { normalizeEmail, platformActor, waitlistDb, waitlistExecutor } from "../lib.js";
|
|
12
12
|
import {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"@cosmicdrift/kumiko-framework": "workspace:*",
|
|
16
16
|
"@cosmicdrift/kumiko-headless": "workspace:*",
|
|
17
17
|
"@cosmicdrift/kumiko-renderer-web": "workspace:*",
|
|
18
|
+
"@cosmicdrift/kumiko-types": "workspace:*",
|
|
18
19
|
"react": "^19.2.6",
|
|
19
20
|
"react-dom": "^19.2.6",
|
|
20
21
|
"@cosmicdrift/kumiko-locale-de": "workspace:*"
|
|
@@ -306,7 +306,7 @@ export const ASSET_TEMPLATES: ReadonlyArray<AssetTemplate> = [
|
|
|
306
306
|
// treffen ohne Sonderfall. Wird nur für status==="lent" konsultiert.
|
|
307
307
|
export const OWNERS: ReadonlyArray<string> = [
|
|
308
308
|
"",
|
|
309
|
-
"
|
|
309
|
+
"Alex Morgan",
|
|
310
310
|
"Anna Weber",
|
|
311
311
|
"Lars Bergmann",
|
|
312
312
|
"Sina Klein",
|
|
@@ -435,7 +435,7 @@ export const REPORTERS: ReadonlyArray<string> = [
|
|
|
435
435
|
// unzugewiesen damit die Liste nicht zu uniform aussieht.
|
|
436
436
|
export const ASSIGNEES: ReadonlyArray<string> = [
|
|
437
437
|
"",
|
|
438
|
-
"
|
|
438
|
+
"Alex Morgan",
|
|
439
439
|
"IT-Team",
|
|
440
440
|
"Sysadmin",
|
|
441
441
|
"Helpdesk-L1",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { SeedFn } from "@cosmicdrift/kumiko-dev-server";
|
|
12
12
|
import { TestUsers } from "@cosmicdrift/kumiko-framework/stack";
|
|
13
|
-
import { Temporal } from "temporal
|
|
13
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
14
14
|
import { countAssetsForTenant, countTicketsForTenant } from "../db/queries/seed-counts";
|
|
15
15
|
import { ASSET_STATUSES } from "../features/assets/schema";
|
|
16
16
|
import { TICKET_SEVERITIES, TICKET_STATUSES } from "../features/helpdesk/schema";
|
|
@@ -58,7 +58,7 @@ export function AppShell({
|
|
|
58
58
|
],
|
|
59
59
|
])
|
|
60
60
|
}
|
|
61
|
-
sidebarFooter={<SidebarUser name="
|
|
61
|
+
sidebarFooter={<SidebarUser name="Alex Morgan" email="alex@example.com" />}
|
|
62
62
|
>
|
|
63
63
|
{activeTarget !== undefined ? <EditorPanel resolvers={resolvers} /> : children}
|
|
64
64
|
</DefaultAppShell>
|
|
@@ -606,20 +606,26 @@ export function Gallery(): ReactNode {
|
|
|
606
606
|
<div className="flex flex-col gap-5 px-6 py-6">
|
|
607
607
|
<div className="flex items-center gap-4">
|
|
608
608
|
<div className="bg-muted text-foreground flex size-16 items-center justify-center rounded-full text-lg font-semibold">
|
|
609
|
-
|
|
609
|
+
AM
|
|
610
610
|
</div>
|
|
611
611
|
<Button variant="secondary">Change avatar</Button>
|
|
612
612
|
</div>
|
|
613
613
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
614
614
|
<Field id="pf-name" label="Full name">
|
|
615
|
-
<Input
|
|
615
|
+
<Input
|
|
616
|
+
kind="text"
|
|
617
|
+
id="pf-name"
|
|
618
|
+
name="pf-name"
|
|
619
|
+
value="Alex Morgan"
|
|
620
|
+
onChange={noop}
|
|
621
|
+
/>
|
|
616
622
|
</Field>
|
|
617
623
|
<Field id="pf-email" label="Email">
|
|
618
624
|
<Input
|
|
619
625
|
kind="text"
|
|
620
626
|
id="pf-email"
|
|
621
627
|
name="pf-email"
|
|
622
|
-
value="
|
|
628
|
+
value="alex@example.com"
|
|
623
629
|
onChange={noop}
|
|
624
630
|
/>
|
|
625
631
|
</Field>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { usePrimitives } from "@cosmicdrift/kumiko-renderer";
|
|
1
|
+
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
2
2
|
import { WandSparkles } from "lucide-react";
|
|
3
3
|
import { type ReactNode, useEffect, useState } from "react";
|
|
4
4
|
|
|
@@ -6,6 +6,7 @@ const noop = (): void => {};
|
|
|
6
6
|
|
|
7
7
|
export function AssistantAction(): ReactNode {
|
|
8
8
|
const { Dialog } = usePrimitives();
|
|
9
|
+
const t = useTranslation();
|
|
9
10
|
const [open, setOpen] = useState(false);
|
|
10
11
|
|
|
11
12
|
// Registered here on purpose: an overflow menu that unmounts this component
|
|
@@ -30,7 +31,7 @@ export function AssistantAction(): ReactNode {
|
|
|
30
31
|
className="inline-flex h-9 items-center gap-2 rounded-md border border-border bg-card px-3 text-sm font-medium text-foreground hover:bg-accent"
|
|
31
32
|
>
|
|
32
33
|
<WandSparkles className="h-4 w-4" aria-hidden="true" />
|
|
33
|
-
|
|
34
|
+
{t("tasks.assistant.label")}
|
|
34
35
|
<kbd className="rounded border border-border px-1 font-sans text-xs text-muted-foreground">
|
|
35
36
|
⌘K
|
|
36
37
|
</kbd>
|
|
@@ -38,9 +39,9 @@ export function AssistantAction(): ReactNode {
|
|
|
38
39
|
<Dialog
|
|
39
40
|
open={open}
|
|
40
41
|
onOpenChange={setOpen}
|
|
41
|
-
title="
|
|
42
|
-
description="
|
|
43
|
-
confirmLabel="
|
|
42
|
+
title={t("tasks.assistant.label")}
|
|
43
|
+
description={t("tasks.assistant.description")}
|
|
44
|
+
confirmLabel={t("tasks.assistant.close")}
|
|
44
45
|
onConfirm={noop}
|
|
45
46
|
testId="assistant-dialog"
|
|
46
47
|
/>
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
SidebarBrand,
|
|
14
14
|
ThemeMenuItem,
|
|
15
15
|
ThemeToggle,
|
|
16
|
+
useTranslation,
|
|
16
17
|
} from "@cosmicdrift/kumiko-renderer-web";
|
|
17
18
|
import { MoonStar, Sun } from "lucide-react";
|
|
18
19
|
import type { ReactNode } from "react";
|
|
@@ -20,7 +21,6 @@ import { AssistantAction } from "./assistant-action";
|
|
|
20
21
|
import { BETA_TENANT_ID, DEV_TENANT_ID } from "./auth-constants";
|
|
21
22
|
|
|
22
23
|
const APP_NAME = "Kumiko Walkthrough";
|
|
23
|
-
const APP_TAGLINE = "Design-Referenz";
|
|
24
24
|
|
|
25
25
|
const tenantName = (tenantId: string): string => {
|
|
26
26
|
if (tenantId === DEV_TENANT_ID) return "Dev Tenant";
|
|
@@ -40,9 +40,10 @@ export function AppShell({
|
|
|
40
40
|
readonly children: ReactNode;
|
|
41
41
|
readonly schema: AppSchema;
|
|
42
42
|
}): ReactNode {
|
|
43
|
+
const t = useTranslation();
|
|
43
44
|
return (
|
|
44
45
|
<DefaultAppShell
|
|
45
|
-
brand={<SidebarBrand name={APP_NAME} plan={
|
|
46
|
+
brand={<SidebarBrand name={APP_NAME} plan={t("tasks.shell.tagline")} />}
|
|
46
47
|
schema={schema}
|
|
47
48
|
headerActions={
|
|
48
49
|
<>
|
|
@@ -5,6 +5,10 @@ import type { TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
|
|
|
5
5
|
|
|
6
6
|
export const tasksTranslations: TranslationsByLocale = {
|
|
7
7
|
de: {
|
|
8
|
+
"tasks.shell.tagline": "Design-Referenz",
|
|
9
|
+
"tasks.assistant.label": "Assistent",
|
|
10
|
+
"tasks.assistant.description": "Der Assistent ist im Walkthrough nicht angebunden.",
|
|
11
|
+
"tasks.assistant.close": "Schließen",
|
|
8
12
|
"tasks.nav.group": "Allgemein",
|
|
9
13
|
"tasks.nav.list": "Aufgaben",
|
|
10
14
|
"tasks.nav.new": "Neue Aufgabe",
|
|
@@ -21,6 +25,10 @@ export const tasksTranslations: TranslationsByLocale = {
|
|
|
21
25
|
"tasks:entity:task:field:notes": "Notizen",
|
|
22
26
|
},
|
|
23
27
|
en: {
|
|
28
|
+
"tasks.shell.tagline": "Design reference",
|
|
29
|
+
"tasks.assistant.label": "Assistant",
|
|
30
|
+
"tasks.assistant.description": "The assistant is not connected in the walkthrough.",
|
|
31
|
+
"tasks.assistant.close": "Close",
|
|
24
32
|
"tasks.nav.group": "General",
|
|
25
33
|
"tasks.nav.list": "Tasks",
|
|
26
34
|
"tasks.nav.new": "New task",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"@cosmicdrift/kumiko-headless": "workspace:*",
|
|
23
23
|
"@cosmicdrift/kumiko-renderer-web": "workspace:*",
|
|
24
24
|
"@cosmicdrift/kumiko-server-runtime": "workspace:*",
|
|
25
|
+
"@cosmicdrift/kumiko-types": "workspace:*",
|
|
25
26
|
"react": "^19.2.6",
|
|
26
27
|
"react-dom": "^19.2.6",
|
|
27
28
|
"zod": "^4.4.3",
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
35
35
|
import { TestUsers, unsafeCreateEntityTable } from "@cosmicdrift/kumiko-framework/stack";
|
|
36
36
|
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
37
|
-
import { Temporal } from "temporal
|
|
37
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
38
38
|
import { ADMIN_EMAIL, BETA_TENANT_ID, DEMO_NOTE_ID, DEV_TENANT_ID } from "./auth-constants";
|
|
39
39
|
import { noteEntity } from "./notes-feature";
|
|
40
40
|
|
|
@@ -5,7 +5,7 @@ File-based migrations for Event-Sourcing operations — like `kumiko schema appl
|
|
|
5
5
|
## What it shows
|
|
6
6
|
|
|
7
7
|
- **`seedsDir`-Option** in `runProdApp({ seedsDir: "./seeds" })` — beim Boot werden pending Files in chronologischer Reihenfolge angewendet und in `kumiko_es_operations` markiert.
|
|
8
|
-
- **`SeedMigration`-Interface** — default-Export einer seed-File mit `description` + `run(ctx)`. ctx liefert `systemWriteAs` (System-User bypassed Access-Check) + Read-Helpers (`findUserByEmail`, `findMembershipsOfUser`, `findTenants`).
|
|
8
|
+
- **`SeedMigration`-Interface** — default-Export einer seed-File mit `description` + `run(ctx)`. ctx liefert `systemWriteAs` (System-User bypassed Access-Check) + Read-Helpers (`findUserByEmail`, `findMembershipsOfUser`, `findTenants`, `findTemplateResources`).
|
|
9
9
|
- **Idempotency** — Marker landet nach Erfolg in der Tracking-Tabelle, zweiter Boot skipped applied Seeds.
|
|
10
10
|
- **Tx-Atomicity** — Jede Migration läuft in eigener Transaction; Failure rollt zurück + bricht Boot ab (kein Partial-Apply).
|
|
11
11
|
- **Chronologische File-IDs** — Filename `<date>-<slug>.ts` (z.B. `2026-05-20-fix-admin-roles.ts`) ist die ID.
|
|
@@ -35,6 +35,18 @@ seed: 2026-05-20-fix-admin-roles.ts
|
|
|
35
35
|
|
|
36
36
|
Bei typo / drift fail-t mit klarer message + exit-code 1 — CI nutzt es als prä-build-step. App-Author muss in `smoke.ts` den `features`-Array gegen die eigene App-Feature-Set tauschen (siehe TODO im File).
|
|
37
37
|
|
|
38
|
+
### Archive template rows
|
|
39
|
+
|
|
40
|
+
`findTemplateResources({ slug?, kind?, status?, locale?, tenantId? })` replaces a raw `SELECT … FROM read_template_resources`. It defaults to the system tenant and returns `[]` when template-resolver is not mounted:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
for (const t of await ctx.findTemplateResources({ kind: "notification", status: "active" })) {
|
|
44
|
+
await ctx.systemWriteAs("template-resolver:write:archive", { id: t.id }, SYSTEM_TENANT_ID, [
|
|
45
|
+
"SystemAdmin",
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
38
50
|
## When to reach for it
|
|
39
51
|
|
|
40
52
|
Du hast einen idempotent-Seeder der bei initialer Erstellung „Insert wenn nicht existiert" macht (z.B. `auth.admin.memberships`), und dann ändern sich die Soll-Daten — neue Rolle, anderer Tier, korrigierte Bezeichnung. Idempotent-Seeder skippen die existing Rows → DB driftet vom Code-Stand ab.
|