@cosmicdrift/kumiko-bundled-features 0.27.0 → 0.28.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/src/audit/feature.ts +3 -0
- package/src/auth-email-password/feature.ts +3 -0
- package/src/billing-foundation/feature.ts +3 -0
- package/src/billing-foundation/webhook-handler.ts +13 -4
- package/src/cap-counter/feature.ts +3 -0
- package/src/channel-email/feature.ts +3 -0
- package/src/channel-in-app/feature.ts +3 -0
- package/src/channel-push/feature.ts +3 -0
- package/src/compliance-profiles/feature.ts +3 -0
- package/src/config/feature.ts +3 -0
- package/src/custom-fields/feature.ts +3 -0
- package/src/data-retention/feature.ts +3 -0
- package/src/delivery/feature.ts +3 -0
- package/src/feature-toggles/feature.ts +3 -0
- package/src/file-foundation/feature.ts +3 -0
- package/src/file-provider-inmemory/feature.ts +3 -0
- package/src/file-provider-s3/feature.ts +3 -0
- package/src/jobs/feature.ts +3 -0
- package/src/legal-pages/feature.ts +3 -0
- package/src/mail-foundation/feature.ts +3 -0
- package/src/mail-transport-inmemory/feature.ts +3 -0
- package/src/mail-transport-smtp/feature.ts +3 -0
- package/src/rate-limiting/feature.ts +3 -0
- package/src/renderer-foundation/feature.ts +3 -0
- package/src/renderer-simple/feature.ts +3 -0
- package/src/secrets/feature.ts +3 -0
- package/src/sessions/feature.ts +3 -0
- package/src/step-dispatcher/feature.ts +3 -0
- package/src/subscription-mollie/feature.ts +3 -0
- package/src/subscription-stripe/feature.ts +3 -0
- package/src/template-resolver/feature.ts +3 -0
- package/src/tenant/feature.ts +3 -0
- package/src/text-content/feature.ts +3 -0
- package/src/tier-engine/feature.ts +3 -0
- package/src/user/feature.ts +3 -0
- package/src/user-data-rights/feature.ts +3 -0
- package/src/user-data-rights-defaults/feature.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.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>",
|
package/src/audit/feature.ts
CHANGED
|
@@ -15,6 +15,9 @@ import { listQuery } from "./handlers/list.query";
|
|
|
15
15
|
// the framework).
|
|
16
16
|
export function createAuditFeature(): FeatureDefinition {
|
|
17
17
|
return defineFeature("audit", (r) => {
|
|
18
|
+
r.describe(
|
|
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
|
+
);
|
|
18
21
|
const queries = {
|
|
19
22
|
list: r.queryHandler(listQuery),
|
|
20
23
|
};
|
|
@@ -125,6 +125,9 @@ export function createAuthEmailPasswordFeature(
|
|
|
125
125
|
opts.emailVerification !== undefined && (opts.emailVerification.mode ?? "strict") === "strict";
|
|
126
126
|
|
|
127
127
|
return defineFeature("auth-email-password", (r) => {
|
|
128
|
+
r.describe(
|
|
129
|
+
"Provides email+password authentication: the always-on handlers are `login`, `changePassword`, and `logout`; optional flows \u2014 password reset, email verification, magic-link self-signup, and tenant invite \u2014 are registered only when you pass their respective option objects (`passwordReset`, `emailVerification`, `signup`, `invite`) to `createAuthEmailPasswordFeature(opts)`. Each opt-in flow uses HMAC-signed or opaque-random tokens delivered via callback (e.g. `sendResetEmail`) so the feature stays transport-agnostic. Requires the `user` and `tenant` features, and declares `JWT_SECRET` (\u2265 32 chars) in `authEmailPasswordEnvSchema` so a missing secret surfaces at boot validation rather than on the first login attempt.",
|
|
130
|
+
);
|
|
128
131
|
r.requires("user");
|
|
129
132
|
r.requires("tenant");
|
|
130
133
|
r.envSchema(authEmailPasswordEnvSchema);
|
|
@@ -71,6 +71,9 @@ import {
|
|
|
71
71
|
} from "./projection";
|
|
72
72
|
|
|
73
73
|
export const billingFoundationFeature = defineFeature(BILLING_FOUNDATION_FEATURE, (r) => {
|
|
74
|
+
r.describe(
|
|
75
|
+
"Plugin host for subscription billing \u2014 manages the `read_subscriptions` projection table and exposes 5 domain events (subscription created/updated/canceled, invoice paid/failed) appended by the foundation's own `billing-foundation:write:process-event` write-handler after provider plugins verify and normalize each webhook. Also ships `billing-foundation:write:create-checkout-session` and `billing-foundation:write:create-portal-session` write-handlers, a `billing-foundation:query:subscription:list` query handler, and a `createSubscriptionWebhookHandler` factory for the `/api/subscription/webhook/:providerName` route. Low-level building block \u2014 use `subscription-stripe` or `subscription-mollie` unless you are writing a new payment provider.",
|
|
76
|
+
);
|
|
74
77
|
// 5 fine-grained domain-events. Alle 5 nutzen denselben payload-
|
|
75
78
|
// shape (= subscription-state-snapshot); der event-type taggt was
|
|
76
79
|
// passiert ist. Future-consumer (billing-history, accounting)
|
|
@@ -7,12 +7,20 @@
|
|
|
7
7
|
// `/api/subscription/webhook/paypal`. Eine Hono-Route, alle Plugins
|
|
8
8
|
// gleichzeitig aktiv.
|
|
9
9
|
//
|
|
10
|
-
// Beispiel-Verwendung in bin/server.ts
|
|
10
|
+
// Beispiel-Verwendung in bin/server.ts (runDevApp wie runProdApp liefern
|
|
11
|
+
// `registry` + `dispatchSystemWrite` in den extraRoutes-deps):
|
|
11
12
|
//
|
|
12
13
|
// await runDevApp({
|
|
13
14
|
// features: APP_FEATURES,
|
|
14
15
|
// extraRoutes: (app, deps) => {
|
|
15
|
-
// const handler = createSubscriptionWebhookHandler(
|
|
16
|
+
// const handler = createSubscriptionWebhookHandler({
|
|
17
|
+
// dispatchWrite: ({ handlerQn, payload, tenantId }) =>
|
|
18
|
+
// deps.dispatchSystemWrite({ handlerQn, payload, tenantId: tenantId as TenantId }),
|
|
19
|
+
// resolveProvider: (name) =>
|
|
20
|
+
// deps.registry.getExtensionUsages("subscriptionProvider")
|
|
21
|
+
// .find((u) => u.entityName === name)?.options as
|
|
22
|
+
// SubscriptionProviderPlugin | undefined,
|
|
23
|
+
// });
|
|
16
24
|
// app.post("/api/subscription/webhook/:providerName", handler);
|
|
17
25
|
// },
|
|
18
26
|
// });
|
|
@@ -43,8 +51,9 @@ import {
|
|
|
43
51
|
import type { SubscriptionProviderPlugin } from "./types";
|
|
44
52
|
|
|
45
53
|
/**
|
|
46
|
-
* Dependencies the App-Owner gibt dem webhook-handler
|
|
47
|
-
* `extraRoutes`-
|
|
54
|
+
* Dependencies the App-Owner gibt dem webhook-handler — beide direkt aus
|
|
55
|
+
* den `extraRoutes`-deps ableitbar (`dispatchSystemWrite` + `registry`),
|
|
56
|
+
* siehe Beispiel im Header.
|
|
48
57
|
*/
|
|
49
58
|
export type SubscriptionWebhookDeps = {
|
|
50
59
|
/** Schreibt durch den Standard-Dispatcher mit einem auto-konstruierten
|
|
@@ -60,6 +60,9 @@ import { markSoftWarnedHandler } from "./handlers/mark-soft-warned.write";
|
|
|
60
60
|
const sysadminAccess = { access: { roles: ["SystemAdmin"] } } as const;
|
|
61
61
|
|
|
62
62
|
export const capCounterFeature = defineFeature(CAP_COUNTER_FEATURE, (r) => {
|
|
63
|
+
r.describe(
|
|
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
|
+
);
|
|
63
66
|
r.entity("cap-counter", capCounterEntity);
|
|
64
67
|
|
|
65
68
|
// Custom Domain-Event für Rolling-Counter. r.defineEvent registriert
|
|
@@ -5,6 +5,9 @@ export function createChannelEmailFeature(options: EmailChannelOptions): Feature
|
|
|
5
5
|
const channel = createEmailChannel(options);
|
|
6
6
|
|
|
7
7
|
return defineFeature("channel-email", (r) => {
|
|
8
|
+
r.describe(
|
|
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
|
+
);
|
|
8
11
|
r.requires("delivery");
|
|
9
12
|
|
|
10
13
|
r.useExtension("deliveryChannel", "email", {
|
|
@@ -7,6 +7,9 @@ import { inAppChannel } from "./in-app-channel";
|
|
|
7
7
|
|
|
8
8
|
export function createChannelInAppFeature(): FeatureDefinition {
|
|
9
9
|
return defineFeature("channel-in-app", (r) => {
|
|
10
|
+
r.describe(
|
|
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
|
+
);
|
|
10
13
|
r.requires("delivery");
|
|
11
14
|
|
|
12
15
|
// Register as delivery channel via extension system
|
|
@@ -5,6 +5,9 @@ export function createChannelPushFeature(options: PushChannelOptions): FeatureDe
|
|
|
5
5
|
const channel = createPushChannel(options);
|
|
6
6
|
|
|
7
7
|
return defineFeature("channel-push", (r) => {
|
|
8
|
+
r.describe(
|
|
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
|
+
);
|
|
8
11
|
r.requires("delivery");
|
|
9
12
|
|
|
10
13
|
r.useExtension("deliveryChannel", "push", {
|
|
@@ -27,6 +27,9 @@ export {
|
|
|
27
27
|
// Begruendung in schema/profile-selection.ts.
|
|
28
28
|
export function createComplianceProfilesFeature(): FeatureDefinition {
|
|
29
29
|
return defineFeature("compliance-profiles", (r) => {
|
|
30
|
+
r.describe(
|
|
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
|
+
);
|
|
30
33
|
// Standalone — kein r.requires noetig: tenantId kommt aus dem User-
|
|
31
34
|
// Context, Profile-Selection ist eigene Entity, sub-processor-Liste
|
|
32
35
|
// sind Constants. Wenn S1.4+ Cross-Feature-Reads dazukommen, kommt
|
package/src/config/feature.ts
CHANGED
|
@@ -24,6 +24,9 @@ export type ConfigContext = { readonly config: ConfigAccessor };
|
|
|
24
24
|
|
|
25
25
|
export function createConfigFeature(): FeatureDefinition {
|
|
26
26
|
return defineFeature("config", (r) => {
|
|
27
|
+
r.describe(
|
|
28
|
+
"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.",
|
|
29
|
+
);
|
|
27
30
|
r.systemScope();
|
|
28
31
|
|
|
29
32
|
// One aggregate stream per (key, scope) pair — the executor handles the
|
|
@@ -84,6 +84,9 @@ function registerCustomFields(
|
|
|
84
84
|
r: FeatureRegistrar<typeof CUSTOM_FIELDS_FEATURE_NAME>,
|
|
85
85
|
defineTenantHandler: WriteHandlerDef,
|
|
86
86
|
) {
|
|
87
|
+
r.describe(
|
|
88
|
+
"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`, `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.",
|
|
89
|
+
);
|
|
87
90
|
r.entity("field-definition", fieldDefinitionEntity);
|
|
88
91
|
|
|
89
92
|
// Event-types — qualified als "custom-fields:event:<short-name>".
|
|
@@ -44,6 +44,9 @@ export {
|
|
|
44
44
|
// pflicht. Das Wiring kommt in Sprint 2.U5.
|
|
45
45
|
export function createDataRetentionFeature(): FeatureDefinition {
|
|
46
46
|
return defineFeature("data-retention", (r) => {
|
|
47
|
+
r.describe(
|
|
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
|
+
);
|
|
47
50
|
r.entity("tenant-retention-override", tenantRetentionOverrideEntity);
|
|
48
51
|
|
|
49
52
|
// S2.D3: Cross-Feature-API fuer Forget-Flow + Cleanup-Job
|
package/src/delivery/feature.ts
CHANGED
|
@@ -14,6 +14,9 @@ import {
|
|
|
14
14
|
|
|
15
15
|
export function createDeliveryFeature(): FeatureDefinition {
|
|
16
16
|
return defineFeature("delivery", (r) => {
|
|
17
|
+
r.describe(
|
|
18
|
+
"The notification dispatch core: call `ctx.notify(notificationType, { to, route, data, priority, idempotencyKey })` from any handler to fan out a notification across all registered channels (email, in-app, push). It stores per-user channel preferences in the `notification-preference` entity, logs every attempt to `read_delivery_attempts`, and enforces idempotency and rate-limiting \u2014 add `channel-email`, `channel-in-app`, or `channel-push` on top to actually send anything.",
|
|
19
|
+
);
|
|
17
20
|
r.systemScope();
|
|
18
21
|
r.entity("notification-preference", notificationPreferenceEntity);
|
|
19
22
|
r.unmanagedTable(deliveryAttemptsTableMeta, {
|
|
@@ -37,6 +37,9 @@ export function createFeatureTogglesFeature(
|
|
|
37
37
|
options: FeatureTogglesOptions = {},
|
|
38
38
|
): FeatureDefinition {
|
|
39
39
|
return defineFeature("feature-toggles", (r) => {
|
|
40
|
+
r.describe(
|
|
41
|
+
'Persists per-feature enabled/disabled state in the `read_global_feature_state` table and exposes a `set` write-handler plus `list`/`registered` query-handlers so operators can flip features at runtime without redeploying. Each API instance keeps an in-memory `GlobalFeatureToggleRuntime` snapshot (initialize it via `createFeatureToggleRuntime`, pass a `() => runtime` accessor to `createFeatureTogglesFeature`) that the dispatcher gate reads on every request; a `toggle-cache-sync` multi-stream projection with `delivery: "per-instance"` syncs the snapshot across instances whenever a `toggle-set` event is appended.',
|
|
42
|
+
);
|
|
40
43
|
r.systemScope();
|
|
41
44
|
|
|
42
45
|
// Toggle-change domain event. The event ends up in the events-table
|
|
@@ -90,6 +90,9 @@ export type FileProviderPlugin = {
|
|
|
90
90
|
// =============================================================================
|
|
91
91
|
|
|
92
92
|
export const fileFoundationFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
93
|
+
r.describe(
|
|
94
|
+
"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.",
|
|
95
|
+
);
|
|
93
96
|
r.requires("config");
|
|
94
97
|
|
|
95
98
|
r.extendsRegistrar("fileProvider", {
|
|
@@ -60,6 +60,9 @@ export function clearStorage(tenantId: string): void {
|
|
|
60
60
|
// =============================================================================
|
|
61
61
|
|
|
62
62
|
export const fileProviderInMemoryFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
63
|
+
r.describe(
|
|
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
|
+
);
|
|
63
66
|
// Kein r.requires("config") + kein r.requires("secrets") — der
|
|
64
67
|
// In-Memory-Provider hat keine Config + kein Secret. Nur die
|
|
65
68
|
// file-foundation muss da sein (Plugin-extension-point).
|
|
@@ -38,6 +38,9 @@ const FEATURE_NAME = "file-provider-s3";
|
|
|
38
38
|
// =============================================================================
|
|
39
39
|
|
|
40
40
|
export const fileProviderS3Feature = defineFeature(FEATURE_NAME, (r) => {
|
|
41
|
+
r.describe(
|
|
42
|
+
'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.',
|
|
43
|
+
);
|
|
41
44
|
r.requires("config");
|
|
42
45
|
r.requires("secrets");
|
|
43
46
|
r.requires("file-foundation");
|
package/src/jobs/feature.ts
CHANGED
|
@@ -23,6 +23,9 @@ import { jobRunLogsTable, jobRunLogsTableMeta, jobRunsTable } from "./job-run-ta
|
|
|
23
23
|
|
|
24
24
|
export function createJobsFeature(): FeatureDefinition {
|
|
25
25
|
return defineFeature("jobs", (r) => {
|
|
26
|
+
r.describe(
|
|
27
|
+
"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.",
|
|
28
|
+
);
|
|
26
29
|
r.systemScope();
|
|
27
30
|
r.unmanagedTable(jobRunLogsTableMeta, {
|
|
28
31
|
reason: "read_side.job_run_logs",
|
|
@@ -60,6 +60,9 @@ export type LegalPagesOptions = {
|
|
|
60
60
|
export function createLegalPagesFeature(opts: LegalPagesOptions = {}): FeatureDefinition {
|
|
61
61
|
const wrapLayout = opts.wrapLayout ?? wrapInLayout;
|
|
62
62
|
return defineFeature("legal-pages", (r) => {
|
|
63
|
+
r.describe(
|
|
64
|
+
"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.",
|
|
65
|
+
);
|
|
63
66
|
r.requires("text-content");
|
|
64
67
|
|
|
65
68
|
// 4 Public-HTML-Routes
|
|
@@ -65,6 +65,9 @@ export type MailTransportPlugin = {
|
|
|
65
65
|
// =============================================================================
|
|
66
66
|
|
|
67
67
|
export const mailFoundationFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
68
|
+
r.describe(
|
|
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
|
+
);
|
|
68
71
|
r.requires("config");
|
|
69
72
|
|
|
70
73
|
// Plugin extension-point. Provider-features register here. The
|
|
@@ -73,6 +73,9 @@ export function clearInbox(tenantId: string): void {
|
|
|
73
73
|
// =============================================================================
|
|
74
74
|
|
|
75
75
|
export const mailTransportInMemoryFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
76
|
+
r.describe(
|
|
77
|
+
'Registers an in-process `"inmemory"` provider for `mail-foundation` that buffers sent mails per tenant instead of contacting an SMTP server. Use `getInbox(tenantId)` and `clearInbox(tenantId)` in demo apps and tests; not for production (buffer is process-memory, lost on restart).',
|
|
78
|
+
);
|
|
76
79
|
// Kein r.requires("config") + kein r.requires("secrets") — der
|
|
77
80
|
// In-Memory-Transport hat keine Config (nichts zu konfigurieren) und
|
|
78
81
|
// kein Secret. Der einzige hard-require ist mail-foundation, das den
|
|
@@ -49,6 +49,9 @@ const FEATURE_NAME = "mail-transport-smtp";
|
|
|
49
49
|
// =============================================================================
|
|
50
50
|
|
|
51
51
|
export const mailTransportSmtpFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
52
|
+
r.describe(
|
|
53
|
+
'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.',
|
|
54
|
+
);
|
|
52
55
|
r.requires("config");
|
|
53
56
|
r.requires("secrets");
|
|
54
57
|
r.requires("mail-foundation");
|
|
@@ -11,6 +11,9 @@ import { rateLimitStatus } from "./handlers/status.query";
|
|
|
11
11
|
// skip this feature entirely — the resolver still runs.
|
|
12
12
|
export function createRateLimitingFeature() {
|
|
13
13
|
return defineFeature("rate-limiting", (r) => {
|
|
14
|
+
r.describe(
|
|
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
|
+
);
|
|
14
17
|
r.queryHandler(rateLimitStatus);
|
|
15
18
|
});
|
|
16
19
|
}
|
|
@@ -11,6 +11,9 @@ import type { RendererPlugin } from "./types";
|
|
|
11
11
|
// Konsumenten holen sich Plugin runtime via createRendererForTenant.
|
|
12
12
|
export function createRendererFoundationFeature() {
|
|
13
13
|
return defineFeature("renderer-foundation", (r) => {
|
|
14
|
+
r.describe(
|
|
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
|
+
);
|
|
14
17
|
r.requires("template-resolver");
|
|
15
18
|
|
|
16
19
|
r.extendsRegistrar("renderer", {
|
|
@@ -27,6 +27,9 @@ export async function adaptToFoundation(req: RenderRequest): Promise<RenderRespo
|
|
|
27
27
|
|
|
28
28
|
export function createRendererSimpleFeature(): FeatureDefinition {
|
|
29
29
|
return defineFeature("renderer-simple", (r) => {
|
|
30
|
+
r.describe(
|
|
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
|
+
);
|
|
30
33
|
r.requires("renderer-foundation");
|
|
31
34
|
|
|
32
35
|
r.useExtension("renderer", "simple", {
|
package/src/secrets/feature.ts
CHANGED
|
@@ -82,6 +82,9 @@ export function requireSecretsContext(
|
|
|
82
82
|
|
|
83
83
|
export function createSecretsFeature(): FeatureDefinition {
|
|
84
84
|
return defineFeature("secrets", (r) => {
|
|
85
|
+
r.describe(
|
|
86
|
+
"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.",
|
|
87
|
+
);
|
|
85
88
|
r.envSchema(secretsEnvSchema);
|
|
86
89
|
|
|
87
90
|
// ES entity: set/delete go through the executor, `tenantSecret.created/
|
package/src/sessions/feature.ts
CHANGED
|
@@ -38,6 +38,9 @@ export type SessionsFeatureOptions = {
|
|
|
38
38
|
// see rows in the caller's active tenant.
|
|
39
39
|
export function createSessionsFeature(options?: SessionsFeatureOptions): FeatureDefinition {
|
|
40
40
|
return defineFeature("sessions", (r) => {
|
|
41
|
+
r.describe(
|
|
42
|
+
"Tracks signed-in clients in the `read_user_sessions` table (one row per JWT, keyed by the `sid`/`jti` claim) and exposes handlers for `mine` (list your sessions), `revoke`, and `revokeAllOthers`. Session creation and revocation on the hot auth path are handled by `createSessionCallbacks()`, wired into `buildServer({ auth: { ... } })` outside the dispatcher; the feature also ships a manual-trigger cleanup job for pruning expired rows and an optional `autoRevokeOnPasswordChange` hook that mass-revokes all sessions for a user whenever their `passwordHash` changes.",
|
|
43
|
+
);
|
|
41
44
|
r.entity("user-session", userSessionEntity);
|
|
42
45
|
|
|
43
46
|
const handlers = {
|
|
@@ -29,6 +29,9 @@ type DispatchRequestedPayload =
|
|
|
29
29
|
|
|
30
30
|
export function createStepDispatcherFeature(): FeatureDefinition {
|
|
31
31
|
return defineFeature("step-dispatcher", (r) => {
|
|
32
|
+
r.describe(
|
|
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
|
+
);
|
|
32
35
|
r.systemScope();
|
|
33
36
|
|
|
34
37
|
r.multiStreamProjection({
|
|
@@ -146,6 +146,9 @@ export function createSubscriptionMollieFeature(
|
|
|
146
146
|
);
|
|
147
147
|
|
|
148
148
|
return defineFeature(SUBSCRIPTION_MOLLIE_FEATURE, (r) => {
|
|
149
|
+
r.describe(
|
|
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
|
+
);
|
|
149
152
|
r.requires("billing-foundation");
|
|
150
153
|
r.envSchema(subscriptionMollieEnvSchema);
|
|
151
154
|
|
|
@@ -124,6 +124,9 @@ export function createSubscriptionStripeFeature(
|
|
|
124
124
|
const cancel = createStripeCancelSubscription(stripe);
|
|
125
125
|
|
|
126
126
|
return defineFeature(SUBSCRIPTION_STRIPE_FEATURE, (r) => {
|
|
127
|
+
r.describe(
|
|
128
|
+
"Stripe payment provider plugin for `billing-foundation`. Mount via `createSubscriptionStripeFeature({ webhookSecret, apiKey, priceToTier })` \u2014 a factory function that holds the app-wide credentials in a closure for use before tenant resolution. Implements all four provider methods: `verifyAndParseWebhook` (HMAC signature verification), `createCheckoutSession`, `createPortalSession`, and `cancelSubscription`. The `priceToTier` map connects Stripe price IDs to your tier names.",
|
|
129
|
+
);
|
|
127
130
|
// Hard-deps: subscription-foundation als plugin-host. KEIN
|
|
128
131
|
// `r.requires("config", "secrets")` — der Plugin nutzt weder
|
|
129
132
|
// tenant-config noch tenant-secrets (alles app-wide via factory-
|
|
@@ -17,6 +17,9 @@ import { templateResourceEntity } from "./table";
|
|
|
17
17
|
// - Cross-Feature: requireTemplateResolver(ctx, callerName) — Pattern wie requireTextContent
|
|
18
18
|
export function createTemplateResolverFeature() {
|
|
19
19
|
return defineFeature("template-resolver", (r) => {
|
|
20
|
+
r.describe(
|
|
21
|
+
"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.",
|
|
22
|
+
);
|
|
20
23
|
r.entity("template-resource", templateResourceEntity);
|
|
21
24
|
|
|
22
25
|
const handlers = {
|
package/src/tenant/feature.ts
CHANGED
|
@@ -29,6 +29,9 @@ export { tenantEntity, tenantTable } from "./schema/tenant";
|
|
|
29
29
|
|
|
30
30
|
export function createTenantFeature(): FeatureDefinition {
|
|
31
31
|
return defineFeature("tenant", (r) => {
|
|
32
|
+
r.describe(
|
|
33
|
+
"Registers the three core multi-tenancy entities \u2014 `tenant`, `tenant-membership`, and `tenant-invitation` (DB tables `read_tenants`, `read_tenant_memberships`, and `read_tenant_invitations`) \u2014 along with write handlers for create/update/disable/addMember/removeMember/updateMemberRoles and the matching queries. It also declares a set of per-tenant config keys (companyName, timezone, locale, SMTP credentials) and system-only keys (priceModel, maxUsers) via `r.config({ keys: { ... } })`. Use this feature in every multi-tenant app; membership resolution and invitation flows depend on it, and `auth-email-password` requires it.",
|
|
34
|
+
);
|
|
32
35
|
r.systemScope();
|
|
33
36
|
r.requires("config");
|
|
34
37
|
r.entity("tenant", tenantEntity);
|
|
@@ -23,6 +23,9 @@ import { textBlockEntity } from "./table";
|
|
|
23
23
|
// Target nutzt. Der Client-side TreeProvider lebt in `web/client-plugin.ts`.
|
|
24
24
|
export function createTextContentFeature() {
|
|
25
25
|
return defineFeature("text-content", (r) => {
|
|
26
|
+
r.describe(
|
|
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
|
+
);
|
|
26
29
|
r.entity("text-block", textBlockEntity);
|
|
27
30
|
|
|
28
31
|
const handlers = {
|
|
@@ -167,6 +167,9 @@ export function createTierEngineFeature<
|
|
|
167
167
|
TCaps extends Readonly<Record<string, unknown>> = Readonly<Record<string, unknown>>,
|
|
168
168
|
>(opts: CreateTierEngineOptions<TCaps> = {}): FeatureDefinition {
|
|
169
169
|
return defineFeature(TIER_ENGINE_FEATURE, (r) => {
|
|
170
|
+
r.describe(
|
|
171
|
+
"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`.",
|
|
172
|
+
);
|
|
170
173
|
r.requires("config");
|
|
171
174
|
r.requires("tenant");
|
|
172
175
|
|
package/src/user/feature.ts
CHANGED
|
@@ -12,6 +12,9 @@ import { userEntity } from "./schema/user";
|
|
|
12
12
|
// Membership + tenant-specific roles live in the tenant feature.
|
|
13
13
|
export function createUserFeature(): FeatureDefinition {
|
|
14
14
|
return defineFeature("user", (r) => {
|
|
15
|
+
r.describe(
|
|
16
|
+
"Manages the cross-tenant user identity: the `read_users` table holds each user's email, `displayName`, global `roles`, `emailVerified` flag, and lifecycle `status` (active / restricted / deletionRequested / deleted). Because users exist above any individual tenant, the feature runs with `r.systemScope()` \u2014 membership and tenant-specific roles live in the `tenant` feature instead. Add this feature whenever your app needs a persistent, tenant-agnostic user record that auth and GDPR pipelines can reference.",
|
|
17
|
+
);
|
|
15
18
|
r.systemScope();
|
|
16
19
|
r.entity("user", userEntity);
|
|
17
20
|
|
|
@@ -84,6 +84,9 @@ export type UserDataRightsOptions = {
|
|
|
84
84
|
|
|
85
85
|
export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): FeatureDefinition {
|
|
86
86
|
return defineFeature("user-data-rights", (r) => {
|
|
87
|
+
r.describe(
|
|
88
|
+
'Implements GDPR Art. 15 (access / `my-audit-log` query), Art. 17 (erasure / `request-deletion` + `cancel-deletion` + 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`.',
|
|
89
|
+
);
|
|
87
90
|
r.requires("user", "data-retention", "compliance-profiles", "sessions");
|
|
88
91
|
r.usesApi("compliance.forTenant");
|
|
89
92
|
r.usesApi("retention.policyFor");
|
|
@@ -39,6 +39,9 @@ export function createUserDataRightsDefaultsFeature(
|
|
|
39
39
|
): FeatureDefinition {
|
|
40
40
|
const fileRefDeleteHook = createFileRefDeleteHook(options.storageProvider);
|
|
41
41
|
return defineFeature("user-data-rights-defaults", (r) => {
|
|
42
|
+
r.describe(
|
|
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
|
+
);
|
|
42
45
|
r.requires("user", "files", "user-data-rights");
|
|
43
46
|
|
|
44
47
|
r.useExtension(EXT_USER_DATA, "user", {
|