@cosmicdrift/kumiko-bundled-features 0.74.0 → 0.76.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 +6 -6
- package/src/audit/feature.ts +5 -0
- package/src/auth-email-password/__tests__/auth.integration.test.ts +2 -2
- package/src/auth-email-password/__tests__/email-verification.integration.test.ts +3 -3
- package/src/auth-email-password/__tests__/password-reset.integration.test.ts +3 -3
- package/src/cap-counter/feature.ts +5 -0
- package/src/channel-email/feature.ts +5 -0
- package/src/channel-in-app/feature.ts +5 -0
- package/src/channel-push/feature.ts +5 -0
- package/src/compliance-profiles/feature.ts +5 -0
- package/src/config/feature.ts +5 -0
- package/src/custom-fields/feature.ts +5 -0
- package/src/data-retention/feature.ts +5 -0
- package/src/file-foundation/feature.ts +5 -0
- package/src/file-provider-inmemory/feature.ts +5 -0
- package/src/file-provider-s3/feature.ts +5 -0
- package/src/jobs/feature.ts +5 -0
- package/src/legal-pages/feature.ts +5 -0
- package/src/mail-foundation/feature.ts +5 -0
- package/src/mail-transport-smtp/feature.ts +5 -0
- package/src/managed-pages/feature.ts +5 -0
- package/src/rate-limiting/feature.ts +5 -0
- package/src/readiness/feature.ts +5 -0
- package/src/renderer-foundation/feature.ts +5 -0
- package/src/renderer-simple/feature.ts +5 -0
- package/src/secrets/feature.ts +5 -0
- package/src/step-dispatcher/feature.ts +5 -0
- package/src/subscription-mollie/feature.ts +5 -0
- package/src/subscription-stripe/feature.ts +5 -0
- package/src/tags/feature.ts +5 -0
- package/src/tags/web/client-plugin.tsx +0 -5
- package/src/template-resolver/feature.ts +5 -0
- package/src/text-content/feature.ts +5 -0
- package/src/tier-engine/feature.ts +5 -0
- package/src/user/schema/user.ts +3 -0
- package/src/user-data-rights/feature.ts +5 -0
- package/src/user-data-rights-defaults/feature.ts +5 -0
- package/src/user-profile/feature.ts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.76.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -84,11 +84,11 @@
|
|
|
84
84
|
"./step-dispatcher": "./src/step-dispatcher/index.ts"
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
88
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
89
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
90
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
91
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
87
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.76.0",
|
|
88
|
+
"@cosmicdrift/kumiko-framework": "0.76.0",
|
|
89
|
+
"@cosmicdrift/kumiko-headless": "0.76.0",
|
|
90
|
+
"@cosmicdrift/kumiko-renderer": "0.76.0",
|
|
91
|
+
"@cosmicdrift/kumiko-renderer-web": "0.76.0",
|
|
92
92
|
"@mollie/api-client": "^4.5.0",
|
|
93
93
|
"@node-rs/argon2": "^2.0.2",
|
|
94
94
|
"@types/nodemailer": "^8.0.0",
|
package/src/audit/feature.ts
CHANGED
|
@@ -18,6 +18,11 @@ export function createAuditFeature(): FeatureDefinition {
|
|
|
18
18
|
r.describe(
|
|
19
19
|
"Exposes the framework's event store as a paginated, filterable audit log via the `audit:query:list` handler (accessible to `Admin` and `SystemAdmin` roles). No separate table or projection \u2014 the event store is the audit trail by construction: every entity write already records who, when, what entity, and the event payload with PII stripped. Filter by `aggregateType`, `aggregateId`, `eventType`, `userId`, or time range.",
|
|
20
20
|
);
|
|
21
|
+
r.uiHints({
|
|
22
|
+
displayLabel: "Audit Log",
|
|
23
|
+
category: "compliance",
|
|
24
|
+
recommended: false,
|
|
25
|
+
});
|
|
21
26
|
const queries = {
|
|
22
27
|
list: r.queryHandler(listQuery),
|
|
23
28
|
};
|
|
@@ -2,7 +2,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:tes
|
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
3
|
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
4
4
|
import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
|
|
5
|
-
import type
|
|
5
|
+
import { SYSTEM_TENANT_ID, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
6
6
|
import {
|
|
7
7
|
createTestUser,
|
|
8
8
|
setupTestStack,
|
|
@@ -255,7 +255,7 @@ describe("scenario 5b: change-password when the aggregate stream tenant != sessi
|
|
|
255
255
|
`SELECT "tenant_id" AS t FROM "kumiko_events" WHERE "aggregate_id" = $1 AND "aggregate_type" = $2 ORDER BY "version" LIMIT 1`,
|
|
256
256
|
[seed.id, "user"],
|
|
257
257
|
)) as ReadonlyArray<{ t: string }>;
|
|
258
|
-
expect(streamRows[0]?.t).toBe(
|
|
258
|
+
expect(streamRows[0]?.t).toBe(SYSTEM_TENANT_ID);
|
|
259
259
|
expect(streamRows[0]?.t).not.toBe(membershipTenant);
|
|
260
260
|
|
|
261
261
|
const signedIn = createTestUser({ id: seed.id, tenantId: seed.tenantId, roles: ["User"] });
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
updateMany,
|
|
8
8
|
} from "@cosmicdrift/kumiko-framework/bun-db";
|
|
9
9
|
import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
|
|
10
|
-
import type
|
|
10
|
+
import { SYSTEM_TENANT_ID, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
11
11
|
import {
|
|
12
12
|
setupTestStack,
|
|
13
13
|
type TestStack,
|
|
@@ -250,7 +250,7 @@ describe("POST /auth/verify-email", () => {
|
|
|
250
250
|
`SELECT "tenant_id" FROM "kumiko_events" WHERE "aggregate_id" = $1 AND "aggregate_type" = 'user' ORDER BY "version" LIMIT 1`,
|
|
251
251
|
[seed.id],
|
|
252
252
|
)) as ReadonlyArray<{ tenant_id: string }>;
|
|
253
|
-
expect(streamRows[0]?.tenant_id).toBe(
|
|
253
|
+
expect(streamRows[0]?.tenant_id).toBe(SYSTEM_TENANT_ID);
|
|
254
254
|
|
|
255
255
|
const { token } = signVerificationToken(seed.id, 60, verifySecret);
|
|
256
256
|
const res = await post("/api/auth/verify-email", { token });
|
|
@@ -407,7 +407,7 @@ describe("verify-email — aggregate stream in a non-membership tenant (sysadmin
|
|
|
407
407
|
[seed.id],
|
|
408
408
|
)) as ReadonlyArray<{ tenant_id: string; aggregate_type: string }>;
|
|
409
409
|
expect(streamRows[0]?.aggregate_type).toBe("user");
|
|
410
|
-
expect(streamRows[0]?.tenant_id).toBe(
|
|
410
|
+
expect(streamRows[0]?.tenant_id).toBe(SYSTEM_TENANT_ID);
|
|
411
411
|
expect(streamRows[0]?.tenant_id).not.toBe(membershipTenant);
|
|
412
412
|
|
|
413
413
|
const { token } = signVerificationToken(seed.id, 60, verifySecret);
|
|
@@ -2,7 +2,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:tes
|
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
3
|
import { asRawClient, insertOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
4
4
|
import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
|
|
5
|
-
import type
|
|
5
|
+
import { SYSTEM_TENANT_ID, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
6
6
|
import {
|
|
7
7
|
setupTestStack,
|
|
8
8
|
type TestStack,
|
|
@@ -308,7 +308,7 @@ describe("POST /auth/reset-password", () => {
|
|
|
308
308
|
`SELECT "tenant_id" FROM "kumiko_events" WHERE "aggregate_id" = $1 AND "aggregate_type" = 'user' ORDER BY "version" LIMIT 1`,
|
|
309
309
|
[seed.id],
|
|
310
310
|
)) as ReadonlyArray<{ tenant_id: string }>;
|
|
311
|
-
expect(streamRows[0]?.tenant_id).toBe(
|
|
311
|
+
expect(streamRows[0]?.tenant_id).toBe(SYSTEM_TENANT_ID);
|
|
312
312
|
|
|
313
313
|
const { token } = signResetToken(seed.id, 15, resetSecret);
|
|
314
314
|
const res = await post("/api/auth/reset-password", {
|
|
@@ -390,7 +390,7 @@ describe("POST /auth/reset-password", () => {
|
|
|
390
390
|
[seed.id],
|
|
391
391
|
)) as ReadonlyArray<{ tenant_id: string; aggregate_type: string }>;
|
|
392
392
|
expect(streamRows[0]?.aggregate_type).toBe("user");
|
|
393
|
-
expect(streamRows[0]?.tenant_id).toBe(
|
|
393
|
+
expect(streamRows[0]?.tenant_id).toBe(SYSTEM_TENANT_ID);
|
|
394
394
|
expect(streamRows[0]?.tenant_id).not.toBe(membershipTenant);
|
|
395
395
|
|
|
396
396
|
const { token } = signResetToken(seed.id, 15, resetSecret);
|
|
@@ -63,6 +63,11 @@ export const capCounterFeature = defineFeature(CAP_COUNTER_FEATURE, (r) => {
|
|
|
63
63
|
r.describe(
|
|
64
64
|
"Tracks per-tenant usage against configurable limits using two complementary storage models: calendar-period counters (one projection row per tenant/capName/period, reset implicitly by period rollover) and rolling-window counters (append-only event stream, no projection). Use `enforceCap` / `enforceRollingCap` (or the `withCapEnforcement` / `withRollingCapEnforcement` handler wrappers) in your write-handlers to check limits with soft-warn and hard-block tolerances; call `enforceCapAndMaybeNotify` when you also want to trigger a delivery notification on soft-threshold hits.",
|
|
65
65
|
);
|
|
66
|
+
r.uiHints({
|
|
67
|
+
displayLabel: "Cap Counter · Usage Limits",
|
|
68
|
+
category: "operations",
|
|
69
|
+
recommended: false,
|
|
70
|
+
});
|
|
66
71
|
r.entity("cap-counter", capCounterEntity);
|
|
67
72
|
|
|
68
73
|
// Custom Domain-Event für Rolling-Counter. r.defineEvent registriert
|
|
@@ -8,6 +8,11 @@ export function createChannelEmailFeature(options: EmailChannelOptions): Feature
|
|
|
8
8
|
r.describe(
|
|
9
9
|
"Wires an `EmailTransport` (typically `mail-transport-smtp` in production, `createInMemoryTransport()` in tests) into the delivery system as the `email` channel. Requires `delivery`; pass an `EmailChannelOptions` with a `transport`, a `renderer: NotificationRenderer` (e.g. backed by `renderer-simple`), and a `resolveEmail` function that maps a user ID to their email address.",
|
|
10
10
|
);
|
|
11
|
+
r.uiHints({
|
|
12
|
+
displayLabel: "Email Channel",
|
|
13
|
+
category: "notifications",
|
|
14
|
+
recommended: false,
|
|
15
|
+
});
|
|
11
16
|
r.requires("delivery");
|
|
12
17
|
|
|
13
18
|
r.useExtension("deliveryChannel", "email", {
|
|
@@ -10,6 +10,11 @@ export function createChannelInAppFeature(): FeatureDefinition {
|
|
|
10
10
|
r.describe(
|
|
11
11
|
"Persists notifications to an in-app inbox table so users can retrieve them via `handlers.inbox` and track unread state with `handlers.markRead` / `handlers.markAllRead` and `queries.unreadCount`. Requires `delivery`; no external service needed \u2014 messages are stored in the app's own database.",
|
|
12
12
|
);
|
|
13
|
+
r.uiHints({
|
|
14
|
+
displayLabel: "In-App Inbox",
|
|
15
|
+
category: "notifications",
|
|
16
|
+
recommended: false,
|
|
17
|
+
});
|
|
13
18
|
r.requires("delivery");
|
|
14
19
|
|
|
15
20
|
// Register as delivery channel via extension system
|
|
@@ -8,6 +8,11 @@ export function createChannelPushFeature(options: PushChannelOptions): FeatureDe
|
|
|
8
8
|
r.describe(
|
|
9
9
|
"Delivers push notifications through a `PushTransport` (bring your own FCM/APNs adapter or use `createInMemoryPushTransport()` for tests) registered as the `push` channel in the delivery system. Requires `delivery`; supply a `PushChannelOptions` with a transport and a resolver that maps a user ID to their device token.",
|
|
10
10
|
);
|
|
11
|
+
r.uiHints({
|
|
12
|
+
displayLabel: "Push Channel",
|
|
13
|
+
category: "notifications",
|
|
14
|
+
recommended: false,
|
|
15
|
+
});
|
|
11
16
|
r.requires("delivery");
|
|
12
17
|
|
|
13
18
|
r.useExtension("deliveryChannel", "push", {
|
|
@@ -30,6 +30,11 @@ export function createComplianceProfilesFeature(): FeatureDefinition {
|
|
|
30
30
|
r.describe(
|
|
31
31
|
"Lets each tenant select a compliance regime (e.g. `eu-dsgvo`, `swiss-dsg`, `de-hr-dsgvo-hgb`) that bundles user-rights grace periods, breach-disclosure deadlines, sub-processor requirements, and audit-retention rules into a single named profile. Tenant admins call `compliance-profiles:write:set-profile` to choose a profile (with optional JSON override for edge cases); other features resolve the effective profile via the `compliance.forTenant` cross-feature API. Required by `user-data-rights` \u2014 mount this feature before it.",
|
|
32
32
|
);
|
|
33
|
+
r.uiHints({
|
|
34
|
+
displayLabel: "Compliance Profiles",
|
|
35
|
+
category: "compliance",
|
|
36
|
+
recommended: false,
|
|
37
|
+
});
|
|
33
38
|
// Standalone — kein r.requires noetig: tenantId kommt aus dem User-
|
|
34
39
|
// Context, Profile-Selection ist eigene Entity, sub-processor-Liste
|
|
35
40
|
// sind Constants. Wenn S1.4+ Cross-Feature-Reads dazukommen, kommt
|
package/src/config/feature.ts
CHANGED
|
@@ -29,6 +29,11 @@ export function createConfigFeature(): FeatureDefinition {
|
|
|
29
29
|
r.describe(
|
|
30
30
|
"Stores per-tenant (and optionally per-user) configuration values with a multi-layer cascade: user-row \u2192 tenant-row \u2192 system-row \u2192 app-override (deploy-time `AppConfigOverrides`) \u2192 computed \u2192 feature default. Access a value in handlers via `ctx.config(handle)`, declare keys with `r.config({ keys: { ... } })` inside a feature's registry callback, and optionally mark them `encrypted: true` to route storage through an `EncryptionProvider`. Use this feature whenever a tenant admin needs to customise behaviour at runtime without a code deploy.",
|
|
31
31
|
);
|
|
32
|
+
r.uiHints({
|
|
33
|
+
displayLabel: "Tenant Config Store",
|
|
34
|
+
category: "infrastructure",
|
|
35
|
+
recommended: true,
|
|
36
|
+
});
|
|
32
37
|
r.systemScope();
|
|
33
38
|
|
|
34
39
|
// One aggregate stream per (key, scope) pair — the executor handles the
|
|
@@ -102,6 +102,11 @@ function registerCustomFields(
|
|
|
102
102
|
r.describe(
|
|
103
103
|
"Tenant- and system-scoped custom field definitions with generic value storage on any host entity. Registers the `field-definition` entity (event-sourced CRUD via `define-tenant-field`, `define-system-field`, `update-tenant-field`, `delete-tenant-field`, `delete-system-field`) and two value write-handlers (`set-custom-field`, `clear-custom-field`) that emit `custom-fields:event:custom-field-set` / `custom-fields:event:custom-field-cleared` events on the host aggregate's stream. To attach custom fields to your own entity, call `wireCustomFieldsFor(r, entityName, entityTable)` in the host feature — this wires the JSONB projection, `postQuery` flattening hook, and search-payload extension. The host entity must declare a `customFieldsField()` JSONB column.",
|
|
104
104
|
);
|
|
105
|
+
r.uiHints({
|
|
106
|
+
displayLabel: "Custom Fields",
|
|
107
|
+
category: "data",
|
|
108
|
+
recommended: false,
|
|
109
|
+
});
|
|
105
110
|
r.entity("field-definition", fieldDefinitionEntity);
|
|
106
111
|
|
|
107
112
|
// Event-types — qualified als "custom-fields:event:<short-name>".
|
|
@@ -47,6 +47,11 @@ export function createDataRetentionFeature(): FeatureDefinition {
|
|
|
47
47
|
r.describe(
|
|
48
48
|
"Resolves the effective retention policy for any entity using a 3-layer stack: entity-level default \u2192 tenant preset (`dsgvo-basic`, `dsgvo-hgb`, `swiss-dsg`) \u2192 per-tenant override stored in `tenantRetentionOverride`. Other features query the resolved policy via the `retention.policyFor` cross-feature API \u2014 most notably `user-data-rights`, which uses it to decide whether to anonymize instead of hard-delete a record that is still within a mandatory retention window.",
|
|
49
49
|
);
|
|
50
|
+
r.uiHints({
|
|
51
|
+
displayLabel: "Data Retention Policy",
|
|
52
|
+
category: "compliance",
|
|
53
|
+
recommended: false,
|
|
54
|
+
});
|
|
50
55
|
r.entity("tenant-retention-override", tenantRetentionOverrideEntity);
|
|
51
56
|
|
|
52
57
|
// S2.D3: Cross-Feature-API fuer Forget-Flow + Cleanup-Job
|
|
@@ -99,6 +99,11 @@ export const fileFoundationFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
|
99
99
|
r.describe(
|
|
100
100
|
"Defines the `fileProvider` extension point and a per-tenant `provider` config key that selects which registered storage plugin to use at runtime. Call `createFileProviderForTenant(ctx, tenantId)` to get a `FileStorageProvider` \u2014 use this feature together with at least one `file-provider-*` feature; the `files` feature builds on top of it for tracked `FileRef` entities with GDPR hooks.",
|
|
101
101
|
);
|
|
102
|
+
r.uiHints({
|
|
103
|
+
displayLabel: "File Provider Foundation",
|
|
104
|
+
category: "storage",
|
|
105
|
+
recommended: false,
|
|
106
|
+
});
|
|
102
107
|
r.requires("config");
|
|
103
108
|
|
|
104
109
|
r.extendsRegistrar("fileProvider", {
|
|
@@ -63,6 +63,11 @@ export const fileProviderInMemoryFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
|
63
63
|
r.describe(
|
|
64
64
|
'Registers an in-process `"inmemory"` provider for `file-foundation` that stores file bytes per tenant in a module-level Map. Use `listKeys(tenantId)` and `clearStorage(tenantId)` in demo apps and tests; not for production (data is lost on restart and grows without bound).',
|
|
65
65
|
);
|
|
66
|
+
r.uiHints({
|
|
67
|
+
displayLabel: "File Provider · In-Memory",
|
|
68
|
+
category: "storage",
|
|
69
|
+
recommended: false,
|
|
70
|
+
});
|
|
66
71
|
// Kein r.requires("config") + kein r.requires("secrets") — der
|
|
67
72
|
// In-Memory-Provider hat keine Config + kein Secret. Nur die
|
|
68
73
|
// file-foundation muss da sein (Plugin-extension-point).
|
|
@@ -42,6 +42,11 @@ export const fileProviderS3Feature = defineFeature(FEATURE_NAME, (r) => {
|
|
|
42
42
|
r.describe(
|
|
43
43
|
'Registers itself as the `"s3"` provider for `file-foundation` and owns the per-tenant config keys (`bucket`, `region`, `endpoint`, `forcePathStyle`, `accessKeyId`) and the encrypted `s3.secretAccessKey` secret. Compatible with any S3-compatible object store (AWS S3, Hetzner Object Storage); set credentials via the admin UI or a seed handler before the first file operation.',
|
|
44
44
|
);
|
|
45
|
+
r.uiHints({
|
|
46
|
+
displayLabel: "File Provider · S3",
|
|
47
|
+
category: "storage",
|
|
48
|
+
recommended: false,
|
|
49
|
+
});
|
|
45
50
|
r.requires("config");
|
|
46
51
|
r.requires("secrets");
|
|
47
52
|
r.requires("file-foundation");
|
package/src/jobs/feature.ts
CHANGED
|
@@ -30,6 +30,11 @@ export function createJobsFeature(): FeatureDefinition {
|
|
|
30
30
|
r.describe(
|
|
31
31
|
"Persistence and operator tooling for background jobs registered via `r.job(...)`. Every job execution appends `run-started`, `run-completed`, and `run-failed` events to the `jobRun` aggregate stream, which two inline projections materialize into `read_job_runs` (current status + duration) and `read_job_run_logs` (per-line log rows). Exposes `jobs:write:trigger` (manual run) and `jobs:write:retry` (operator retry of a failed run), plus `jobs:query:list` and `jobs:query:details` for the operator UI.",
|
|
32
32
|
);
|
|
33
|
+
r.uiHints({
|
|
34
|
+
displayLabel: "Jobs · Audit & Operator UI",
|
|
35
|
+
category: "operations",
|
|
36
|
+
recommended: false,
|
|
37
|
+
});
|
|
33
38
|
r.systemScope();
|
|
34
39
|
r.unmanagedTable(jobRunLogsTableMeta, {
|
|
35
40
|
reason: "read_side.job_run_logs",
|
|
@@ -64,6 +64,11 @@ export function createLegalPagesFeature(opts: LegalPagesOptions = {}): FeatureDe
|
|
|
64
64
|
r.describe(
|
|
65
65
|
"Opt-in wrapper around `text-content` that registers four public HTML routes (`/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 DE blocks (`imprint/de`, `privacy/de`) are not seeded in `SYSTEM_TENANT`. Requires `anonymousAccess: { defaultTenantId: SYSTEM_TENANT_ID }` and `extraContext.textContent` to be wired at app bootstrap; for per-tenant imprints or a custom layout call `text-content:query:by-slug` directly.",
|
|
66
66
|
);
|
|
67
|
+
r.uiHints({
|
|
68
|
+
displayLabel: "Legal Pages",
|
|
69
|
+
category: "content",
|
|
70
|
+
recommended: false,
|
|
71
|
+
});
|
|
67
72
|
r.requires("text-content");
|
|
68
73
|
|
|
69
74
|
// 4 Public-HTML-Routes
|
|
@@ -68,6 +68,11 @@ export const mailFoundationFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
|
68
68
|
r.describe(
|
|
69
69
|
"Defines the `mailTransport` extension point and a per-tenant `provider` config key that selects which registered transport plugin to use at runtime. Call `createTransportForTenant(ctx, tenantId)` to get an `EmailTransport` ready for sending \u2014 use this feature together with at least one `mail-transport-*` feature; use `delivery` + `channel-email` instead when you need the full notification pipeline with delivery attempts and user preferences.",
|
|
70
70
|
);
|
|
71
|
+
r.uiHints({
|
|
72
|
+
displayLabel: "Mail Transport Foundation",
|
|
73
|
+
category: "notifications",
|
|
74
|
+
recommended: false,
|
|
75
|
+
});
|
|
71
76
|
r.requires("config");
|
|
72
77
|
|
|
73
78
|
// Plugin extension-point. Provider-features register here. The
|
|
@@ -53,6 +53,11 @@ export const mailTransportSmtpFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
|
53
53
|
r.describe(
|
|
54
54
|
'Registers itself as the `"smtp"` provider for `mail-foundation` and owns the per-tenant config keys (`host`, `port`, `secure`, `from`, `authUser`) and the encrypted `smtp.password` secret. Tenants set `mail-foundation`\'s `provider` config key to `"smtp"` to activate it; set the SMTP credentials via the admin UI or a seed handler before sending the first mail.',
|
|
55
55
|
);
|
|
56
|
+
r.uiHints({
|
|
57
|
+
displayLabel: "Mail Transport · SMTP",
|
|
58
|
+
category: "notifications",
|
|
59
|
+
recommended: false,
|
|
60
|
+
});
|
|
56
61
|
r.requires("config");
|
|
57
62
|
r.requires("secrets");
|
|
58
63
|
r.requires("mail-foundation");
|
|
@@ -144,6 +144,11 @@ export function createManagedPagesFeature(opts: ManagedPagesOptions): FeatureDef
|
|
|
144
144
|
r.describe(
|
|
145
145
|
"Tenant-editable, server-rendered public pages with per-tenant branding. Stores one Markdown `page` per `(tenantId, slug, lang)` in the `read_pages` entity table with a `published` gate plus `description`/`ogImage` SEO meta. Registers an anonymous `GET {basePath}/:slug` route that resolves the tenant from the request Host via the app-supplied `resolveApexTenant`, serves only published pages (drafts → 404), renders Markdown through the hardened `page-render` core, and isolates per-tenant content with `Vary: Host`. Ships TenantAdmin/SystemAdmin admin screens (`entityList` + `entityEdit`) backed by convention CRUD handlers (`managed-pages:write:page:{create,update,delete}`, `managed-pages:query:page:{list,detail}`); the app wires nav/workspace onto `managed-pages:screen:page-list`. Branding (via `config`, scope tenant): `branding-{title,description,site-url,accent-color,logo-url,layout-preset}` keys with write-time validation (hex color, https URLs), a `configEdit` self-service screen (`managed-pages:screen:branding-settings`), and a `managed-pages:query:branding` read that the render path applies as scoped `:root` CSS vars + a logo/title header. Also exposes `managed-pages:write:set` (idempotent slug-keyed upsert, SystemAdmin cross-tenant via `tenantIdOverride`) as a provisioning API. Requires `config` + `anonymousAccess` wired at app bootstrap.",
|
|
146
146
|
);
|
|
147
|
+
r.uiHints({
|
|
148
|
+
displayLabel: "Managed Pages · Public CMS",
|
|
149
|
+
category: "content",
|
|
150
|
+
recommended: false,
|
|
151
|
+
});
|
|
147
152
|
r.requires("config");
|
|
148
153
|
r.entity("page", pageEntity);
|
|
149
154
|
|
|
@@ -14,6 +14,11 @@ export function createRateLimitingFeature() {
|
|
|
14
14
|
r.describe(
|
|
15
15
|
"Adds an ops-side `rate-limiting:query:status` query handler for inspecting current bucket state; the actual request throttling is wired automatically by the dispatcher when any handler declares a `rateLimit` option (e.g. `{ per: 'user', limit: 3, windowSeconds: 60 }`) or when you pass `context.rateLimit` to `buildServer`. Loading this feature is optional if you only need L3 per-handler rate limits and have no need for ops introspection.",
|
|
16
16
|
);
|
|
17
|
+
r.uiHints({
|
|
18
|
+
displayLabel: "Rate Limiting · Ops Query",
|
|
19
|
+
category: "operations",
|
|
20
|
+
recommended: false,
|
|
21
|
+
});
|
|
17
22
|
r.queryHandler(rateLimitStatus);
|
|
18
23
|
});
|
|
19
24
|
}
|
package/src/readiness/feature.ts
CHANGED
|
@@ -15,6 +15,11 @@ export const readinessFeature = defineFeature("readiness", (r) => {
|
|
|
15
15
|
r.describe(
|
|
16
16
|
"One-call tenant-onboarding probe: `readiness:query:status` rolls up every config key and secret declared `required: true` across all mounted features and reports which still lack a usable value for the calling tenant, plus a single `ready` boolean. Provider-features under an `r.extensionSelector`-declared extension point count only while their provider is the selected one — a tenant on the inmemory mail transport is not blocked by unset SMTP keys. Mount it (together with `config` and `secrets`) when an admin UI needs a settings checklist before the first mail-send or file-write; the per-concern lists stay available via `config:query:readiness` and `secrets:query:list`.",
|
|
17
17
|
);
|
|
18
|
+
r.uiHints({
|
|
19
|
+
displayLabel: "Readiness · Onboarding Probe",
|
|
20
|
+
category: "operations",
|
|
21
|
+
recommended: false,
|
|
22
|
+
});
|
|
18
23
|
r.requires("config");
|
|
19
24
|
r.requires("secrets");
|
|
20
25
|
|
|
@@ -14,6 +14,11 @@ export function createRendererFoundationFeature() {
|
|
|
14
14
|
r.describe(
|
|
15
15
|
'Plugin registry for content rendering (notification HTML, mail HTML, PDF, images): call `foundation.createRendererForTenant({ tenantId, kind })` at render time to get the right renderer plugin selected by kind, with tenant-level overrides via the `rendererPluginByKind` config key. Requires `template-resolver` (declared via `r.requires`). Low-level building block \u2014 add `renderer-simple` (or write a custom plugin via `r.useExtension("renderer", name, { kinds, render })`) rather than using this feature alone.',
|
|
16
16
|
);
|
|
17
|
+
r.uiHints({
|
|
18
|
+
displayLabel: "Renderer Foundation",
|
|
19
|
+
category: "notifications",
|
|
20
|
+
recommended: false,
|
|
21
|
+
});
|
|
17
22
|
r.requires("template-resolver");
|
|
18
23
|
|
|
19
24
|
r.extendsRegistrar("renderer", {
|
|
@@ -30,6 +30,11 @@ export function createRendererSimpleFeature(): FeatureDefinition {
|
|
|
30
30
|
r.describe(
|
|
31
31
|
'Default renderer plugin for `kind="notification"`: takes a structured `EmailTemplateData` variable map (with `header`, `sections[]` of text/button objects, and optional `footer`; falls back to `title`/`body` if no structured fields are present) and returns rendered HTML with inline CSS. Requires `renderer-foundation`; sufficient for plain notification emails \u2014 swap it for `renderer-mail-html` if you need MJML/Markdown layouts.',
|
|
32
32
|
);
|
|
33
|
+
r.uiHints({
|
|
34
|
+
displayLabel: "Renderer \u00b7 Simple",
|
|
35
|
+
category: "notifications",
|
|
36
|
+
recommended: false,
|
|
37
|
+
});
|
|
33
38
|
r.requires("renderer-foundation");
|
|
34
39
|
|
|
35
40
|
r.useExtension("renderer", "simple", {
|
package/src/secrets/feature.ts
CHANGED
|
@@ -87,6 +87,11 @@ export function createSecretsFeature(): FeatureDefinition {
|
|
|
87
87
|
r.describe(
|
|
88
88
|
"Stores arbitrary per-tenant secrets (API keys, tokens, credentials) encrypted at rest using AES-256 with a KEK loaded from `KUMIKO_SECRETS_MASTER_KEY_V1` (and successive versions for rotation). Read a secret in handlers via `ctx.secrets.get(tenantId, handle)`, which automatically appends a `tenantSecretRead` audit event so every access is traceable. A `rotate` job re-encrypts all envelopes after a KEK version bump.",
|
|
89
89
|
);
|
|
90
|
+
r.uiHints({
|
|
91
|
+
displayLabel: "Tenant Secrets",
|
|
92
|
+
category: "infrastructure",
|
|
93
|
+
recommended: true,
|
|
94
|
+
});
|
|
90
95
|
r.envSchema(secretsEnvSchema);
|
|
91
96
|
|
|
92
97
|
// ES entity: set/delete go through the executor, `tenantSecret.created/
|
|
@@ -32,6 +32,11 @@ export function createStepDispatcherFeature(): FeatureDefinition {
|
|
|
32
32
|
r.describe(
|
|
33
33
|
"Internal system feature that drains deferred Tier-2 side-effects (currently `webhook.send` and `mail.send`) after their originating transaction commits. Listens via `r.multiStreamProjection` on the `kumiko:system:step.dispatch-requested` system event, performs the actual HTTP or mail delivery, then appends `kumiko:system:step.dispatched` or `kumiko:system:step.dispatch-failed` back onto the same stream so the outcome is recorded in the event log without a separate status table. Mount this feature explicitly via `createStepDispatcherFeature()` in your app's feature list alongside any features that use `r.step.webhook.send` or `r.step.mail.send`.",
|
|
34
34
|
);
|
|
35
|
+
r.uiHints({
|
|
36
|
+
displayLabel: "Step Dispatcher · Deferred Side-Effects",
|
|
37
|
+
category: "infrastructure",
|
|
38
|
+
recommended: false,
|
|
39
|
+
});
|
|
35
40
|
r.systemScope();
|
|
36
41
|
|
|
37
42
|
r.multiStreamProjection({
|
|
@@ -149,6 +149,11 @@ export function createSubscriptionMollieFeature(
|
|
|
149
149
|
r.describe(
|
|
150
150
|
'Mollie payment provider plugin for `billing-foundation`, covering the DACH/EU mid-market use case. Mount via `createSubscriptionMollieFeature({ apiKey, webhookUrl, priceToTier, priceToConfig })` \u2014 the factory validates that `priceToTier` and `priceToConfig` keys are identical at boot time. Implements `verifyAndParseWebhook` (lazy Mollie-API fetch + heuristic event-type mapping) and `createCheckoutSession` (customer + first-payment with `sequenceType="first"`); `createPortalSession` and `cancelSubscription` are not available because Mollie has no customer portal and requires a `customerId` that the plugin contract does not carry.',
|
|
151
151
|
);
|
|
152
|
+
r.uiHints({
|
|
153
|
+
displayLabel: "Billing \u00b7 Mollie",
|
|
154
|
+
category: "billing",
|
|
155
|
+
recommended: false,
|
|
156
|
+
});
|
|
152
157
|
r.requires("billing-foundation");
|
|
153
158
|
r.envSchema(subscriptionMollieEnvSchema);
|
|
154
159
|
|
|
@@ -111,6 +111,11 @@ export function createSubscriptionStripeFeature(
|
|
|
111
111
|
r.describe(
|
|
112
112
|
'Stripe payment provider plugin for `billing-foundation`. Reads its Stripe API key + webhook secret from system config keys with `backing:"secrets"` (envelope-encrypted in the secrets store under the system tenant) and a `billingLive` **system config** flag — all at runtime, so keys rotate and prod goes live without a redeploy. The `mask` on each key derives the sysadmin settings screen + nav, so no app wires a hand-written config UI. Mount via `createSubscriptionStripeFeature({ priceToTier })`; the optional `apiKey`/`webhookSecret` options are env→secrets bridge fallbacks. The plugin always mounts — `createCheckoutSession` throws `feature_disabled` unless `billingLive` is true, so sk_test_ keys in prod never produce a live checkout. Implements all four provider methods (webhook verify, checkout, portal, cancel).',
|
|
113
113
|
);
|
|
114
|
+
r.uiHints({
|
|
115
|
+
displayLabel: "Billing · Stripe",
|
|
116
|
+
category: "billing",
|
|
117
|
+
recommended: false,
|
|
118
|
+
});
|
|
114
119
|
// Hard-deps: billing-foundation (plugin-host) + config (billing-live +
|
|
115
120
|
// backing:"secrets" credentials) + secrets (the store the backing:secrets
|
|
116
121
|
// dispatch reads/writes + its tenant_secrets table).
|
package/src/tags/feature.ts
CHANGED
|
@@ -44,6 +44,11 @@ function registerTags(
|
|
|
44
44
|
r.describe(
|
|
45
45
|
"Generic, host-agnostic tagging for any entity. Owns two event-sourced entities — the per-tenant `tag` catalog (`read_tags`) and `tag-assignment` join rows keyed by (entityType, entityId) (`read_tag_assignments`) — so tagging adds NO column to the host entity and needs no relational pivot or JOIN. Provides write-handlers `create-tag`, `assign-tag` (idempotent), `remove-tag` (idempotent) and list queries for the catalog and the assignments. Read which tags an entity has, or which entities carry a tag, by listing `tag-assignment` filtered on `entityId` or `tagId` and composing in the read-layer. Every path uses one access rule — adopt the host's model with createTagsFeature({ access: { openToAll: true } }) or pin roles with createTagsFeature({ roles }). Pass { toggleable: { default: false } } to make the whole feature tier-gatable via the tier-engine (no host hook).",
|
|
46
46
|
);
|
|
47
|
+
r.uiHints({
|
|
48
|
+
displayLabel: "Tags",
|
|
49
|
+
category: "data",
|
|
50
|
+
recommended: false,
|
|
51
|
+
});
|
|
47
52
|
|
|
48
53
|
// Tier-gating is a framework concern, not a per-app hook: declaring the
|
|
49
54
|
// feature toggleable lets tier-engine/feature-toggles cut it per tenant.
|
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
// @runtime client
|
|
2
|
-
// Client-feature factory for tags. Mounted via
|
|
3
|
-
// createKumikoApp({ clientFeatures: [tagsClient()] }) — registers TagSection
|
|
4
|
-
// under TAGS_SECTION_EXTENSION_NAME and contributes the default translations.
|
|
5
|
-
// Required even for standalone <TagSection> use, otherwise its i18n keys render
|
|
6
|
-
// raw.
|
|
7
2
|
|
|
8
3
|
import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer-web";
|
|
9
4
|
import { TAGS_FEATURE_NAME, TAGS_SECTION_EXTENSION_NAME } from "../constants";
|
|
@@ -19,6 +19,11 @@ export function createTemplateResolverFeature() {
|
|
|
19
19
|
r.describe(
|
|
20
20
|
"Stores notification and mail templates in the database with a 4-level fallback: tenant+locale \u2192 system+locale \u2192 tenant+fallback-locale \u2192 system+fallback-locale. Call `ctx.templateResolver.resolveTemplate({ tenantId, slug, kind, locale })` at render time; manage templates via the `upsertSystem`, `upsertTenant`, `publish`, and `archive` write handlers. Tenants can override system-default templates without touching application code.",
|
|
21
21
|
);
|
|
22
|
+
r.uiHints({
|
|
23
|
+
displayLabel: "Template Resolver",
|
|
24
|
+
category: "notifications",
|
|
25
|
+
recommended: false,
|
|
26
|
+
});
|
|
22
27
|
r.entity("template-resource", templateResourceEntity);
|
|
23
28
|
|
|
24
29
|
const handlers = {
|
|
@@ -26,6 +26,11 @@ export function createTextContentFeature() {
|
|
|
26
26
|
r.describe(
|
|
27
27
|
"Generic Markdown text store keyed by `(tenantId, slug, lang)` \u2014 one row per combination in the `read_text_blocks` entity table. Provides `text-content:write:set` (TenantAdmin upsert) and `text-content:query:by-slug` (anonymous-capable read); use `SYSTEM_TENANT_ID` as the tenant for app-wide texts such as imprint, privacy policy, or FAQ. Other features (e.g. `legal-pages`) read blocks without a direct code import via the `createTextContentApi` / `requireTextContent` extraContext pattern.",
|
|
28
28
|
);
|
|
29
|
+
r.uiHints({
|
|
30
|
+
displayLabel: "Text Content \u00b7 Markdown Store",
|
|
31
|
+
category: "content",
|
|
32
|
+
recommended: false,
|
|
33
|
+
});
|
|
29
34
|
r.entity("text-block", textBlockEntity);
|
|
30
35
|
|
|
31
36
|
const handlers = {
|
|
@@ -187,6 +187,11 @@ export function createTierEngineFeature<
|
|
|
187
187
|
r.describe(
|
|
188
188
|
'Stores a `tier-assignment` entity per tenant (which pricing tier is active) and, when configured with a `TierMap`, registers itself as the `tenantTierResolver` extension so the dispatcher automatically gates `r.toggleable()` features per tenant based on their assigned tier. Call `createTierEngineFeature({ defaultTier, tierMap })` to get full tier composition \u2014 including an `inTransaction` entity hook that atomically writes the default tier when a new tenant is created \u2014 or use `createTierEngineFeature()` without options for storage-only mode when you manage tier assignment yourself via `composeApp`. A SystemAdmin-only `set-tenant-tier` write plus `get-tenant-tier`/`tier-options` reads let an operator assign a tier to ANY tenant manually \u2014 without a billing purchase \u2014 stamping `source: "manual"` so a future Stripe\u2192tier sync won\'t overwrite the grant. Apps surface this via the `tier-admin` screen.',
|
|
189
189
|
);
|
|
190
|
+
r.uiHints({
|
|
191
|
+
displayLabel: "Tier Engine \u00b7 Plan Composition",
|
|
192
|
+
category: "billing",
|
|
193
|
+
recommended: false,
|
|
194
|
+
});
|
|
190
195
|
r.requires("config");
|
|
191
196
|
r.requires("tenant");
|
|
192
197
|
|
package/src/user/schema/user.ts
CHANGED
|
@@ -56,6 +56,9 @@ const USER_STATUS_OPTIONS = [
|
|
|
56
56
|
export const userEntity = createEntity({
|
|
57
57
|
table: "read_users",
|
|
58
58
|
softDelete: true,
|
|
59
|
+
// Tenant-independent identity aggregate — its event stream lives on
|
|
60
|
+
// SYSTEM_TENANT_ID instead of whichever tenant happened to create it (#497).
|
|
61
|
+
systemStream: true,
|
|
59
62
|
fields: {
|
|
60
63
|
// Identity — anyone who can see the user can read the email, but only
|
|
61
64
|
// privileged roles (SYSTEM auth code, SystemAdmin) may change it.
|
|
@@ -107,6 +107,11 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
|
|
|
107
107
|
r.describe(
|
|
108
108
|
'Implements GDPR Art. 15 (access / `my-audit-log` query), Art. 17 (erasure / `request-deletion` + `cancel-deletion`, plus the anonymous email-verified `request-deletion-by-email` + `confirm-deletion-by-token` flow for lockout-safe self-service, + cron cleanup with grace period), Art. 18 (restriction / `restrict-account` + `lift-restriction`), and Art. 20 (portability / async `request-export` \u2192 ZIP via `file-foundation`, Magic-Link download) as first-class HTTP handlers and cron jobs. Each domain feature opts in by calling `r.useExtension(EXT_USER_DATA, "<entity>", { export, delete })` \u2014 the feature then orchestrates the export and forget pipelines across all registered hooks automatically. Requires `user`, `data-retention`, `compliance-profiles`, and `sessions`.',
|
|
109
109
|
);
|
|
110
|
+
r.uiHints({
|
|
111
|
+
displayLabel: "User Data Rights \u00b7 GDPR",
|
|
112
|
+
category: "compliance",
|
|
113
|
+
recommended: false,
|
|
114
|
+
});
|
|
110
115
|
r.requires("user", "data-retention", "compliance-profiles", "sessions");
|
|
111
116
|
r.usesApi("compliance.forTenant");
|
|
112
117
|
r.usesApi("retention.policyFor");
|
|
@@ -42,6 +42,11 @@ export function createUserDataRightsDefaultsFeature(
|
|
|
42
42
|
r.describe(
|
|
43
43
|
"Registers ready-made `EXT_USER_DATA` export and delete hooks for the two core entities: `user` (delete strategy sets email to `deleted-<id>@anonymized.invalid`, nulls `passwordHash`, sets status to `Deleted`; anonymize strategy sets email to `anonymized-<id>@anonymized.invalid` without touching `passwordHash`) and `fileRef` (delete removes both the DB row and the storage binary). Mount this alongside `user-data-rights` for standard GDPR compliance; omit it only if your app needs custom anonymization logic for these entities.",
|
|
44
44
|
);
|
|
45
|
+
r.uiHints({
|
|
46
|
+
displayLabel: "User Data Rights · Default Hooks",
|
|
47
|
+
category: "compliance",
|
|
48
|
+
recommended: false,
|
|
49
|
+
});
|
|
45
50
|
r.requires("user", "files", "user-data-rights");
|
|
46
51
|
|
|
47
52
|
r.useExtension(EXT_USER_DATA, "user", {
|
|
@@ -13,6 +13,11 @@ export function createUserProfileFeature(): FeatureDefinition {
|
|
|
13
13
|
'`type: "custom"` with `__component: "UserProfileScreen"`. Requires `user`, ' +
|
|
14
14
|
"`auth-email-password`, and `user-data-rights`.",
|
|
15
15
|
);
|
|
16
|
+
r.uiHints({
|
|
17
|
+
displayLabel: "User Profile · Self-Service",
|
|
18
|
+
category: "identity",
|
|
19
|
+
recommended: true,
|
|
20
|
+
});
|
|
16
21
|
r.requires("user");
|
|
17
22
|
r.requires("auth-email-password");
|
|
18
23
|
r.requires("user-data-rights");
|