@cosmicdrift/kumiko-samples 0.365.1 → 0.367.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/changes.json +6 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +12 -2
- package/packages/bundled-features/src/channel-email/feature.ts +2 -2
- 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 +18 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +1 -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 +71 -4
- package/packages/bundled-features/src/delivery/feature.ts +10 -6
- 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 +13 -6
- package/packages/bundled-features/src/delivery/handlers/unsubscribe-user.write.ts +3 -1
- package/packages/bundled-features/src/delivery/index.ts +6 -0
- package/packages/bundled-features/src/delivery/notification-type-aliases.ts +13 -0
- package/packages/bundled-features/src/delivery/testing.ts +2 -0
- package/packages/bundled-features/src/delivery/types.ts +4 -0
- package/packages/bundled-features/src/delivery/unsubscribe-link.ts +73 -0
- package/packages/bundled-features/src/delivery/unsubscribe-page-strings.ts +17 -0
- package/packages/bundled-features/src/delivery/unsubscribe.ts +94 -31
- 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 +51 -10
- package/packages/bundled-features/src/mail-foundation/index.ts +3 -0
- 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/changes.json +25 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +61 -9
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +19 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +33 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +2 -2
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +9 -1
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +8 -2
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +8 -1
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +6 -1
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +7 -2
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +21 -1
- package/packages/bundled-features/src/template-resolver/index.ts +4 -0
- package/packages/bundled-features/src/template-resolver/qualified-names.ts +1 -0
- package/packages/bundled-features/src/template-resolver/seeding.ts +54 -7
- package/packages/bundled-features/src/template-resolver/template-preview.ts +2 -0
- package/packages/bundled-features/src/template-resolver/template-system-only.ts +21 -0
- package/packages/bundled-features/src/template-resolver/template-validation.ts +48 -24
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +2 -0
- 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/changes.json +6 -0
- 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/app/seed-templates.ts +2 -0
- package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +12 -0
- package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +7 -0
- package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +2 -0
- 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/feature-manifest.json +4 -4
- package/samples/apps/use-all-bundled/package.json +1 -0
- 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
|
@@ -46,10 +46,10 @@ function cannedText(payload: RunInput): string {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export const aiTextDemoFeature = defineFeature("ai-text", (r) => {
|
|
49
|
-
r.queryHandler(
|
|
50
|
-
"run",
|
|
51
|
-
runInput,
|
|
52
|
-
async ({ payload }) => {
|
|
49
|
+
r.queryHandler({
|
|
50
|
+
name: "run",
|
|
51
|
+
schema: runInput,
|
|
52
|
+
handler: async ({ payload }) => {
|
|
53
53
|
const text = cannedText(payload);
|
|
54
54
|
return {
|
|
55
55
|
type: "text" as const,
|
|
@@ -57,14 +57,12 @@ export const aiTextDemoFeature = defineFeature("ai-text", (r) => {
|
|
|
57
57
|
usage: { inputTokens: payload.text.length, outputTokens: text.length },
|
|
58
58
|
};
|
|
59
59
|
},
|
|
60
|
-
{
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"side effects or per-user data; any signed-in user may query it",
|
|
66
|
-
},
|
|
60
|
+
access: {
|
|
61
|
+
openToAll: {
|
|
62
|
+
reason:
|
|
63
|
+
"demo stand-in for a real LLM call: returns canned text transforms with no " +
|
|
64
|
+
"side effects or per-user data; any signed-in user may query it",
|
|
67
65
|
},
|
|
68
66
|
},
|
|
69
|
-
);
|
|
67
|
+
});
|
|
70
68
|
});
|
|
@@ -198,10 +198,10 @@ export const widgetsFeature = defineFeature("widgets", (r) => {
|
|
|
198
198
|
],
|
|
199
199
|
});
|
|
200
200
|
|
|
201
|
-
r.queryHandler(
|
|
202
|
-
"metrics:portfolio-stat",
|
|
203
|
-
z.object({ region: z.string().optional() }),
|
|
204
|
-
async ({ payload: { region } }, ctx) => ({
|
|
201
|
+
r.queryHandler({
|
|
202
|
+
name: "metrics:portfolio-stat",
|
|
203
|
+
schema: z.object({ region: z.string().optional() }),
|
|
204
|
+
handler: async ({ payload: { region } }, ctx) => ({
|
|
205
205
|
value:
|
|
206
206
|
region === "us"
|
|
207
207
|
? demoMoney(38120, "USD", ctx.locale)
|
|
@@ -212,86 +212,76 @@ export const widgetsFeature = defineFeature("widgets", (r) => {
|
|
|
212
212
|
deltaDirection: "up",
|
|
213
213
|
deltaTone: "positive",
|
|
214
214
|
}),
|
|
215
|
-
{
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
"tenant scoping; any signed-in user may query it",
|
|
221
|
-
},
|
|
215
|
+
access: {
|
|
216
|
+
openToAll: {
|
|
217
|
+
reason:
|
|
218
|
+
"demo dashboard widget: returns static canned data with no per-user or " +
|
|
219
|
+
"tenant scoping; any signed-in user may query it",
|
|
222
220
|
},
|
|
223
221
|
},
|
|
224
|
-
);
|
|
225
|
-
r.queryHandler(
|
|
226
|
-
"metrics:net-worth-assets",
|
|
227
|
-
z.object({ region: z.string().optional() }),
|
|
228
|
-
async (_query, ctx) => ({ value: demoMoney(120000, "EUR", ctx.locale) }),
|
|
229
|
-
{
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
"tenant scoping; any signed-in user may query it",
|
|
235
|
-
},
|
|
222
|
+
});
|
|
223
|
+
r.queryHandler({
|
|
224
|
+
name: "metrics:net-worth-assets",
|
|
225
|
+
schema: z.object({ region: z.string().optional() }),
|
|
226
|
+
handler: async (_query, ctx) => ({ value: demoMoney(120000, "EUR", ctx.locale) }),
|
|
227
|
+
access: {
|
|
228
|
+
openToAll: {
|
|
229
|
+
reason:
|
|
230
|
+
"demo dashboard widget: returns static canned data with no per-user or " +
|
|
231
|
+
"tenant scoping; any signed-in user may query it",
|
|
236
232
|
},
|
|
237
233
|
},
|
|
238
|
-
);
|
|
239
|
-
r.queryHandler(
|
|
240
|
-
"metrics:net-worth-debts",
|
|
241
|
-
z.object({ region: z.string().optional() }),
|
|
242
|
-
async (_query, ctx) => ({ value: demoMoney(65370, "EUR", ctx.locale) }),
|
|
243
|
-
{
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
"tenant scoping; any signed-in user may query it",
|
|
249
|
-
},
|
|
234
|
+
});
|
|
235
|
+
r.queryHandler({
|
|
236
|
+
name: "metrics:net-worth-debts",
|
|
237
|
+
schema: z.object({ region: z.string().optional() }),
|
|
238
|
+
handler: async (_query, ctx) => ({ value: demoMoney(65370, "EUR", ctx.locale) }),
|
|
239
|
+
access: {
|
|
240
|
+
openToAll: {
|
|
241
|
+
reason:
|
|
242
|
+
"demo dashboard widget: returns static canned data with no per-user or " +
|
|
243
|
+
"tenant scoping; any signed-in user may query it",
|
|
250
244
|
},
|
|
251
245
|
},
|
|
252
|
-
);
|
|
253
|
-
r.queryHandler(
|
|
254
|
-
"metrics:response-times",
|
|
255
|
-
z.object({}),
|
|
256
|
-
async () => ({
|
|
246
|
+
});
|
|
247
|
+
r.queryHandler({
|
|
248
|
+
name: "metrics:response-times",
|
|
249
|
+
schema: z.object({}),
|
|
250
|
+
handler: async () => ({
|
|
257
251
|
points: RESPONSE_POINTS,
|
|
258
252
|
windowStartMs: 0,
|
|
259
253
|
windowEndMs: 24 * 60 * 60 * 1000,
|
|
260
254
|
}),
|
|
261
|
-
{
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
"tenant scoping; any signed-in user may query it",
|
|
267
|
-
},
|
|
255
|
+
access: {
|
|
256
|
+
openToAll: {
|
|
257
|
+
reason:
|
|
258
|
+
"demo dashboard widget: returns static canned data with no per-user or " +
|
|
259
|
+
"tenant scoping; any signed-in user may query it",
|
|
268
260
|
},
|
|
269
261
|
},
|
|
270
|
-
);
|
|
271
|
-
r.queryHandler(
|
|
272
|
-
"metrics:loan",
|
|
273
|
-
z.object({}),
|
|
274
|
-
async () => ({
|
|
262
|
+
});
|
|
263
|
+
r.queryHandler({
|
|
264
|
+
name: "metrics:loan",
|
|
265
|
+
schema: z.object({}),
|
|
266
|
+
handler: async () => ({
|
|
275
267
|
series: LOAN_SERIES,
|
|
276
268
|
markers: LOAN_MARKERS,
|
|
277
269
|
windowStartMs: LOAN_START_MS,
|
|
278
270
|
windowEndMs: loanMonthMs(LOAN_MONTHS),
|
|
279
271
|
todayMs: LOAN_TODAY_MS,
|
|
280
272
|
}),
|
|
281
|
-
{
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
"tenant scoping; any signed-in user may query it",
|
|
287
|
-
},
|
|
273
|
+
access: {
|
|
274
|
+
openToAll: {
|
|
275
|
+
reason:
|
|
276
|
+
"demo dashboard widget: returns static canned data with no per-user or " +
|
|
277
|
+
"tenant scoping; any signed-in user may query it",
|
|
288
278
|
},
|
|
289
279
|
},
|
|
290
|
-
);
|
|
291
|
-
r.queryHandler(
|
|
292
|
-
"metrics:latest-items",
|
|
293
|
-
z.object({}),
|
|
294
|
-
async (_query, ctx) => ({
|
|
280
|
+
});
|
|
281
|
+
r.queryHandler({
|
|
282
|
+
name: "metrics:latest-items",
|
|
283
|
+
schema: z.object({}),
|
|
284
|
+
handler: async (_query, ctx) => ({
|
|
295
285
|
rows: [
|
|
296
286
|
{
|
|
297
287
|
id: "i1",
|
|
@@ -306,25 +296,23 @@ export const widgetsFeature = defineFeature("widgets", (r) => {
|
|
|
306
296
|
],
|
|
307
297
|
nextCursor: null,
|
|
308
298
|
}),
|
|
309
|
-
{
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
"tenant scoping; any signed-in user may query it",
|
|
315
|
-
},
|
|
299
|
+
access: {
|
|
300
|
+
openToAll: {
|
|
301
|
+
reason:
|
|
302
|
+
"demo dashboard widget: returns static canned data with no per-user or " +
|
|
303
|
+
"tenant scoping; any signed-in user may query it",
|
|
316
304
|
},
|
|
317
305
|
},
|
|
318
|
-
);
|
|
319
|
-
r.queryHandler(
|
|
320
|
-
"metrics:inbox-messages",
|
|
321
|
-
z.object({
|
|
306
|
+
});
|
|
307
|
+
r.queryHandler({
|
|
308
|
+
name: "metrics:inbox-messages",
|
|
309
|
+
schema: z.object({
|
|
322
310
|
cursor: z.coerce.number().int().min(0).optional(),
|
|
323
311
|
limit: z.number().int().min(1).max(100).optional(),
|
|
324
312
|
unreadOnly: z.boolean().optional(),
|
|
325
313
|
search: z.string().optional(),
|
|
326
314
|
}),
|
|
327
|
-
async ({ payload: { cursor, limit, unreadOnly, search } }) => {
|
|
315
|
+
handler: async ({ payload: { cursor, limit, unreadOnly, search } }) => {
|
|
328
316
|
const term = search?.trim().toLowerCase() ?? "";
|
|
329
317
|
const filtered = INBOX_MESSAGES.filter(
|
|
330
318
|
(m) =>
|
|
@@ -339,20 +327,18 @@ export const widgetsFeature = defineFeature("widgets", (r) => {
|
|
|
339
327
|
const nextCursor = start + pageSize < filtered.length ? String(start + pageSize) : null;
|
|
340
328
|
return { rows, nextCursor };
|
|
341
329
|
},
|
|
342
|
-
{
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
"tenant scoping; any signed-in user may query it",
|
|
348
|
-
},
|
|
330
|
+
access: {
|
|
331
|
+
openToAll: {
|
|
332
|
+
reason:
|
|
333
|
+
"demo dashboard widget: returns static canned data with no per-user or " +
|
|
334
|
+
"tenant scoping; any signed-in user may query it",
|
|
349
335
|
},
|
|
350
336
|
},
|
|
351
|
-
);
|
|
352
|
-
r.queryHandler(
|
|
353
|
-
"metrics:upcoming-events",
|
|
354
|
-
z.object({}),
|
|
355
|
-
async (_query, ctx) => ({
|
|
337
|
+
});
|
|
338
|
+
r.queryHandler({
|
|
339
|
+
name: "metrics:upcoming-events",
|
|
340
|
+
schema: z.object({}),
|
|
341
|
+
handler: async (_query, ctx) => ({
|
|
356
342
|
rows: [
|
|
357
343
|
{
|
|
358
344
|
primary: demoText("widgets:dashboard:demo-event-rate-adjustment", ctx.locale),
|
|
@@ -364,20 +350,18 @@ export const widgetsFeature = defineFeature("widgets", (r) => {
|
|
|
364
350
|
},
|
|
365
351
|
],
|
|
366
352
|
}),
|
|
367
|
-
{
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
"tenant scoping; any signed-in user may query it",
|
|
373
|
-
},
|
|
353
|
+
access: {
|
|
354
|
+
openToAll: {
|
|
355
|
+
reason:
|
|
356
|
+
"demo dashboard widget: returns static canned data with no per-user or " +
|
|
357
|
+
"tenant scoping; any signed-in user may query it",
|
|
374
358
|
},
|
|
375
359
|
},
|
|
376
|
-
);
|
|
377
|
-
r.queryHandler(
|
|
378
|
-
"metrics:goal-progress",
|
|
379
|
-
z.object({}),
|
|
380
|
-
async (_query, ctx) => ({
|
|
360
|
+
});
|
|
361
|
+
r.queryHandler({
|
|
362
|
+
name: "metrics:goal-progress",
|
|
363
|
+
schema: z.object({}),
|
|
364
|
+
handler: async (_query, ctx) => ({
|
|
381
365
|
rows: [
|
|
382
366
|
{
|
|
383
367
|
label: demoText("widgets:dashboard:demo-goal-mortgage", ctx.locale),
|
|
@@ -391,16 +375,14 @@ export const widgetsFeature = defineFeature("widgets", (r) => {
|
|
|
391
375
|
},
|
|
392
376
|
],
|
|
393
377
|
}),
|
|
394
|
-
{
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
"tenant scoping; any signed-in user may query it",
|
|
400
|
-
},
|
|
378
|
+
access: {
|
|
379
|
+
openToAll: {
|
|
380
|
+
reason:
|
|
381
|
+
"demo dashboard widget: returns static canned data with no per-user or " +
|
|
382
|
+
"tenant scoping; any signed-in user may query it",
|
|
401
383
|
},
|
|
402
384
|
},
|
|
403
|
-
);
|
|
385
|
+
});
|
|
404
386
|
|
|
405
387
|
r.translations({ keys: WIDGETS_I18N });
|
|
406
388
|
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
SYSTEM_INVOICE_ISSUED,
|
|
33
33
|
SYSTEM_PDF_LAYOUT,
|
|
34
34
|
SYSTEM_TENANCY_CERTIFICATE,
|
|
35
|
+
SYSTEM_TERMS_CHANGED,
|
|
35
36
|
TENANT_INVOICE_OVERRIDES,
|
|
36
37
|
TENANT_MAIL_LAYOUT_VERSIONS,
|
|
37
38
|
TENANT_UTILITY_STATEMENT,
|
|
@@ -165,6 +166,7 @@ export const seedTemplates: SeedFn = async (stack) => {
|
|
|
165
166
|
await upsertSystem(SYSTEM_PDF_LAYOUT);
|
|
166
167
|
for (const mail of SYSTEM_AUTH_MAILS) await upsertSystem(mail);
|
|
167
168
|
await upsertSystem(SYSTEM_INVOICE_ISSUED);
|
|
169
|
+
await upsertSystem(SYSTEM_TERMS_CHANGED);
|
|
168
170
|
for (const document of SYSTEM_INVOICE_DOCUMENTS) await upsertSystem(document);
|
|
169
171
|
const certificate = await upsertSystem(SYSTEM_TENANCY_CERTIFICATE);
|
|
170
172
|
await stack.http.writeOk(
|
|
@@ -276,6 +276,18 @@ export const INVOICE_CORRECTED_DRAFT: SeedTemplate = {
|
|
|
276
276
|
meta: invoiceIssuedMeta("Ihre Rechnung {{invoiceNumber}} von {{tenantName}}"),
|
|
277
277
|
};
|
|
278
278
|
|
|
279
|
+
export const SYSTEM_TERMS_CHANGED: SeedTemplate = {
|
|
280
|
+
slug: "billing:notify:terms-changed",
|
|
281
|
+
kind: "mail",
|
|
282
|
+
locale: "de",
|
|
283
|
+
title: "AGB-Änderung",
|
|
284
|
+
content: [
|
|
285
|
+
`<p>Guten Tag ${v("customerName")},</p>`,
|
|
286
|
+
`<p>zum ${v("effectiveDate")} ändern sich die Allgemeinen Geschäftsbedingungen des Mieterportals.</p>`,
|
|
287
|
+
].join(""),
|
|
288
|
+
meta: { subject: "Änderung der Geschäftsbedingungen" },
|
|
289
|
+
};
|
|
290
|
+
|
|
279
291
|
export const PAYMENT_REMINDER_TEMPLATES: readonly SeedTemplate[] = [
|
|
280
292
|
{
|
|
281
293
|
slug: "billing:notify:payment-reminder",
|
|
@@ -63,10 +63,17 @@ const PAYMENT_REMINDER_VARIABLES = {
|
|
|
63
63
|
invoiceUrl: templateVariable.link({ required: true }),
|
|
64
64
|
} as const;
|
|
65
65
|
|
|
66
|
+
const TERMS_CHANGED_VARIABLES = {
|
|
67
|
+
customerName: templateVariable.text({ required: true, example: "Familie Yilmaz" }),
|
|
68
|
+
effectiveDate: templateVariable.date({ required: true, example: "2026-12-01" }),
|
|
69
|
+
} as const;
|
|
70
|
+
|
|
66
71
|
export const billingFeature = defineFeature("billing", (r) => {
|
|
67
72
|
r.translations({ keys: toKeyFirst(billingTranslations) });
|
|
68
73
|
|
|
69
74
|
r.notification("invoice-issued", { variables: INVOICE_ISSUED_VARIABLES });
|
|
70
75
|
r.notification("invoice-corrected", { variables: INVOICE_ISSUED_VARIABLES });
|
|
71
76
|
r.notification("payment-reminder", { variables: PAYMENT_REMINDER_VARIABLES });
|
|
77
|
+
// Legal wording the platform owns: tenant admins neither see nor override it.
|
|
78
|
+
r.notification("terms-changed", { variables: TERMS_CHANGED_VARIABLES, systemOnly: true });
|
|
72
79
|
});
|
|
@@ -8,11 +8,13 @@ export const billingTranslations: TranslationsByLocale = {
|
|
|
8
8
|
"billing:notify.invoice-issued.label": "Rechnung gestellt",
|
|
9
9
|
"billing:notify.invoice-corrected.label": "Rechnung korrigiert",
|
|
10
10
|
"billing:notify.payment-reminder.label": "Mahnlauf",
|
|
11
|
+
"billing:notify.terms-changed.label": "AGB-Änderung",
|
|
11
12
|
},
|
|
12
13
|
en: {
|
|
13
14
|
"billing:feature.label": "Invoices",
|
|
14
15
|
"billing:notify.invoice-issued.label": "Invoice issued",
|
|
15
16
|
"billing:notify.invoice-corrected.label": "Invoice corrected",
|
|
16
17
|
"billing:notify.payment-reminder.label": "Dunning run",
|
|
18
|
+
"billing:notify.terms-changed.label": "Terms update",
|
|
17
19
|
},
|
|
18
20
|
};
|
|
@@ -71,10 +71,10 @@ export const rentalFeature = defineFeature("rental", (r) => {
|
|
|
71
71
|
r.crud("leasePosition", leasePositionEntity, { write: open, read: open });
|
|
72
72
|
r.crud("leaseParty", leasePartyEntity, { write: open, read: open });
|
|
73
73
|
|
|
74
|
-
r.queryHandler(
|
|
75
|
-
"lease:akte",
|
|
76
|
-
idPayloadSchema,
|
|
77
|
-
async (query, ctx) => {
|
|
74
|
+
r.queryHandler({
|
|
75
|
+
name: "lease:akte",
|
|
76
|
+
schema: idPayloadSchema,
|
|
77
|
+
handler: async (query, ctx) => {
|
|
78
78
|
const row = await leaseExecutor.detail({ id: query.payload.id }, query.user, ctx.db);
|
|
79
79
|
if (!row) return failNotFound("lease", query.payload.id);
|
|
80
80
|
const lease = parseLeaseRow(row);
|
|
@@ -104,13 +104,13 @@ export const rentalFeature = defineFeature("rental", (r) => {
|
|
|
104
104
|
positionCount: await countChildren(positionExecutor),
|
|
105
105
|
};
|
|
106
106
|
},
|
|
107
|
-
open,
|
|
108
|
-
);
|
|
107
|
+
...open,
|
|
108
|
+
});
|
|
109
109
|
|
|
110
|
-
r.queryHandler(
|
|
111
|
-
"lease:positions",
|
|
112
|
-
idPayloadSchema,
|
|
113
|
-
async (query, ctx) => {
|
|
110
|
+
r.queryHandler({
|
|
111
|
+
name: "lease:positions",
|
|
112
|
+
schema: idPayloadSchema,
|
|
113
|
+
handler: async (query, ctx) => {
|
|
114
114
|
const page = await positionExecutor.list(
|
|
115
115
|
{ filter: { field: "lease", op: "eq", value: query.payload.id }, limit: 100 },
|
|
116
116
|
query.user,
|
|
@@ -132,13 +132,13 @@ export const rentalFeature = defineFeature("rental", (r) => {
|
|
|
132
132
|
}),
|
|
133
133
|
};
|
|
134
134
|
},
|
|
135
|
-
open,
|
|
136
|
-
);
|
|
135
|
+
...open,
|
|
136
|
+
});
|
|
137
137
|
|
|
138
|
-
r.queryHandler(
|
|
139
|
-
"lease:kennzahlen",
|
|
140
|
-
z.object({}),
|
|
141
|
-
async () => ({
|
|
138
|
+
r.queryHandler({
|
|
139
|
+
name: "lease:kennzahlen",
|
|
140
|
+
schema: z.object({}),
|
|
141
|
+
handler: async () => ({
|
|
142
142
|
debt: "123.456 €",
|
|
143
143
|
debtSub: "nach 10 Jahren",
|
|
144
144
|
debtDelta: "15 %",
|
|
@@ -146,13 +146,13 @@ export const rentalFeature = defineFeature("rental", (r) => {
|
|
|
146
146
|
rent: "48.200 €",
|
|
147
147
|
rentSub: "319 Verträge",
|
|
148
148
|
}),
|
|
149
|
-
open,
|
|
150
|
-
);
|
|
149
|
+
...open,
|
|
150
|
+
});
|
|
151
151
|
|
|
152
|
-
r.writeHandler(
|
|
153
|
-
"lease:terminate",
|
|
154
|
-
idPayloadSchema,
|
|
155
|
-
async (event, ctx) =>
|
|
152
|
+
r.writeHandler({
|
|
153
|
+
name: "lease:terminate",
|
|
154
|
+
schema: idPayloadSchema,
|
|
155
|
+
handler: async (event, ctx) =>
|
|
156
156
|
leaseExecutor.update(
|
|
157
157
|
{
|
|
158
158
|
id: event.payload.id,
|
|
@@ -165,13 +165,13 @@ export const rentalFeature = defineFeature("rental", (r) => {
|
|
|
165
165
|
ctx.db,
|
|
166
166
|
{ skipOptimisticLock: true },
|
|
167
167
|
),
|
|
168
|
-
open,
|
|
169
|
-
);
|
|
168
|
+
...open,
|
|
169
|
+
});
|
|
170
170
|
|
|
171
|
-
r.writeHandler(
|
|
172
|
-
"lease:record-positions",
|
|
173
|
-
recordPositionsPayloadSchema,
|
|
174
|
-
async (event, ctx) => {
|
|
171
|
+
r.writeHandler({
|
|
172
|
+
name: "lease:record-positions",
|
|
173
|
+
schema: recordPositionsPayloadSchema,
|
|
174
|
+
handler: async (event, ctx) => {
|
|
175
175
|
let created: Awaited<ReturnType<typeof positionExecutor.create>> | undefined;
|
|
176
176
|
const unitLabels = new Map<string, string | null | undefined>();
|
|
177
177
|
for (const unitId of new Set(event.payload.items.map((item) => item.unit))) {
|
|
@@ -189,13 +189,13 @@ export const rentalFeature = defineFeature("rental", (r) => {
|
|
|
189
189
|
}
|
|
190
190
|
return created ?? failNotFound("leasePosition", event.payload.lease);
|
|
191
191
|
},
|
|
192
|
-
open,
|
|
193
|
-
);
|
|
192
|
+
...open,
|
|
193
|
+
});
|
|
194
194
|
|
|
195
|
-
r.writeHandler(
|
|
196
|
-
"rent:adjust",
|
|
197
|
-
rentAdjustPayloadSchema,
|
|
198
|
-
async (event, ctx) => {
|
|
195
|
+
r.writeHandler({
|
|
196
|
+
name: "rent:adjust",
|
|
197
|
+
schema: rentAdjustPayloadSchema,
|
|
198
|
+
handler: async (event, ctx) => {
|
|
199
199
|
const { positionId, wirksamAb, neuerBetrag } = event.payload;
|
|
200
200
|
const current = await positionExecutor.detail({ id: positionId }, event.user, ctx.db);
|
|
201
201
|
if (!current) return failNotFound("leasePosition", positionId);
|
|
@@ -229,8 +229,8 @@ export const rentalFeature = defineFeature("rental", (r) => {
|
|
|
229
229
|
{ skipOptimisticLock: true },
|
|
230
230
|
);
|
|
231
231
|
},
|
|
232
|
-
open,
|
|
233
|
-
);
|
|
232
|
+
...open,
|
|
233
|
+
});
|
|
234
234
|
|
|
235
235
|
r.screen(leaseListScreen);
|
|
236
236
|
r.screen(leaseListShortScreen);
|
|
@@ -32,10 +32,10 @@ export const vehiclesFeature = defineFeature("vehicles", (r) => {
|
|
|
32
32
|
r.crud("campaign", campaignEntity, { write: open, read: open });
|
|
33
33
|
r.crud("campaignPost", campaignPostEntity, { write: open, read: open });
|
|
34
34
|
|
|
35
|
-
r.writeHandler(
|
|
36
|
-
"campaign-post:mark-posted",
|
|
37
|
-
markPostedPayloadSchema,
|
|
38
|
-
async (event, ctx) => {
|
|
35
|
+
r.writeHandler({
|
|
36
|
+
name: "campaign-post:mark-posted",
|
|
37
|
+
schema: markPostedPayloadSchema,
|
|
38
|
+
handler: async (event, ctx) => {
|
|
39
39
|
const current = await campaignPostExecutor.detail(
|
|
40
40
|
{ id: event.payload.id },
|
|
41
41
|
event.user,
|
|
@@ -62,8 +62,8 @@ export const vehiclesFeature = defineFeature("vehicles", (r) => {
|
|
|
62
62
|
{ skipOptimisticLock: true },
|
|
63
63
|
);
|
|
64
64
|
},
|
|
65
|
-
open,
|
|
66
|
-
);
|
|
65
|
+
...open,
|
|
66
|
+
});
|
|
67
67
|
|
|
68
68
|
r.screen(vehicleListScreen);
|
|
69
69
|
r.screen(vehicleEditScreen);
|
|
@@ -117,14 +117,14 @@ export const galleryProviderFeature: FeatureDefinition = defineFeature(
|
|
|
117
117
|
"gallery-mock-provider",
|
|
118
118
|
(r) => {
|
|
119
119
|
r.describe("E2E-only mock subscription provider for the consent/termination gallery.");
|
|
120
|
-
r.requires("billing-foundation");
|
|
120
|
+
r.requires({ features: ["billing-foundation"] });
|
|
121
121
|
r.useExtension("subscriptionProvider", GALLERY_PROVIDER, galleryProviderPlugin);
|
|
122
122
|
},
|
|
123
123
|
);
|
|
124
124
|
|
|
125
125
|
export const galleryScreensFeature: FeatureDefinition = defineFeature("gallery-screens", (r) => {
|
|
126
126
|
r.describe("E2E-only nav entry for the billing plans screen plus plan labels.");
|
|
127
|
-
r.requires("billing-foundation", "admin-shell");
|
|
127
|
+
r.requires({ features: ["billing-foundation", "admin-shell"] });
|
|
128
128
|
r.nav({
|
|
129
129
|
id: "billing",
|
|
130
130
|
label: "gallery:nav.billing",
|
|
@@ -407,7 +407,7 @@
|
|
|
407
407
|
},
|
|
408
408
|
{
|
|
409
409
|
"name": "channel-email",
|
|
410
|
-
"description": "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` and a `resolveEmail` function that maps a user ID to their email address. Mails render from stored templates (`renderer-mail-html`): a notification routed to this channel needs `template` + `meta` on `r.notification`, otherwise its delivery fails.",
|
|
410
|
+
"description": "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` and a `resolveEmail` function that maps a user ID to their email address. Mails render from stored templates (`renderer-mail-html`): a notification routed to this channel needs `template` + `meta` on `r.notification`, otherwise its delivery fails. Pass `tenantTransport: tenantMailTransport` (from `mail-foundation`) to send each tenant's mail through its own configured provider: a broken provider fails the send, a tenant without one uses the app `transport`. Notifications declared with `unsubscribe: true` get a signed `data.unsubscribeUrl` from delivery, which turns into the `List-Unsubscribe` headers.",
|
|
411
411
|
"toggleableDefault": null,
|
|
412
412
|
"requires": [
|
|
413
413
|
"delivery"
|
|
@@ -725,7 +725,7 @@
|
|
|
725
725
|
},
|
|
726
726
|
{
|
|
727
727
|
"name": "delivery",
|
|
728
|
-
"description": "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). Inside a write handler the delivery runs after the write has committed (the call returns `{ kind: \"deferred\" }`, a rolled-back write sends nothing); `immediate: true` delivers synchronously and returns `{ kind: \"dispatched\", deliveries }`. It stores per-user channel preferences in the `notification-preference` entity, opt-outs for no-account recipient addresses in `notification-address-opt-out` (keyed by a blind-index hash, never the plaintext address), logs every attempt to `store_delivery_attempts`, and enforces idempotency and rate-limiting — add `channel-email`, `channel-in-app`, or `channel-push` on top to actually send anything. Unsubscribe/resubscribe links are served by `createUnsubscribeRoutes({ secret })` mounted via the app's `extraRoutes` at `/api/delivery/unsubscribe` and `/api/delivery/resubscribe`: `GET /unsubscribe?token=` renders a confirmation page (no write), `POST /unsubscribe` performs the opt-out (RFC 8058 one-click, token from the form body or query), `POST /resubscribe` undoes it (JSON `{token}` or form body only, never the query — reachable by a mail-client link prefetcher) — sign links with `signUnsubscribeToken` / `signAddressUnsubscribeToken` using the same secret. `channel-email` sets `List-Unsubscribe` / `List-Unsubscribe-Post` automatically when a message's `data.unsubscribeUrl` points at the unsubscribe route.",
|
|
728
|
+
"description": "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). Inside a write handler the delivery runs after the write has committed (the call returns `{ kind: \"deferred\" }`, a rolled-back write sends nothing); `immediate: true` delivers synchronously and returns `{ kind: \"dispatched\", deliveries }`. It stores per-user channel preferences in the `notification-preference` entity, opt-outs for no-account recipient addresses in `notification-address-opt-out` (keyed by a blind-index hash, never the plaintext address), logs every attempt to `store_delivery_attempts`, and enforces idempotency and rate-limiting — add `channel-email`, `channel-in-app`, or `channel-push` on top to actually send anything. Unsubscribe/resubscribe links are served by `createUnsubscribeRoutes({ secret })` mounted via the app's `extraRoutes` at `/api/delivery/unsubscribe` and `/api/delivery/resubscribe`: `GET /unsubscribe?token=` renders a confirmation page (no write), `POST /unsubscribe` performs the opt-out (RFC 8058 one-click, token from the form body or query), `POST /resubscribe` undoes it (JSON `{token}` or form body only, never the query — reachable by a mail-client link prefetcher) — sign links with `signUnsubscribeToken` / `signAddressUnsubscribeToken` using the same secret. `channel-email` sets `List-Unsubscribe` / `List-Unsubscribe-Post` automatically when a message's `data.unsubscribeUrl` points at the unsubscribe route. A notification declared with `unsubscribe: true` gets that link from delivery itself when the service has `unsubscribeLinks` (`runProdApp` option `unsubscribe: { secret }` mounts the routes and wires it; base URL from `auth.publicOrigin`, `auth.mail.baseUrl` or `APP_BASE_URL`): user recipients get a user token, `route` recipients an address token (needs a blind-index key, otherwise no link); a `data.unsubscribeUrl` from the caller wins. `unsubscribe.tenantOrigin: (ctx) => origin | null` points the link at the tenant's own host (subdomain or custom domain, must serve this app's `/api`); null, a throw or a non-https value fall back to the app origin. The token carries the mail's locale, so the confirmation and result pages render in the mail's language. To react to a no-account opt-out (e.g. remove the app's own subscriber row), declare `r.job(name, { trigger: { on: DeliveryHandlers.unsubscribeAddress } }, handler)` on a lane the app consumes: it runs after each committed opt-out, also an idempotent repeat, with `unsubscribeAddressPayloadSchema` (`addressHash`, `notificationType`, `channel`) as payload and the tenant in `ctx.triggeredBy.tenantId`; match the subscriber via `hashUnsubscribeAddress(email)`. Declare `unsubscribeUrl` as an optional template variable if the mail body shows it.",
|
|
729
729
|
"toggleableDefault": null,
|
|
730
730
|
"requires": [
|
|
731
731
|
"tenant"
|
|
@@ -1507,7 +1507,7 @@
|
|
|
1507
1507
|
},
|
|
1508
1508
|
{
|
|
1509
1509
|
"name": "mail-foundation",
|
|
1510
|
-
"description": "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 — 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.",
|
|
1510
|
+
"description": "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 — 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. Pass `tenantMailTransport` as `tenantTransport` to `createChannelEmailFeature` so a tenant with a provider configured sends delivery mails through its own transport and other tenants through the app transport.",
|
|
1511
1511
|
"toggleableDefault": null,
|
|
1512
1512
|
"requires": [
|
|
1513
1513
|
"config"
|
|
@@ -2122,7 +2122,7 @@
|
|
|
2122
2122
|
},
|
|
2123
2123
|
{
|
|
2124
2124
|
"name": "renderer-mail-html",
|
|
2125
|
-
"description": "A `renderer-foundation` plugin that handles `RenderRequest { kind: \"mail\" }` and returns `{ subject, html, text, previewText? }` suitable for sending via `channel-email`. The mail template (`kind: \"mail\"`, document HTML, subject in `meta`) resolves through `template-resolver`'s DB fallback, its body is compiled to Handlebars by template-resolver and rendered with escaping, then placed into an MJML layout (`kind: \"mail-layout\"`, slug `default` unless `meta.layoutSlug` says otherwise, an in-code structured layout with the tenant branding logo and accent color as last resort) that `mjml()` compiles to email-safe table HTML, with `html-to-text` producing the plain-text alternative. An optional `footer` resolver adds the platform imprint, privacy link and footer text below every structured layout,
|
|
2125
|
+
"description": "A `renderer-foundation` plugin that handles `RenderRequest { kind: \"mail\" }` and returns `{ subject, html, text, previewText? }` suitable for sending via `channel-email`. The mail template (`kind: \"mail\"`, document HTML, subject in `meta`) resolves through `template-resolver`'s DB fallback, its body is compiled to Handlebars by template-resolver and rendered with escaping, then placed into an MJML layout (`kind: \"mail-layout\"`, slug `default` unless `meta.layoutSlug` says otherwise, an in-code structured layout with the tenant branding logo and accent color as last resort) that `mjml()` compiles to email-safe table HTML, with `html-to-text` producing the plain-text alternative. An optional `footer` resolver adds the platform imprint, privacy link and footer text below every structured layout, an optional `brandSuffix` resolver adds a word after the tenant name in the brand row of the in-code layout, and an optional `siteUrl` resolver supplies the site link of the card when the tenant branding has no site URL (without either, the link is left out). The brand row and `data-resource=\"logo\"` images use the branding logo upload, or the branding logo https address when no file is uploaded. Mail meta `headerChip`, `tone` and `footerNote` add a chip beside the brand, the tone of the top line and chip, and a note above the site and unsubscribe links of the card. Seed an editable system layout with `seedSystemMailLayout(db)`. Requires `renderer-foundation` + `template-resolver`.",
|
|
2126
2126
|
"toggleableDefault": null,
|
|
2127
2127
|
"requires": [
|
|
2128
2128
|
"renderer-foundation",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"@cosmicdrift/kumiko-document-designer": "workspace:*",
|
|
22
22
|
"@cosmicdrift/kumiko-framework": "workspace:*",
|
|
23
23
|
"@cosmicdrift/kumiko-headless": "workspace:*",
|
|
24
|
+
"@cosmicdrift/kumiko-renderer-mail-html": "workspace:*",
|
|
24
25
|
"@cosmicdrift/kumiko-renderer-puppeteer-client": "workspace:*",
|
|
25
26
|
"@cosmicdrift/kumiko-renderer-web": "workspace:*",
|
|
26
27
|
"@cosmicdrift/kumiko-server-runtime": "workspace:*",
|
|
@@ -89,13 +89,13 @@ export function extractFeaturesFromText(rawText: string): Set<string> {
|
|
|
89
89
|
const name = m[1];
|
|
90
90
|
if (name) feats.add(name);
|
|
91
91
|
}
|
|
92
|
-
for (const m of text.matchAll(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
for (const m of text.matchAll(
|
|
93
|
+
/r\.(?:requires|optionalRequires)\(\s*\{\s*features:\s*\[([^\]]*)\]/g,
|
|
94
|
+
)) {
|
|
95
|
+
for (const quoted of (m[1] ?? "").matchAll(/["']([a-z0-9-]+)["']/g)) {
|
|
96
|
+
const name = quoted[1];
|
|
97
|
+
if (name) feats.add(name);
|
|
98
|
+
}
|
|
99
99
|
}
|
|
100
100
|
return feats;
|
|
101
101
|
}
|
|
@@ -120,9 +120,9 @@ export const notesFeature: FeatureDefinition = defineFeature("notes-demo", (r) =
|
|
|
120
120
|
r.describe(
|
|
121
121
|
"Dev-only host entity demonstrating tags, custom-fields, and folders on a real entityEdit screen.",
|
|
122
122
|
);
|
|
123
|
-
r.requires("tags");
|
|
124
|
-
r.requires("custom-fields");
|
|
125
|
-
r.requires("folders");
|
|
123
|
+
r.requires({ features: ["tags"] });
|
|
124
|
+
r.requires({ features: ["custom-fields"] });
|
|
125
|
+
r.requires({ features: ["folders"] });
|
|
126
126
|
|
|
127
127
|
wireCustomFieldsFor(r, "note", noteTable);
|
|
128
128
|
|
|
@@ -9,7 +9,7 @@ import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-frame
|
|
|
9
9
|
|
|
10
10
|
export const appScreensFeature: FeatureDefinition = defineFeature("app-screens", (r) => {
|
|
11
11
|
r.describe("App-side nav placement for the user-profile self-service page.");
|
|
12
|
-
r.requires("user-profile");
|
|
12
|
+
r.requires({ features: ["user-profile"] });
|
|
13
13
|
r.nav({
|
|
14
14
|
id: "profile",
|
|
15
15
|
label: "screen:profile.title",
|