@cosmicdrift/kumiko-samples 0.365.1 → 0.366.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 +1 -1
- package/packages/bundled-features/src/admin-shell/feature.ts +9 -9
- package/packages/bundled-features/src/agent-tools/changes.json +6 -0
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +87 -5
- package/packages/bundled-features/src/audit/feature.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/changes.json +13 -0
- package/packages/bundled-features/src/auth-email-password/constants.ts +0 -2
- package/packages/bundled-features/src/auth-email-password/feature.ts +3 -3
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +1 -0
- package/packages/bundled-features/src/auth-mfa/feature.ts +4 -4
- package/packages/bundled-features/src/auth-mfa-user-data/index.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +89 -28
- package/packages/bundled-features/src/cap-counter/changes.json +7 -0
- package/packages/bundled-features/src/cap-counter/constants.ts +3 -0
- package/packages/bundled-features/src/cap-counter/feature.ts +2 -2
- package/packages/bundled-features/src/cap-counter/tables.ts +5 -2
- package/packages/bundled-features/src/cap-overview/feature.ts +3 -3
- package/packages/bundled-features/src/channel-discord/feature.ts +2 -2
- package/packages/bundled-features/src/channel-email/feature.ts +1 -1
- package/packages/bundled-features/src/channel-in-app/feature.ts +1 -1
- package/packages/bundled-features/src/channel-push/feature.ts +1 -1
- package/packages/bundled-features/src/channel-slack/feature.ts +2 -2
- package/packages/bundled-features/src/channel-teams/feature.ts +2 -2
- package/packages/bundled-features/src/channel-telegram/feature.ts +2 -2
- package/packages/bundled-features/src/compliance-profiles/feature.ts +1 -1
- package/packages/bundled-features/src/compliance-profiles-ops/index.ts +3 -3
- package/packages/bundled-features/src/config/feature.ts +1 -1
- package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
- package/packages/bundled-features/src/custom-fields/feature.ts +2 -2
- package/packages/bundled-features/src/custom-fields/wire-for-entity.ts +17 -13
- package/packages/bundled-features/src/data-retention/feature.ts +1 -1
- package/packages/bundled-features/src/data-retention/presets.ts +4 -4
- package/packages/bundled-features/src/delivery/changes.json +6 -0
- package/packages/bundled-features/src/delivery/db/queries/address-opt-outs.ts +6 -2
- package/packages/bundled-features/src/delivery/db/queries/preferences.ts +4 -4
- package/packages/bundled-features/src/delivery/delivery-service.ts +17 -2
- package/packages/bundled-features/src/delivery/feature.ts +9 -5
- package/packages/bundled-features/src/delivery/handlers/resubscribe-address.write.ts +12 -8
- package/packages/bundled-features/src/delivery/handlers/resubscribe-user.write.ts +3 -1
- package/packages/bundled-features/src/delivery/handlers/set-preference.write.ts +3 -1
- package/packages/bundled-features/src/delivery/handlers/unsubscribe-address.write.ts +3 -1
- package/packages/bundled-features/src/delivery/handlers/unsubscribe-user.write.ts +3 -1
- package/packages/bundled-features/src/delivery/notification-type-aliases.ts +13 -0
- package/packages/bundled-features/src/derivatives-sharp/feature.ts +1 -1
- package/packages/bundled-features/src/document-ingest-foundation/feature.ts +2 -2
- package/packages/bundled-features/src/file-derivatives/feature.ts +3 -3
- package/packages/bundled-features/src/file-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/file-provider-inmemory/feature.ts +2 -2
- package/packages/bundled-features/src/file-provider-s3/feature.ts +3 -3
- package/packages/bundled-features/src/file-provider-s3-env/feature.ts +2 -2
- package/packages/bundled-features/src/files/README.md +1 -1
- package/packages/bundled-features/src/files-tenant-data/index.ts +1 -1
- package/packages/bundled-features/src/folders/feature.ts +2 -2
- package/packages/bundled-features/src/folders-user-data/index.ts +1 -1
- package/packages/bundled-features/src/form-draft/feature.ts +2 -2
- package/packages/bundled-features/src/form-draft-user-data/index.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +13 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +9 -1
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +19 -7
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +21 -2
- package/packages/bundled-features/src/inbound-provider-imap/feature.ts +2 -2
- package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +1 -1
- package/packages/bundled-features/src/ledger/feature.ts +5 -5
- package/packages/bundled-features/src/legal-pages/feature.ts +1 -1
- package/packages/bundled-features/src/mail-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/mail-transport-inmemory/feature.ts +2 -2
- package/packages/bundled-features/src/mail-transport-smtp/feature.ts +3 -3
- package/packages/bundled-features/src/managed-pages/css-gate.ts +1 -1
- package/packages/bundled-features/src/managed-pages/feature.ts +2 -2
- package/packages/bundled-features/src/metrics/feature.ts +2 -2
- package/packages/bundled-features/src/notes-history/feature.ts +2 -2
- package/packages/bundled-features/src/notes-history-user-data/index.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +15 -11
- package/packages/bundled-features/src/readiness/feature.ts +2 -2
- package/packages/bundled-features/src/renderer-foundation/README.md +1 -1
- package/packages/bundled-features/src/renderer-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/renderer-pdf/feature.ts +1 -1
- package/packages/bundled-features/src/secrets/feature.ts +1 -1
- package/packages/bundled-features/src/seo/feature.ts +1 -1
- package/packages/bundled-features/src/sessions/feature.ts +17 -13
- package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -1
- package/packages/bundled-features/src/subscription-mollie/feature.ts +1 -1
- package/packages/bundled-features/src/subscription-stripe/feature.ts +3 -3
- package/packages/bundled-features/src/tags/feature.ts +2 -2
- package/packages/bundled-features/src/template-resolver/feature.ts +2 -2
- package/packages/bundled-features/src/template-resolver-user-data/index.ts +1 -1
- package/packages/bundled-features/src/tenant/feature.ts +4 -4
- package/packages/bundled-features/src/tenant/seeding.ts +1 -1
- package/packages/bundled-features/src/tenant-lifecycle/feature.ts +2 -3
- package/packages/bundled-features/src/tenant-settings/feature.ts +1 -1
- package/packages/bundled-features/src/tier-engine/feature.ts +36 -27
- package/packages/bundled-features/src/user/feature.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/README.md +1 -1
- package/packages/bundled-features/src/user-data-rights/feature.ts +4 -4
- package/packages/bundled-features/src/user-data-rights-defaults/feature.ts +2 -2
- package/packages/bundled-features/src/user-profile/feature.ts +4 -4
- package/packages/bundled-features/src/waitlist/feature.ts +2 -2
- package/samples/apps/cap-billing-demo/README.md +1 -1
- package/samples/apps/cap-billing-demo/src/feature.ts +4 -4
- package/samples/apps/cap-billing-demo/src/run-config.ts +1 -1
- package/samples/apps/config-demo/src/features/demo/feature.ts +2 -2
- package/samples/apps/styleguide/src/features/ai-settings/feature.ts +43 -28
- package/samples/apps/styleguide/src/features/demo/feature.ts +18 -18
- package/samples/apps/styleguide/src/features/widgets/ai-text-demo.ts +10 -12
- package/samples/apps/styleguide/src/features/widgets/feature.ts +90 -108
- package/samples/apps/ui-walkthrough/src/features/rental/feature.ts +36 -36
- package/samples/apps/ui-walkthrough/src/features/vehicles/feature.ts +6 -6
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +2 -2
- package/samples/apps/use-all-bundled/scripts/gen-sample-index.ts +7 -7
- package/samples/apps/use-all-bundled/src/app/notes-feature.ts +3 -3
- package/samples/apps/use-all-bundled/src/app/screens-feature.ts +1 -1
- package/samples/apps/user-data-rights-demo/src/feature.ts +5 -5
- package/samples/recipes/access-control/src/feature.ts +21 -25
- package/samples/recipes/anonymous-access/src/feature.ts +36 -43
- package/samples/recipes/anonymous-access-multitenant/src/feature.ts +14 -16
- package/samples/recipes/auth-foundation-providers/src/feature.ts +1 -1
- package/samples/recipes/basic-entity/README.md +1 -1
- package/samples/recipes/boot-check/README.md +1 -1
- package/samples/recipes/boot-check/src/feature.ts +2 -2
- package/samples/recipes/chat-channels/src/feature.ts +1 -1
- package/samples/recipes/cross-feature-events/src/feature.ts +7 -7
- package/samples/recipes/currencies-global/src/feature.ts +3 -3
- package/samples/recipes/currencies-per-tenant/src/feature.ts +2 -2
- package/samples/recipes/custom-fields-basic/README.md +1 -1
- package/samples/recipes/custom-fields-basic/src/feature.ts +3 -2
- package/samples/recipes/custom-handlers/README.md +3 -3
- package/samples/recipes/custom-handlers/src/feature.ts +37 -41
- package/samples/recipes/dashboard-stats/src/feature.ts +6 -6
- package/samples/recipes/delivery-notifications/README.md +1 -1
- package/samples/recipes/delivery-notifications/src/feature.ts +8 -8
- package/samples/recipes/designer-demo/src/feature.ts +2 -1
- package/samples/recipes/embedded/src/feature.ts +1 -1
- package/samples/recipes/embedded-entity-form/src/feature.ts +13 -13
- package/samples/recipes/encrypted-tenant-config/src/feature.ts +1 -1
- package/samples/recipes/error-contract/src/feature.ts +25 -25
- package/samples/recipes/event-sourcing/src/feature.ts +70 -76
- package/samples/recipes/feature-toggles/src/feature.ts +28 -20
- package/samples/recipes/files-post-processing/src/feature.ts +22 -26
- package/samples/recipes/idempotency/src/feature.ts +16 -18
- package/samples/recipes/inbound-mail-triage/src/feature.ts +1 -1
- package/samples/recipes/lane-routing/src/feature.ts +10 -12
- package/samples/recipes/legal-pages/src/feature.ts +1 -1
- package/samples/recipes/lifecycle-hooks/README.md +4 -4
- package/samples/recipes/lifecycle-hooks/src/feature.ts +55 -35
- package/samples/recipes/managed-config/src/feature.ts +8 -8
- package/samples/recipes/managed-pages/src/feature.ts +1 -1
- package/samples/recipes/notes-history-basic/README.md +1 -1
- package/samples/recipes/notes-history-basic/src/feature.ts +2 -2
- package/samples/recipes/pdf-audit-export/src/feature.ts +7 -7
- package/samples/recipes/pipeline-basics/README.md +1 -1
- package/samples/recipes/pipeline-basics/src/feature.ts +7 -7
- package/samples/recipes/public-share-token/src/feature.ts +1 -1
- package/samples/recipes/rate-limiting/src/feature.ts +7 -9
- package/samples/recipes/record-detail-layout/src/feature.ts +18 -18
- package/samples/recipes/reference-data/src/feature.ts +1 -1
- package/samples/recipes/relations/src/feature.ts +2 -2
- package/samples/recipes/screens-nav/src/feature.ts +1 -1
- package/samples/recipes/search/src/feature.ts +17 -19
- package/samples/recipes/security-baseline/README.md +1 -1
- package/samples/recipes/state-machine/src/feature.ts +1 -1
- package/samples/recipes/tags-basic/README.md +1 -1
- package/samples/recipes/tags-basic/src/feature.ts +2 -2
- package/samples/recipes/tenant-settings/README.md +1 -1
- package/samples/recipes/tenant-settings/src/feature.ts +1 -1
- package/samples/recipes/timezones/src/feature.ts +12 -12
- package/samples/recipes/user-data-rights/README.md +2 -2
- package/samples/recipes/user-data-rights/src/feature.ts +1 -1
- package/samples/recipes/user-profile/src/feature.ts +1 -1
- package/samples/recipes/webhook-step/src/feature.ts +1 -1
- package/samples/recipes/writeform-section/src/feature.ts +12 -12
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.366.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Inbound mail fetch can skip already-seen message ids",
|
|
6
|
+
"detail": "InboundMailProviderPlugin.fetch receives an optional filterUnknownMessageIds(providerMessageIds) hook. The watch supervisor implements it with a tenant- and account-scoped lookup of already-seen provider message ids and returns the ids not seen yet, so a plugin can avoid downloading messages it has already processed."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.366.0",
|
|
10
|
+
"type": "breaking",
|
|
11
|
+
"title": "InboundMailContext exposes metricsFor to provider plugins",
|
|
12
|
+
"detail": "InboundMailContext gains an optional metricsFor(featureName) returning a MetricsHandle. runProdApp, the worker app and the dev server wire it from the observability meter, so provider plugins can emit their own metrics through the same pipeline.",
|
|
13
|
+
"migration": "Code that builds SystemWireDeps itself adds metricsFor (createMetricsForFactory(meter))."
|
|
14
|
+
},
|
|
2
15
|
{
|
|
3
16
|
"version": "0.364.0",
|
|
4
17
|
"type": "fix",
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from "./oauth-access-token.js";
|
|
31
31
|
import { type OAuthStatePayload, signOAuthState, verifyOAuthState } from "./oauth-state.js";
|
|
32
32
|
import { resolveInboundProviderForKey } from "./provider-factory.js";
|
|
33
|
+
import type { InboundMailContext } from "./types.js";
|
|
33
34
|
|
|
34
35
|
const DEFAULT_STATE_TTL_MINUTES = 15;
|
|
35
36
|
const DEFAULT_CONNECT_PATH = "/api/inbound-mail/connect";
|
|
@@ -56,6 +57,8 @@ export type InboundMailConnectRoutesOptions = {
|
|
|
56
57
|
* lock and primes the access-token cache from the code exchange. Omitted
|
|
57
58
|
* → plain secret write, no cache priming. */
|
|
58
59
|
readonly oauthTokens?: OAuthAccessTokenManager;
|
|
60
|
+
/** Handed to the provider plugin's context (`InboundMailContext.metricsFor`). */
|
|
61
|
+
readonly metricsFor?: InboundMailContext["metricsFor"];
|
|
59
62
|
};
|
|
60
63
|
|
|
61
64
|
function errorJson(c: Context, status: 400 | 401 | 404 | 500 | 502, code: string, message: string) {
|
|
@@ -172,7 +175,12 @@ export function createInboundMailConnectRoutes(
|
|
|
172
175
|
`${INBOUND_MAIL_FOUNDATION_FEATURE}: no secrets context wired for the oauth callback route — the refresh token cannot be persisted`,
|
|
173
176
|
);
|
|
174
177
|
}
|
|
175
|
-
const providerCtx = {
|
|
178
|
+
const providerCtx: InboundMailContext = {
|
|
179
|
+
registry: deps.registry,
|
|
180
|
+
secrets: deps.secrets,
|
|
181
|
+
config: undefined,
|
|
182
|
+
...(options.metricsFor && { metricsFor: options.metricsFor }),
|
|
183
|
+
};
|
|
176
184
|
|
|
177
185
|
let plugin: ReturnType<typeof resolveInboundProviderForKey>;
|
|
178
186
|
try {
|
|
@@ -107,7 +107,7 @@ export const inboundMailFoundationFeature = defineFeature(INBOUND_MAIL_FOUNDATIO
|
|
|
107
107
|
recommended: false,
|
|
108
108
|
});
|
|
109
109
|
// EXT_TENANT_DATA-Hook braucht den tenant-lifecycle-Host.
|
|
110
|
-
r.requires("tenant-lifecycle");
|
|
110
|
+
r.requires({ features: ["tenant-lifecycle"] });
|
|
111
111
|
|
|
112
112
|
// 5 fine-grained domain-events (Payload-Schemas: events.ts).
|
|
113
113
|
// piiFields: "none" — address/subject/snippet are already ciphertext under the tenant subject key before append (see events.ts).
|
|
@@ -11,23 +11,28 @@
|
|
|
11
11
|
// (sichtbarer breaking change) statt ctx-cast.
|
|
12
12
|
|
|
13
13
|
import type { ConfigAccessor, Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
14
|
+
import type { MetricsHandle } from "@cosmicdrift/kumiko-framework/observability";
|
|
14
15
|
import { INBOUND_MAIL_PROVIDER_EXTENSION, type InboundMailAccountStatus } from "./constants.js";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
|
-
* Slim
|
|
18
|
+
* Slim context for provider plugins.
|
|
18
19
|
*
|
|
19
|
-
* **
|
|
20
|
-
* config — tenant
|
|
21
|
-
* registry — extension
|
|
22
|
-
* secrets — tenant
|
|
23
|
-
* _userId —
|
|
24
|
-
*
|
|
20
|
+
* **Fields:**
|
|
21
|
+
* config — tenant config reads (host/port/... of the plugins)
|
|
22
|
+
* registry — extension lookup in the factory (not plugin-internal)
|
|
23
|
+
* secrets — tenant secret reads (IMAP password, OAuth refresh token)
|
|
24
|
+
* _userId — audit identity for secret reads. Handler path: the dispatcher
|
|
25
|
+
* sets the caller user ID; worker path: system identity.
|
|
26
|
+
* metricsFor — metrics handle of a feature by name, same behavior as
|
|
27
|
+
* HandlerContext.metricsFor: unregistered metrics are a silent
|
|
28
|
+
* no-op instead of a throw.
|
|
25
29
|
*/
|
|
26
30
|
export type InboundMailContext = {
|
|
27
31
|
readonly config?: ConfigAccessor;
|
|
28
32
|
readonly registry?: Registry;
|
|
29
33
|
readonly secrets?: import("@cosmicdrift/kumiko-framework/secrets").SecretsContext;
|
|
30
34
|
readonly _userId?: string | undefined;
|
|
35
|
+
readonly metricsFor?: (featureName: string) => MetricsHandle;
|
|
31
36
|
};
|
|
32
37
|
|
|
33
38
|
/**
|
|
@@ -167,6 +172,13 @@ export type InboundMailProviderPlugin = {
|
|
|
167
172
|
* whose provider declares `oauth` (authMethod oauth/xoauth2). Absent
|
|
168
173
|
* for password accounts and providers without an oauth flow. */
|
|
169
174
|
readonly accessToken?: string;
|
|
175
|
+
/** Batch lookup of the provider message ids not yet ingested for this
|
|
176
|
+
* account (committed seen anchors, one query per call). Call it after
|
|
177
|
+
* the listing/delta page and download MIME only for the returned ids.
|
|
178
|
+
* Ingest dedup stays the correctness anchor; this only saves downloads. */
|
|
179
|
+
readonly filterUnknownMessageIds?: (
|
|
180
|
+
providerMessageIds: readonly string[],
|
|
181
|
+
) => Promise<ReadonlySet<string>>;
|
|
170
182
|
},
|
|
171
183
|
) => Promise<InboundFetchResult>;
|
|
172
184
|
/** Nur OAuth-Provider. */
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// app owner still wires the supervisor in bin/server.ts:
|
|
15
15
|
//
|
|
16
16
|
// const supervisor = createInboundMailSupervisor({
|
|
17
|
-
// providerCtx: { registry: deps.registry, secrets },
|
|
17
|
+
// providerCtx: { registry: deps.registry, secrets, metricsFor: deps.metricsFor },
|
|
18
18
|
// db,
|
|
19
19
|
// dispatchWrite: ({ handlerQn, payload, tenantId }) =>
|
|
20
20
|
// deps.dispatchSystemWrite({ handlerQn, payload, tenantId: tenantId as TenantId }),
|
|
@@ -54,7 +54,7 @@ import type { DistributedLock } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
|
54
54
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
55
55
|
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
56
56
|
import { InboundMailAccountStatuses, InboundMailFoundationHandlers } from "./constants.js";
|
|
57
|
-
import { MAIL_ACCOUNT_PII_FIELDS, syncCursorTable } from "./entities.js";
|
|
57
|
+
import { MAIL_ACCOUNT_PII_FIELDS, seenMessageTable, syncCursorTable } from "./entities.js";
|
|
58
58
|
import {
|
|
59
59
|
createOAuthAccessTokenManager,
|
|
60
60
|
type OAuthAccessTokenManager,
|
|
@@ -398,6 +398,24 @@ export function createInboundMailSupervisor(
|
|
|
398
398
|
// ---------------------------------------------------------------
|
|
399
399
|
// Poll (Reconciliation).
|
|
400
400
|
// ---------------------------------------------------------------
|
|
401
|
+
function unknownMessageIdsFilterFor(account: MailAccountRecord) {
|
|
402
|
+
return async (providerMessageIds: readonly string[]): Promise<ReadonlySet<string>> => {
|
|
403
|
+
if (providerMessageIds.length === 0) return new Set();
|
|
404
|
+
const seen = await selectMany<{ providerMessageId: string }>(
|
|
405
|
+
deps.db,
|
|
406
|
+
seenMessageTable,
|
|
407
|
+
{
|
|
408
|
+
tenantId: account.tenantId,
|
|
409
|
+
accountId: account.id,
|
|
410
|
+
providerMessageId: { in: providerMessageIds },
|
|
411
|
+
},
|
|
412
|
+
{ fields: ["providerMessageId"] },
|
|
413
|
+
);
|
|
414
|
+
const seenIds = new Set(seen.map((row) => row.providerMessageId));
|
|
415
|
+
return new Set(providerMessageIds.filter((id) => !seenIds.has(id)));
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
401
419
|
async function pollAccount(
|
|
402
420
|
account: MailAccountRecord,
|
|
403
421
|
plugin: InboundMailProviderPlugin,
|
|
@@ -411,6 +429,7 @@ export function createInboundMailSupervisor(
|
|
|
411
429
|
const result = await plugin.fetch(deps.providerCtx, account, cursor, {
|
|
412
430
|
backfillWindowDays,
|
|
413
431
|
maxMessages: budget,
|
|
432
|
+
filterUnknownMessageIds: unknownMessageIdsFilterFor(account),
|
|
414
433
|
...(accessToken !== undefined && { accessToken }),
|
|
415
434
|
});
|
|
416
435
|
await ingestBatch(account, result.messages, JSON.stringify(result.nextCursor));
|
|
@@ -281,8 +281,8 @@ export const inboundProviderImapFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
|
281
281
|
category: "notifications",
|
|
282
282
|
recommended: false,
|
|
283
283
|
});
|
|
284
|
-
r.requires("inbound-mail-foundation");
|
|
285
|
-
r.requires("secrets");
|
|
284
|
+
r.requires({ features: ["inbound-mail-foundation"] });
|
|
285
|
+
r.requires({ features: ["secrets"] });
|
|
286
286
|
r.envSchema(mailAllowedPrivateHostsEnvSchema);
|
|
287
287
|
r.useExtension(INBOUND_MAIL_PROVIDER_EXTENSION, IMAP_PROVIDER_KEY, imapInboundMailPlugin);
|
|
288
288
|
});
|
|
@@ -149,6 +149,6 @@ export const inboundProviderInMemoryFeature = defineFeature(FEATURE_NAME, (r) =>
|
|
|
149
149
|
category: "notifications",
|
|
150
150
|
recommended: false,
|
|
151
151
|
});
|
|
152
|
-
r.requires("inbound-mail-foundation");
|
|
152
|
+
r.requires({ features: ["inbound-mail-foundation"] });
|
|
153
153
|
r.useExtension("inboundMailProvider", INMEMORY_PROVIDER_KEY, inMemoryInboundMailPlugin);
|
|
154
154
|
});
|
|
@@ -54,9 +54,9 @@ function registerLedger(
|
|
|
54
54
|
|
|
55
55
|
if (toggleable !== undefined) r.toggleable(toggleable);
|
|
56
56
|
|
|
57
|
-
r.entity("account", accountEntity);
|
|
58
|
-
r.entity("transaction", transactionEntity);
|
|
59
|
-
r.entity("schedule", scheduleEntity);
|
|
57
|
+
r.entity({ name: "account", ...accountEntity });
|
|
58
|
+
r.entity({ name: "transaction", ...transactionEntity });
|
|
59
|
+
r.entity({ name: "schedule", ...scheduleEntity });
|
|
60
60
|
|
|
61
61
|
// Chart of accounts — plain CRUD, no custom logic. No delete in v1: removing an
|
|
62
62
|
// account that has postings would orphan them; a posting-aware guard lands with
|
|
@@ -126,8 +126,8 @@ function registerLedger(
|
|
|
126
126
|
"Changes a recurring booking template's description, accounts, amount, interval or date window; use it when a standing order's terms change, already confirmed periods stay booked as they were.",
|
|
127
127
|
}),
|
|
128
128
|
);
|
|
129
|
-
r.hook("preSave", "schedule:create", requireScheduleSubjectPair);
|
|
130
|
-
r.hook("preSave", "schedule:update", requireScheduleSubjectPair);
|
|
129
|
+
r.hook({ type: "preSave", target: "schedule:create", handler: requireScheduleSubjectPair });
|
|
130
|
+
r.hook({ type: "preSave", target: "schedule:update", handler: requireScheduleSubjectPair });
|
|
131
131
|
r.writeHandler(createConfirmSchedulePeriodHandler(access));
|
|
132
132
|
r.queryHandler(
|
|
133
133
|
defineEntityListHandler("schedule", scheduleEntity, {
|
|
@@ -110,7 +110,7 @@ export function createLegalPagesFeature(opts: LegalPagesOptions = {}): FeatureDe
|
|
|
110
110
|
category: "content",
|
|
111
111
|
recommended: false,
|
|
112
112
|
});
|
|
113
|
-
r.requires("template-resolver");
|
|
113
|
+
r.requires({ features: ["template-resolver"] });
|
|
114
114
|
|
|
115
115
|
// Public HTML routes (default: 4 DACH routes, configurable via opts.routes)
|
|
116
116
|
for (const route of routes) {
|
|
@@ -128,7 +128,7 @@ export const mailFoundationFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
|
128
128
|
category: "notifications",
|
|
129
129
|
recommended: false,
|
|
130
130
|
});
|
|
131
|
-
r.requires("config");
|
|
131
|
+
r.requires({ features: ["config"] });
|
|
132
132
|
|
|
133
133
|
// Plugin extension-point. Provider-features register here. The
|
|
134
134
|
// entityName at registration time becomes the value tenants pick in
|
|
@@ -80,11 +80,11 @@ export const mailTransportInMemoryFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
|
80
80
|
r.describe(
|
|
81
81
|
'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).',
|
|
82
82
|
);
|
|
83
|
-
// Kein
|
|
83
|
+
// Kein requires auf "config" oder "secrets" — der
|
|
84
84
|
// In-Memory-Transport hat keine Config (nichts zu konfigurieren) und
|
|
85
85
|
// kein Secret. Der einzige hard-require ist mail-foundation, das den
|
|
86
86
|
// extension-point "mailTransport" definiert.
|
|
87
|
-
r.requires("mail-foundation");
|
|
87
|
+
r.requires({ features: ["mail-foundation"] });
|
|
88
88
|
|
|
89
89
|
const plugin: MailTransportPlugin = {
|
|
90
90
|
build: async (_ctx: MailTransportContext, tenantId: string): Promise<EmailTransport> => {
|
|
@@ -85,9 +85,9 @@ export const mailTransportSmtpFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
|
85
85
|
category: "notifications",
|
|
86
86
|
recommended: false,
|
|
87
87
|
});
|
|
88
|
-
r.requires("config");
|
|
89
|
-
r.requires("secrets");
|
|
90
|
-
r.requires("mail-foundation");
|
|
88
|
+
r.requires({ features: ["config"] });
|
|
89
|
+
r.requires({ features: ["secrets"] });
|
|
90
|
+
r.requires({ features: ["mail-foundation"] });
|
|
91
91
|
r.envSchema(mailTransportSmtpEnvSchema);
|
|
92
92
|
|
|
93
93
|
// Provider-secret. Sensitive: redact-helper for admin-UI display.
|
|
@@ -18,7 +18,7 @@ export function createManagedPagesCssFeature(): FeatureDefinition {
|
|
|
18
18
|
r.describe(
|
|
19
19
|
'Per-tenant toggle gate for the managed-pages custom-CSS (raw tenant CSS-inject) capability. Handler-less: composing it makes `managed-pages-css` a toggleable feature defaulting OFF, so an operator/tier can grant raw CSS-inject per tenant. managed-pages reads `ctx.hasFeature("managed-pages-css")` in its branding query and emits tenant CSS only when this toggle is on AND the app passed `allowCustomCss: true`. The tenant CSS is allowlist-sanitized + scoped at render regardless; this gate is the commercial/operator control, not the safety boundary.',
|
|
20
20
|
);
|
|
21
|
-
r.requires("managed-pages");
|
|
21
|
+
r.requires({ features: ["managed-pages"] });
|
|
22
22
|
r.toggleable({ default: false });
|
|
23
23
|
});
|
|
24
24
|
}
|
|
@@ -162,8 +162,8 @@ export function createManagedPagesFeature(opts: ManagedPagesOptions): FeatureDef
|
|
|
162
162
|
category: "content",
|
|
163
163
|
recommended: false,
|
|
164
164
|
});
|
|
165
|
-
r.requires("config");
|
|
166
|
-
r.entity("page", pageEntity);
|
|
165
|
+
r.requires({ features: ["config"] });
|
|
166
|
+
r.entity({ name: "page", ...pageEntity });
|
|
167
167
|
|
|
168
168
|
// Per-tenant branding config keys (scope: tenant). Write-validated via
|
|
169
169
|
// keyDef.pattern (hex / https) — see branding.ts. read:all so the
|
|
@@ -146,7 +146,7 @@ export function createMetricsFeature(options: MetricsFeatureOptions): FeatureDef
|
|
|
146
146
|
category: "operations",
|
|
147
147
|
recommended: false,
|
|
148
148
|
});
|
|
149
|
-
for (const feature of requiredFeatures(metrics)) r.requires(feature);
|
|
149
|
+
for (const feature of requiredFeatures(metrics)) r.requires({ features: [feature] });
|
|
150
150
|
for (const metric of metrics) r.queryHandler(createTenantMetricQuery(metric, now));
|
|
151
151
|
r.translations({ keys: METRICS_I18N });
|
|
152
152
|
});
|
|
@@ -167,7 +167,7 @@ export function createSystemMetricsFeature(options: MetricsFeatureOptions): Feat
|
|
|
167
167
|
recommended: false,
|
|
168
168
|
});
|
|
169
169
|
r.systemScope();
|
|
170
|
-
for (const feature of requiredFeatures(metrics)) r.requires(feature);
|
|
170
|
+
for (const feature of requiredFeatures(metrics)) r.requires({ features: [feature] });
|
|
171
171
|
for (const metric of metrics) r.queryHandler(createSystemMetricQuery(metric, now));
|
|
172
172
|
r.translations({ keys: METRICS_I18N });
|
|
173
173
|
});
|
|
@@ -43,13 +43,13 @@ function registerNotesHistory(
|
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
const entity = createNoteEntryEntity(ownership, parents);
|
|
46
|
-
r.entity("note-entry", entity);
|
|
46
|
+
r.entity({ name: "note-entry", ...entity });
|
|
47
47
|
// No write/query handler of its own — populated only as a side effect of
|
|
48
48
|
// add-note (see handlers/add-note.write.ts), looked up by
|
|
49
49
|
// notes-history-user-data's forget cascade. Registering the entity (not
|
|
50
50
|
// just building it inline in the handler) is what makes it visible to the
|
|
51
51
|
// registry-wide GDPR boot guards and to executor.ts's table/projection setup.
|
|
52
|
-
r.entity("note-mention", noteMentionEntity);
|
|
52
|
+
r.entity({ name: "note-mention", ...noteMentionEntity });
|
|
53
53
|
|
|
54
54
|
r.writeHandler(createAddNoteHandler(access, createNoteEntryExecutor(ownership)));
|
|
55
55
|
r.queryHandler(
|
|
@@ -23,7 +23,7 @@ export const notesHistoryUserDataFeature = defineFeature("notes-history-user-dat
|
|
|
23
23
|
// is OPTIONAL: if it's mounted toggleable(default=false), a hard r.requires
|
|
24
24
|
// would throw an "effectively disabled" boot warning even though the
|
|
25
25
|
// note-entry entity exists and the hooks work fine.
|
|
26
|
-
r.requires("user-data-rights");
|
|
26
|
+
r.requires({ features: ["user-data-rights"] });
|
|
27
27
|
r.optionalRequires("notes-history");
|
|
28
28
|
r.useExtension(EXT_USER_DATA, "note-entry", {
|
|
29
29
|
export: noteEntryExportHook,
|
|
@@ -106,7 +106,7 @@ export function createPersonalAccessTokensFeature(
|
|
|
106
106
|
// roles — make both boot-time deps so a mis-wiring fails validateBoot.
|
|
107
107
|
// auth-foundation owns EXT_TOKEN_VERIFIER, which the useExtension below
|
|
108
108
|
// registers against.
|
|
109
|
-
r.requires("user", "tenant", "auth-foundation");
|
|
109
|
+
r.requires({ features: ["user", "tenant", "auth-foundation"] });
|
|
110
110
|
// Self-registers as a bearer-auth provider instead of the app wiring a
|
|
111
111
|
// dedicated patResolver callback — the middleware finds it generically
|
|
112
112
|
// via resolveTokenVerifier(), shape-matched by the PAT token prefix.
|
|
@@ -142,16 +142,20 @@ export function createPersonalAccessTokensFeature(
|
|
|
142
142
|
// #497), so a postSave hook on "user" gets a poisoned ctx.db here. The
|
|
143
143
|
// concrete revoker is bound once run{Prod,Dev}App has a real db handle.
|
|
144
144
|
let autoRevokeOnPasswordChange = options.autoRevokeOnPasswordChange;
|
|
145
|
-
r.hook(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return;
|
|
153
|
-
|
|
154
|
-
|
|
145
|
+
r.hook({
|
|
146
|
+
type: "postSave",
|
|
147
|
+
target: { allOf: "user" },
|
|
148
|
+
handler: async (result) => {
|
|
149
|
+
// skip: nothing bound — same late-bind pattern as sessions/feature.ts
|
|
150
|
+
if (!autoRevokeOnPasswordChange) return;
|
|
151
|
+
// skip: brand-new user, no PATs can exist yet
|
|
152
|
+
if (result.isNew) return;
|
|
153
|
+
// skip: handler didn't touch any revoke-triggering field
|
|
154
|
+
if (!PAT_REVOKE_TRIGGERING_FIELDS.some((field) => result.changes[field] !== undefined)) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
await autoRevokeOnPasswordChange(String(result.id));
|
|
158
|
+
},
|
|
155
159
|
});
|
|
156
160
|
const bindAutoRevokeOnPasswordChange: BindPatAutoRevokeOnPasswordChange = (revoker) => {
|
|
157
161
|
// explicit constructor option wins over the runtime binding
|
|
@@ -20,8 +20,8 @@ export const readinessFeature = defineFeature("readiness", (r) => {
|
|
|
20
20
|
category: "operations",
|
|
21
21
|
recommended: false,
|
|
22
22
|
});
|
|
23
|
-
r.requires("config");
|
|
24
|
-
r.requires("secrets");
|
|
23
|
+
r.requires({ features: ["config"] });
|
|
24
|
+
r.requires({ features: ["secrets"] });
|
|
25
25
|
|
|
26
26
|
const queries = {
|
|
27
27
|
status: r.queryHandler(statusQuery),
|
|
@@ -66,7 +66,7 @@ Mails einer `r.notification` rendert die Delivery selbst: `template` + `meta` an
|
|
|
66
66
|
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
67
67
|
|
|
68
68
|
export const myRendererFeature = defineFeature("renderer-myown", (r) => {
|
|
69
|
-
r.requires("renderer-foundation");
|
|
69
|
+
r.requires({ features: ["renderer-foundation"] });
|
|
70
70
|
r.useExtension("renderer", "myown", {
|
|
71
71
|
kinds: ["document-pdf"],
|
|
72
72
|
// ctx ist optional — nur nehmen wenn Service-Access nötig
|
|
@@ -20,7 +20,7 @@ export function createRendererFoundationFeature() {
|
|
|
20
20
|
category: "notifications",
|
|
21
21
|
recommended: false,
|
|
22
22
|
});
|
|
23
|
-
r.requires("template-resolver");
|
|
23
|
+
r.requires({ features: ["template-resolver"] });
|
|
24
24
|
|
|
25
25
|
r.extendsRegistrar(RENDERER_EXTENSION, {
|
|
26
26
|
onRegister: () => {
|
|
@@ -44,7 +44,7 @@ export function createRendererPdfFeature(): FeatureDefinition {
|
|
|
44
44
|
category: "notifications",
|
|
45
45
|
recommended: false,
|
|
46
46
|
});
|
|
47
|
-
r.requires("renderer-foundation");
|
|
47
|
+
r.requires({ features: ["renderer-foundation"] });
|
|
48
48
|
|
|
49
49
|
r.useExtension(RENDERER_EXTENSION, "pdf", {
|
|
50
50
|
kinds: ["document-pdf"] as const,
|
|
@@ -159,7 +159,7 @@ export function createSecretsFeature(opts: SecretsFeatureOptions = {}): FeatureD
|
|
|
159
159
|
// uniqueIndex are not expressible via the field-DSL (jsonb-without-default,
|
|
160
160
|
// now()-default), so the physical table is the DDL truth. Without this the
|
|
161
161
|
// generated migration omitted those columns → prod-500 (publicstatus#116).
|
|
162
|
-
r.entity("tenant-secret", tenantSecretEntity,
|
|
162
|
+
r.entity({ name: "tenant-secret", ...tenantSecretEntity, backingTable: tenantSecretsTable });
|
|
163
163
|
|
|
164
164
|
// Read-audit domain-event. Registered here so ops tools + MSPs can
|
|
165
165
|
// discover the type; secrets-context.get parses payloads against
|
|
@@ -179,7 +179,7 @@ export function createSeoFeature(opts: SeoOptions): FeatureDefinition {
|
|
|
179
179
|
// config keys need the config feature's write/read machinery to actually
|
|
180
180
|
// function (same hard dependency managed-pages has) — only legal-pages/
|
|
181
181
|
// managed-pages are genuinely optional (merged only when opted in).
|
|
182
|
-
r.requires("config");
|
|
182
|
+
r.requires({ features: ["config"] });
|
|
183
183
|
r.optionalRequires("legal-pages", "managed-pages");
|
|
184
184
|
|
|
185
185
|
r.config({ keys: SEO_CONFIG_KEYS });
|
|
@@ -113,7 +113,7 @@ export function createSessionsFeature(options?: SessionsFeatureOptions): Feature
|
|
|
113
113
|
// sessions-without-user wiring fails validateBoot instead of 500ing live.
|
|
114
114
|
// auth-foundation owns EXT_SESSION_STORE, which the useExtension below
|
|
115
115
|
// registers against.
|
|
116
|
-
r.requires("user", "auth-foundation");
|
|
116
|
+
r.requires({ features: ["user", "auth-foundation"] });
|
|
117
117
|
// store_user_sessions is a hot-path direct-write store: sessionCreator
|
|
118
118
|
// inserts and the revoke handlers update rows WITHOUT emitting lifecycle
|
|
119
119
|
// events (the row columns ARE the audit trail). Registering it as
|
|
@@ -212,18 +212,22 @@ export function createSessionsFeature(options?: SessionsFeatureOptions): Feature
|
|
|
212
212
|
// regardless.
|
|
213
213
|
const REVOKE_TRIGGERING_FIELDS = ["passwordHash"] as const;
|
|
214
214
|
let autoRevoke = options?.autoRevokeOnPasswordChange;
|
|
215
|
-
r.hook(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
215
|
+
r.hook({
|
|
216
|
+
type: "postSave",
|
|
217
|
+
target: { allOf: "user" },
|
|
218
|
+
handler: async (ctx) => {
|
|
219
|
+
// skip: nothing bound — stateless-JWT deployments without a runtime
|
|
220
|
+
// that calls bindAutoRevokeOnPasswordChange keep the old behavior.
|
|
221
|
+
if (!autoRevoke) return;
|
|
222
|
+
// skip: brand-new user, no sessions can possibly exist yet. The
|
|
223
|
+
// initial passwordHash on a user:create would trip the second guard
|
|
224
|
+
// otherwise — every registration would do a mass-revoke roundtrip
|
|
225
|
+
// for a user who literally has no rows in user_sessions.
|
|
226
|
+
if (ctx.isNew) return;
|
|
227
|
+
// skip: handler didn't touch any revoke-triggering field
|
|
228
|
+
if (!REVOKE_TRIGGERING_FIELDS.some((field) => ctx.changes[field] !== undefined)) return;
|
|
229
|
+
await autoRevoke(String(ctx.id));
|
|
230
|
+
},
|
|
227
231
|
});
|
|
228
232
|
|
|
229
233
|
const bindAutoRevokeOnPasswordChange: BindAutoRevokeOnPasswordChange = (revoker) => {
|
|
@@ -64,7 +64,7 @@ export function createStepDispatcherFeature(): FeatureDefinition {
|
|
|
64
64
|
recommended: false,
|
|
65
65
|
});
|
|
66
66
|
r.envSchema(stepDispatcherEnvSchema);
|
|
67
|
-
r.requires("secrets");
|
|
67
|
+
r.requires({ features: ["secrets"] });
|
|
68
68
|
|
|
69
69
|
// Literal name: the few-shot corpus parser only reads string-literal job names.
|
|
70
70
|
// StepDispatcherJobNames.deliver mirrors it; feature.boot.test.ts pins the two together.
|
|
@@ -154,7 +154,7 @@ export function createSubscriptionMollieFeature(
|
|
|
154
154
|
category: "billing",
|
|
155
155
|
recommended: false,
|
|
156
156
|
});
|
|
157
|
-
r.requires("billing-foundation");
|
|
157
|
+
r.requires({ features: ["billing-foundation"] });
|
|
158
158
|
r.envSchema(subscriptionMollieEnvSchema);
|
|
159
159
|
|
|
160
160
|
const plugin: SubscriptionProviderPlugin = {
|
|
@@ -155,9 +155,9 @@ export function createSubscriptionStripeFeature(
|
|
|
155
155
|
// Hard-deps: billing-foundation (plugin-host) + config (billing-live +
|
|
156
156
|
// backing:"secrets" credentials) + secrets (the store the backing:secrets
|
|
157
157
|
// dispatch reads/writes + its tenant_secrets table).
|
|
158
|
-
r.requires("billing-foundation");
|
|
159
|
-
r.requires("config");
|
|
160
|
-
r.requires("secrets");
|
|
158
|
+
r.requires({ features: ["billing-foundation"] });
|
|
159
|
+
r.requires({ features: ["config"] });
|
|
160
|
+
r.requires({ features: ["secrets"] });
|
|
161
161
|
r.envSchema(subscriptionStripeEnvSchema);
|
|
162
162
|
|
|
163
163
|
// Runtime config. api-key + webhook-secret declare backing:"secrets" —
|
|
@@ -68,8 +68,8 @@ function registerTags(
|
|
|
68
68
|
if (toggleable !== undefined) r.toggleable(toggleable);
|
|
69
69
|
|
|
70
70
|
const tagAssignmentEntity = createTagAssignmentEntity(ownership, parents);
|
|
71
|
-
r.entity("tag", tagEntity);
|
|
72
|
-
r.entity("tag-assignment", tagAssignmentEntity);
|
|
71
|
+
r.entity({ name: "tag", ...tagEntity });
|
|
72
|
+
r.entity({ name: "tag-assignment", ...tagAssignmentEntity });
|
|
73
73
|
|
|
74
74
|
r.writeHandler(createCreateTagHandler(access));
|
|
75
75
|
r.writeHandler(createUpdateTagHandler(access));
|
|
@@ -99,12 +99,12 @@ export function createTemplateResolverFeature(opts: TemplateResolverOptions = {}
|
|
|
99
99
|
resourceMaxBytes: createSystemConfig("number", { default: 2_097_152, bounds: { min: 1 } }),
|
|
100
100
|
},
|
|
101
101
|
});
|
|
102
|
-
r.entity("template-resource", templateResourceEntity);
|
|
102
|
+
r.entity({ name: "template-resource", ...templateResourceEntity });
|
|
103
103
|
// Only when the app actually mounts a user-owned collection: the entity's
|
|
104
104
|
// `content` is `userOwned`, which makes it subject data — the boot guard
|
|
105
105
|
// then requires an EXT_USER_DATA hook (mount `template-resolver-user-data`).
|
|
106
106
|
// Registering it unconditionally would impose that on every app.
|
|
107
|
-
if (hasUserOwned) r.entity("user-content-entry", userContentEntryEntity);
|
|
107
|
+
if (hasUserOwned) r.entity({ name: "user-content-entry", ...userContentEntryEntity });
|
|
108
108
|
|
|
109
109
|
const handlers = {
|
|
110
110
|
upsertSystem: r.writeHandler(upsertSystemWrite),
|
|
@@ -16,7 +16,7 @@ export const templateResolverUserDataFeature = defineFeature("template-resolver-
|
|
|
16
16
|
// template-resolver is optional for the same reason as in
|
|
17
17
|
// notes-history-user-data: a toggleable mount would otherwise trip the
|
|
18
18
|
// "effectively disabled" boot warning even though the entity is there.
|
|
19
|
-
r.requires("user-data-rights");
|
|
19
|
+
r.requires({ features: ["user-data-rights"] });
|
|
20
20
|
r.optionalRequires("template-resolver");
|
|
21
21
|
r.useExtension(EXT_USER_DATA, "user-content-entry", {
|
|
22
22
|
export: userContentExportHook,
|
|
@@ -80,7 +80,7 @@ export function createTenantFeature(options?: TenantFeatureOptions): FeatureDefi
|
|
|
80
80
|
recommended: true,
|
|
81
81
|
});
|
|
82
82
|
r.systemScope();
|
|
83
|
-
r.requires("config");
|
|
83
|
+
r.requires({ features: ["config"] });
|
|
84
84
|
r.extendsRegistrar(EXT_ASSIGNABLE_ROLE, {});
|
|
85
85
|
// The elevation guards read the registry, the screens only these options: a role the
|
|
86
86
|
// server accepts but the screen omits is silently stripped on the next member-roles save.
|
|
@@ -92,9 +92,9 @@ export function createTenantFeature(options?: TenantFeatureOptions): FeatureDefi
|
|
|
92
92
|
);
|
|
93
93
|
}
|
|
94
94
|
});
|
|
95
|
-
r.entity("tenant", tenantEntity);
|
|
96
|
-
r.entity("tenant-membership", tenantMembershipEntity);
|
|
97
|
-
r.entity("tenant-invitation", tenantInvitationEntity);
|
|
95
|
+
r.entity({ name: "tenant", ...tenantEntity });
|
|
96
|
+
r.entity({ name: "tenant-membership", ...tenantMembershipEntity });
|
|
97
|
+
r.entity({ name: "tenant-invitation", ...tenantInvitationEntity });
|
|
98
98
|
|
|
99
99
|
r.config({
|
|
100
100
|
keys: {
|
|
@@ -292,7 +292,7 @@ export async function seedTenant(
|
|
|
292
292
|
|
|
293
293
|
// "tenant:seed" matches no handler-scoped hook, only entity-scoped ones
|
|
294
294
|
// (keyed by result.entityName === "tenant") — that's exactly what
|
|
295
|
-
// tier-engine's `r.hook("postSave", { allOf: "tenant" }, ...)` needs.
|
|
295
|
+
// tier-engine's `r.hook({ type: "postSave", target: { allOf: "tenant" }, ... })` needs.
|
|
296
296
|
await fireEntityPostSave(hooks, "tenant:seed", result.data, { tenantId: options.id, db });
|
|
297
297
|
|
|
298
298
|
return { id: options.id };
|
|
@@ -63,7 +63,7 @@ export function createTenantLifecycleFeature(): FeatureDefinition {
|
|
|
63
63
|
category: "compliance",
|
|
64
64
|
recommended: false,
|
|
65
65
|
});
|
|
66
|
-
r.requires("tenant", "compliance-profiles");
|
|
66
|
+
r.requires({ features: ["tenant", "compliance-profiles"] });
|
|
67
67
|
r.optionalRequires("sessions");
|
|
68
68
|
r.usesApi("compliance.forTenant");
|
|
69
69
|
|
|
@@ -140,8 +140,7 @@ export function createTenantLifecycleFeature(): FeatureDefinition {
|
|
|
140
140
|
if (!ctx.registry) {
|
|
141
141
|
throw new Error("run-tenant-destruction: ctx.registry required (JobContext incomplete)");
|
|
142
142
|
}
|
|
143
|
-
const
|
|
144
|
-
const now = T.Now.instant();
|
|
143
|
+
const now = ctx.tz.now();
|
|
145
144
|
// Without crossTenantReads (undeclared hatch) fall through to the sweep.
|
|
146
145
|
if (ctx.crossTenantReads && !(await hasTenantDestructionWork(ctx.crossTenantReads, now))) {
|
|
147
146
|
// skip: no tenant is due, so the sweep would not touch anything
|
|
@@ -22,7 +22,7 @@ export function createTenantSettingsFeature(
|
|
|
22
22
|
opts: TenantSettingsFeatureOptions = {},
|
|
23
23
|
): FeatureDefinition {
|
|
24
24
|
return defineFeature(TENANT_SETTINGS_FEATURE_NAME, (r) => {
|
|
25
|
-
r.requires("config");
|
|
25
|
+
r.requires({ features: ["config"] });
|
|
26
26
|
r.describe(
|
|
27
27
|
"Per-tenant Currency + Locale defaults, exposed as two config keys (tenant-settings:config:currency, tenant-settings:config:locale) that surface in the self-populating Settings-Hub via `mask`. Pair with defineCreateWithTenantDefaults to auto-fill a money field's currency or a locale field on create instead of hard-coding a literal per entity.",
|
|
28
28
|
);
|