@cosmicdrift/kumiko-bundled-features 0.351.0 → 0.353.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.
Files changed (164) hide show
  1. package/dist/agent-tools/agent-manifest.js +8 -2
  2. package/dist/agent-tools/manifest-deny-qns.d.ts +3 -0
  3. package/dist/agent-tools/manifest-deny-qns.js +17 -0
  4. package/dist/agent-tools/tool-catalog.d.ts +2 -1
  5. package/dist/agent-tools/tool-catalog.js +4 -1
  6. package/dist/audit/handlers/list.query.js +13 -8
  7. package/dist/auth-email-password/handlers/signup-confirm.write.js +2 -3
  8. package/dist/auth-mfa/schema/user-mfa.d.ts +6 -0
  9. package/dist/auth-mfa/screens.js +2 -2
  10. package/dist/billing-foundation/checkout-core.d.ts +7 -0
  11. package/dist/billing-foundation/checkout-core.js +7 -2
  12. package/dist/billing-foundation/entities.d.ts +8 -0
  13. package/dist/billing-foundation/handlers/start-plan-checkout.write.js +7 -1
  14. package/dist/billing-foundation/projection.d.ts +8 -0
  15. package/dist/cap-counter/book-cap-usage.d.ts +5 -1
  16. package/dist/cap-counter/book-cap-usage.js +65 -44
  17. package/dist/cap-counter/cap-reservation.d.ts +12 -0
  18. package/dist/cap-counter/cap-reservation.js +135 -0
  19. package/dist/cap-counter/constants.d.ts +3 -0
  20. package/dist/cap-counter/constants.js +6 -0
  21. package/dist/cap-counter/entity.d.ts +1 -0
  22. package/dist/cap-counter/feature.js +7 -2
  23. package/dist/cap-counter/handlers/increment-rolling.write.d.ts +5 -0
  24. package/dist/cap-counter/handlers/increment-rolling.write.js +4 -0
  25. package/dist/cap-counter/index.d.ts +4 -2
  26. package/dist/cap-counter/index.js +3 -1
  27. package/dist/cap-counter/payload-field.d.ts +1 -0
  28. package/dist/cap-counter/payload-field.js +11 -0
  29. package/dist/cap-counter/reservation-chain.d.ts +2 -0
  30. package/dist/cap-counter/reservation-chain.js +44 -0
  31. package/dist/cap-counter/stock-cap-guard.d.ts +3 -0
  32. package/dist/cap-counter/tables.d.ts +3 -0
  33. package/dist/cap-counter/tables.js +36 -0
  34. package/dist/cap-counter/value-cap-guard.d.ts +18 -0
  35. package/dist/cap-counter/value-cap-guard.js +33 -0
  36. package/dist/cap-counter/with-cap-enforcement.d.ts +5 -7
  37. package/dist/cap-counter/with-cap-enforcement.js +35 -51
  38. package/dist/cap-overview/access.js +2 -9
  39. package/dist/cap-overview/handlers/tenant-caps-list.query.js +2 -2
  40. package/dist/cap-overview/handlers/tenant-options.query.js +4 -5
  41. package/dist/channel-discord/feature.js +1 -0
  42. package/dist/channel-slack/feature.js +1 -0
  43. package/dist/channel-teams/feature.js +1 -0
  44. package/dist/compliance-profiles/schema/profile-selection.d.ts +10 -8
  45. package/dist/compliance-profiles-ops/handlers/tenants-missing-profile.query.js +2 -4
  46. package/dist/config/handlers/values.query.d.ts +3 -1
  47. package/dist/config/handlers/values.query.js +7 -2
  48. package/dist/config/table.d.ts +6 -0
  49. package/dist/custom-fields/entity.d.ts +4 -0
  50. package/dist/data-retention/schema/tenant-retention-override.d.ts +4 -0
  51. package/dist/delivery/attempt-log-retention.d.ts +9 -0
  52. package/dist/delivery/attempt-log-retention.js +23 -0
  53. package/dist/delivery/attempt-log.d.ts +1 -1
  54. package/dist/delivery/attempt-log.js +21 -2
  55. package/dist/delivery/constants.d.ts +1 -1
  56. package/dist/delivery/constants.js +1 -1
  57. package/dist/delivery/events.d.ts +1 -0
  58. package/dist/delivery/events.js +2 -0
  59. package/dist/delivery/feature.d.ts +1 -0
  60. package/dist/delivery/feature.js +39 -3
  61. package/dist/delivery/index.d.ts +2 -1
  62. package/dist/delivery/index.js +2 -1
  63. package/dist/delivery/mask-recipient-address.d.ts +1 -0
  64. package/dist/delivery/mask-recipient-address.js +41 -0
  65. package/dist/delivery/public-names.d.ts +2 -0
  66. package/dist/delivery/public-names.js +4 -1
  67. package/dist/delivery/tables.d.ts +6 -0
  68. package/dist/delivery/types.d.ts +1 -0
  69. package/dist/delivery/types.js +6 -2
  70. package/dist/document-ingest-foundation/entity.d.ts +4 -0
  71. package/dist/files-tenant-data/hooks.js +3 -2
  72. package/dist/folders/entity.d.ts +8 -0
  73. package/dist/folders/feature.js +2 -10
  74. package/dist/form-draft/db/queries/cleanup.d.ts +1 -1
  75. package/dist/form-draft/db/queries/cleanup.js +4 -3
  76. package/dist/form-draft/entity.d.ts +2 -0
  77. package/dist/form-draft/handlers/cleanup.job.d.ts +5 -1
  78. package/dist/form-draft/handlers/cleanup.job.js +11 -5
  79. package/dist/inbound-mail-foundation/entities.d.ts +25 -0
  80. package/dist/inbound-mail-foundation/projection.d.ts +20 -0
  81. package/dist/jobs/handlers/list.query.d.ts +2 -1
  82. package/dist/jobs/handlers/list.query.js +39 -14
  83. package/dist/jobs/i18n.js +1 -0
  84. package/dist/jobs/job-run-table.d.ts +6 -0
  85. package/dist/ledger/entity.d.ts +24 -8
  86. package/dist/ledger/entity.js +4 -0
  87. package/dist/ledger/schemas.js +8 -2
  88. package/dist/managed-pages/feature.js +28 -10
  89. package/dist/managed-pages/table.d.ts +12 -0
  90. package/dist/notes-history/entity.d.ts +12 -0
  91. package/dist/notes-history/feature.js +2 -10
  92. package/dist/personal-access-tokens/schema/api-token.d.ts +6 -0
  93. package/dist/personal-access-tokens/screens.js +14 -3
  94. package/dist/renderer-foundation/types.d.ts +1 -0
  95. package/dist/renderer-simple/feature.js +4 -6
  96. package/dist/renderer-simple/index.d.ts +1 -0
  97. package/dist/renderer-simple/mail-blocks.d.ts +21 -0
  98. package/dist/renderer-simple/mail-blocks.js +107 -0
  99. package/dist/renderer-simple/simple-renderer.js +21 -3
  100. package/dist/secrets/table.d.ts +1 -0
  101. package/dist/seo/feature.js +28 -6
  102. package/dist/sessions/schema/user-session.d.ts +4 -0
  103. package/dist/shared/assert-no-where-rule-in-ownership-write.d.ts +2 -0
  104. package/dist/shared/assert-no-where-rule-in-ownership-write.js +18 -0
  105. package/dist/shared/index.d.ts +2 -1
  106. package/dist/shared/index.js +2 -1
  107. package/dist/shared/rate-limited-text-response.d.ts +2 -0
  108. package/dist/shared/rate-limited-text-response.js +9 -0
  109. package/dist/tags/entity.d.ts +9 -0
  110. package/dist/tags/feature.js +2 -10
  111. package/dist/template-resolver/table.d.ts +48 -30
  112. package/dist/template-resolver/user-content-table.d.ts +34 -20
  113. package/dist/tenant/decrypt-tenant-names.d.ts +3 -0
  114. package/dist/tenant/decrypt-tenant-names.js +10 -0
  115. package/dist/tenant/handlers/me.query.js +5 -1
  116. package/dist/tenant/handlers/memberships.query.js +3 -2
  117. package/dist/tenant/handlers/tenant-directory.query.js +9 -3
  118. package/dist/tenant/index.d.ts +1 -0
  119. package/dist/tenant/index.js +1 -0
  120. package/dist/tenant/invitation-table.d.ts +16 -8
  121. package/dist/tenant/membership-table.d.ts +4 -0
  122. package/dist/tenant/schema/tenant.d.ts +18 -10
  123. package/dist/tenant/schema/tenant.js +4 -6
  124. package/dist/tenant/screens.js +1 -1
  125. package/dist/tenant-handover/move-entity-graph.js +11 -18
  126. package/dist/tenant-lifecycle/feature.js +1 -0
  127. package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +3 -0
  128. package/dist/tenant-lifecycle/run-tenant-destroy.js +2 -0
  129. package/dist/tenant-lifecycle/stages.d.ts +2 -0
  130. package/dist/tenant-lifecycle/stages.js +7 -1
  131. package/dist/tier-engine/entity.d.ts +2 -0
  132. package/dist/tier-engine/index.d.ts +1 -0
  133. package/dist/tier-engine/index.js +1 -0
  134. package/dist/tier-engine/tier-option-gate.d.ts +32 -0
  135. package/dist/tier-engine/tier-option-gate.js +62 -0
  136. package/dist/tier-engine/tier-resolver.d.ts +2 -0
  137. package/dist/user/handlers/list.query.js +2 -2
  138. package/dist/user/schema/user.d.ts +22 -8
  139. package/dist/user-data-rights/boot-checks.js +5 -0
  140. package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +2 -2
  141. package/dist/user-data-rights/schema/download-attempt.d.ts +14 -0
  142. package/dist/user-data-rights/schema/download-token.d.ts +8 -0
  143. package/dist/user-data-rights/schema/export-job.d.ts +16 -8
  144. package/dist/user-data-rights-defaults/hooks/delivery-attempt.userdata-hook.js +25 -6
  145. package/dist/waitlist/entity.d.ts +22 -6
  146. package/dist/workflow-runner/event-trigger.js +6 -3
  147. package/dist/workflow-runner/handlers/resume-run.write.js +3 -0
  148. package/package.json +10 -10
  149. package/src/agent-tools/changes.json +12 -0
  150. package/src/audit/changes.json +6 -0
  151. package/src/cap-counter/changes.json +28 -0
  152. package/src/delivery/changes.json +12 -0
  153. package/src/derivatives-sharp/changes.json +6 -0
  154. package/src/form-draft/changes.json +7 -1
  155. package/src/legal-pages/changes.json +6 -0
  156. package/src/managed-pages/changes.json +6 -0
  157. package/src/renderer-simple/changes.json +6 -0
  158. package/src/seo/changes.json +5 -0
  159. package/src/tenant/changes.json +7 -0
  160. package/src/tenant-handover/changes.json +6 -0
  161. package/src/tier-engine/changes.json +6 -0
  162. package/src/workflow-runner/changes.json +5 -0
  163. package/dist/shared/has-where-rule.d.ts +0 -2
  164. package/dist/shared/has-where-rule.js +0 -10
@@ -1,6 +1,11 @@
1
1
  import { i18nKey, MAX_LIST_LIMIT, } from "@cosmicdrift/kumiko-framework/engine";
2
- import { PAT_MINT_SCREEN_ID, PAT_SCREEN_ID, PatHandlers, PatQueries } from "./constants.js";
2
+ import { ACTION_FORM_ENTITY } from "@cosmicdrift/kumiko-framework/ui-types";
3
+ import { fieldOptionLabelKeyPrefix } from "@cosmicdrift/kumiko-headless";
4
+ import { PAT_FEATURE, PAT_MINT_SCREEN_ID, PAT_SCREEN_ID, PatHandlers, PatQueries, } from "./constants.js";
3
5
  const PAT_STATUS_OPTION_KEY_PREFIX = "pat.list.status.";
6
+ // Same keys the mint form's `scopes` multiSelect options resolve through
7
+ // (see patScopeOptionTranslations), so list and form show identical labels.
8
+ const PAT_SCOPE_OPTION_KEY_PREFIX = fieldOptionLabelKeyPrefix(PAT_FEATURE, ACTION_FORM_ENTITY, "scopes");
4
9
  // Both screens render only for tenants whose tier includes the feature; the
5
10
  // availability query is rejected by the feature gate otherwise.
6
11
  const PAT_SCREEN_VISIBLE_WHEN = {
@@ -20,11 +25,17 @@ export const patListScreen = {
20
25
  defaultSort: { field: "createdAt", dir: "desc" },
21
26
  columns: [
22
27
  { field: "name", label: i18nKey("pat.list.col.name") },
23
- { field: "prefix", label: i18nKey("pat.list.col.prefix") },
24
- { field: "scopes", label: i18nKey("pat.list.col.scopes") },
28
+ { field: "prefix", label: i18nKey("pat.list.col.prefix"), sortable: false },
29
+ {
30
+ field: "scopes",
31
+ label: i18nKey("pat.list.col.scopes"),
32
+ sortable: false,
33
+ renderer: { format: "enumOption", keyPrefix: PAT_SCOPE_OPTION_KEY_PREFIX },
34
+ },
25
35
  {
26
36
  field: "status",
27
37
  label: i18nKey("pat.list.col.status"),
38
+ sortable: false,
28
39
  renderer: { format: "enumOption", keyPrefix: PAT_STATUS_OPTION_KEY_PREFIX },
29
40
  },
30
41
  {
@@ -19,6 +19,7 @@ export type RenderRequest = {
19
19
  export type RenderResponse = {
20
20
  kind: "notification";
21
21
  html: string;
22
+ text?: string;
22
23
  } | {
23
24
  kind: "mail-html";
24
25
  html: string;
@@ -16,12 +16,10 @@ export async function adaptToFoundation(req, ctx, renderer = simpleRenderer) {
16
16
  throw new RendererError(`renderer-simple supports only kind="notification", got "${req.kind}"`, "invalid_payload");
17
17
  }
18
18
  const variables = await resolveNotificationVariables(req, ctx);
19
- const html = await renderer.render({
20
- template: req.payload.template ?? "",
21
- variables,
22
- locale: req.payload.locale,
23
- });
24
- return { kind: "notification", html };
19
+ const input = { template: req.payload.template ?? "", variables, locale: req.payload.locale };
20
+ const html = await renderer.render(input);
21
+ const text = renderer.renderText ? await renderer.renderText(input) : undefined;
22
+ return text === undefined ? { kind: "notification", html } : { kind: "notification", html, text };
25
23
  }
26
24
  export function createRendererSimpleFeature(options) {
27
25
  const renderer = options?.mailBranding
@@ -1,2 +1,3 @@
1
1
  export { createRendererSimpleFeature, type RendererSimpleOptions } from "./feature.js";
2
+ export type { MailBadge, MailChip, MailStage, MailStageState, MailTone } from "./mail-blocks.js";
2
3
  export { createSimpleRenderer, type LocalizedText, type MailBranding, simpleRenderer, } from "./simple-renderer.js";
@@ -0,0 +1,21 @@
1
+ export type MailTone = "neutral" | "info" | "success" | "warning" | "danger";
2
+ export type MailStageState = "done" | "current" | "upcoming";
3
+ export type MailBadge = {
4
+ readonly label: string;
5
+ readonly tone?: MailTone;
6
+ };
7
+ export type MailStage = {
8
+ readonly label: string;
9
+ readonly state: MailStageState;
10
+ };
11
+ export type MailChip = {
12
+ readonly label: string;
13
+ readonly tone?: MailTone;
14
+ };
15
+ export declare function parseBadge(value: unknown): MailBadge | undefined;
16
+ export declare function renderBadge(badge: MailBadge): string;
17
+ export declare function renderChips(chips: unknown): string;
18
+ export declare function renderStages(stages: unknown, primaryColor: string): string;
19
+ export declare function badgeText(badge: MailBadge): string;
20
+ export declare function stagesText(stages: unknown): string;
21
+ export declare function chipsText(chips: unknown): string;
@@ -0,0 +1,107 @@
1
+ import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
2
+ // Literal types (as const) keep the colors compile-time known, so they can go into style
3
+ // attributes without escaping.
4
+ const TONE_COLORS = {
5
+ neutral: { bg: "#f3f4f6", fg: "#374151" },
6
+ info: { bg: "#dbeafe", fg: "#1e40af" },
7
+ success: { bg: "#dcfce7", fg: "#166534" },
8
+ warning: { bg: "#fef3c7", fg: "#92400e" },
9
+ danger: { bg: "#fee2e2", fg: "#991b1b" },
10
+ };
11
+ const STAGE_STATES = ["done", "current", "upcoming"];
12
+ const STAGE_DONE_COLOR = "#16a34a";
13
+ const STAGE_UPCOMING_BG = "#e5e7eb";
14
+ const STAGE_UPCOMING_FG = "#9ca3af";
15
+ function isRecord(value) {
16
+ return typeof value === "object" && value !== null;
17
+ }
18
+ function isMailTone(value) {
19
+ return typeof value === "string" && Object.hasOwn(TONE_COLORS, value);
20
+ }
21
+ function toneColors(tone) {
22
+ return TONE_COLORS[isMailTone(tone) ? tone : "neutral"];
23
+ }
24
+ function isStageState(value) {
25
+ return STAGE_STATES.some((state) => state === value);
26
+ }
27
+ // Template data is runtime data (variables of a stored template): entries that do not match are
28
+ // skipped instead of failing the whole mail.
29
+ function isStage(value) {
30
+ return isRecord(value) && typeof value["label"] === "string" && isStageState(value["state"]);
31
+ }
32
+ function isChip(value) {
33
+ return isRecord(value) && typeof value["label"] === "string";
34
+ }
35
+ export function parseBadge(value) {
36
+ return isChip(value) && value.label !== "" ? value : undefined;
37
+ }
38
+ function validStages(value) {
39
+ return Array.isArray(value) ? value.filter(isStage) : [];
40
+ }
41
+ function validChips(value) {
42
+ return Array.isArray(value) ? value.filter(isChip) : [];
43
+ }
44
+ function pill(chip, margin) {
45
+ const { bg, fg } = toneColors(chip.tone);
46
+ return `<span style="display:inline-block;margin:${margin};padding:2px 10px;border-radius:999px;background:${bg};color:${fg};font-size:12px;font-weight:600;line-height:18px">${escapeHtml(chip.label)}</span>`;
47
+ }
48
+ export function renderBadge(badge) {
49
+ return `<p style="margin:0 0 12px">${pill(badge, "0")}</p>`;
50
+ }
51
+ export function renderChips(chips) {
52
+ const valid = validChips(chips);
53
+ if (valid.length === 0)
54
+ return "";
55
+ return `<p style="margin:0 0 16px">${valid.map((chip) => pill(chip, "0 6px 6px 0")).join("")}</p>`;
56
+ }
57
+ function stageMarker(stage, position, primaryColor) {
58
+ const base = "display:inline-block;width:24px;height:24px;line-height:24px;border-radius:12px;text-align:center;font-size:12px;font-weight:700";
59
+ const primary = escapeHtmlAttr(primaryColor);
60
+ if (stage.state === "done") {
61
+ return `<span style="${base};background:${STAGE_DONE_COLOR};color:#fff">&#10003;</span>`;
62
+ }
63
+ if (stage.state === "current") {
64
+ return `<span style="${base};background:#fff;border:2px solid ${primary};line-height:20px;width:20px;height:20px;color:${primary}">${position}</span>`;
65
+ }
66
+ return `<span style="${base};background:${STAGE_UPCOMING_BG};color:${STAGE_UPCOMING_FG}">${position}</span>`;
67
+ }
68
+ function stageCell(stage, position, primaryColor) {
69
+ const labelColor = stage.state === "upcoming" ? STAGE_UPCOMING_FG : "#111";
70
+ const weight = stage.state === "current" ? 700 : 400;
71
+ return `<td align="center" valign="top" style="padding:0 4px;font-family:sans-serif">${stageMarker(stage, position, primaryColor)}<div style="margin:4px 0 0;font-size:12px;font-weight:${weight};color:${labelColor}">${escapeHtml(stage.label)}</div></td>`;
72
+ }
73
+ function stageConnector(previous) {
74
+ const color = previous.state === "done" ? STAGE_DONE_COLOR : STAGE_UPCOMING_BG;
75
+ return `<td valign="top" style="padding:11px 0 0"><div style="width:24px;height:2px;line-height:2px;font-size:2px;background:${color}">&nbsp;</div></td>`;
76
+ }
77
+ export function renderStages(stages, primaryColor) {
78
+ const valid = validStages(stages);
79
+ if (valid.length === 0)
80
+ return "";
81
+ const cellsHtml = valid
82
+ .map((stage, index) => {
83
+ const previous = valid[index - 1];
84
+ const connector = previous ? stageConnector(previous) : "";
85
+ return `${connector}${stageCell(stage, index + 1, primaryColor)}`;
86
+ })
87
+ .join("");
88
+ return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 16px"><tr>${cellsHtml}</tr></table>`;
89
+ }
90
+ const STAGE_TEXT_MARKER = {
91
+ done: "✓",
92
+ current: "▶",
93
+ upcoming: "○",
94
+ };
95
+ export function badgeText(badge) {
96
+ return `[${badge.label}]`;
97
+ }
98
+ export function stagesText(stages) {
99
+ return validStages(stages)
100
+ .map((stage) => `${STAGE_TEXT_MARKER[stage.state]} ${stage.label}`)
101
+ .join(" → ");
102
+ }
103
+ export function chipsText(chips) {
104
+ return validChips(chips)
105
+ .map((chip) => chip.label)
106
+ .join(" · ");
107
+ }
@@ -1,5 +1,6 @@
1
1
  import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
2
2
  import { renderSafeMarkdown } from "../page-render/index.js";
3
+ import { badgeText, chipsText, parseBadge, renderBadge, renderChips, renderStages, stagesText, } from "./mail-blocks.js";
3
4
  const DEFAULT_PRIMARY_COLOR = "#2563eb";
4
5
  const HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
5
6
  function isHttpUrl(value) {
@@ -107,6 +108,7 @@ function templateContent(variables) {
107
108
  const data = variables; // @cast-boundary render-helper
108
109
  // Without structured fields, title + body become the header and a single text section.
109
110
  return {
111
+ badge: parseBadge(data.badge),
110
112
  header: data.header ?? data.title,
111
113
  sections: data.sections ?? (data.body ? [{ text: data.body }] : undefined),
112
114
  footer: data.footer,
@@ -122,6 +124,10 @@ function sectionText(section) {
122
124
  return section.markdown;
123
125
  if ("button" in section)
124
126
  return `${section.button.label}: ${section.button.url}`;
127
+ if ("stages" in section)
128
+ return stagesText(section.stages);
129
+ if ("chips" in section)
130
+ return chipsText(section.chips);
125
131
  return "";
126
132
  }
127
133
  function renderSection(section, primaryColor) {
@@ -138,6 +144,10 @@ function renderSection(section, primaryColor) {
138
144
  if ("button" in section) {
139
145
  return `<p style="margin:0 0 16px"><a href="${escapeHtml(section.button.url)}" style="display:inline-block;padding:10px 24px;background:${escapeHtmlAttr(primaryColor)};color:#fff;text-decoration:none;border-radius:4px;font-size:14px">${escapeHtml(section.button.label)}</a></p>`;
140
146
  }
147
+ if ("stages" in section)
148
+ return renderStages(section.stages, primaryColor);
149
+ if ("chips" in section)
150
+ return renderChips(section.chips);
141
151
  return "";
142
152
  }
143
153
  // Simple Renderer: turns structured email template data into HTML with inline CSS.
@@ -150,11 +160,13 @@ export function createSimpleRenderer(branding) {
150
160
  return {
151
161
  name: "simple",
152
162
  async render(input) {
153
- const { header, sections, footer } = templateContent(input.variables);
163
+ const { badge, header, sections, footer } = templateContent(input.variables);
154
164
  const parts = [];
155
165
  parts.push('<!DOCTYPE html><html><body style="margin:0;padding:0;font-family:sans-serif">');
156
166
  parts.push('<div style="max-width:600px;margin:0 auto;padding:24px">');
157
167
  parts.push(renderBrandingHeader(branding, primaryColor, logoUrl));
168
+ if (badge)
169
+ parts.push(renderBadge(badge));
158
170
  if (header) {
159
171
  parts.push(`<h1 style="margin:0 0 24px;color:#111;font-size:20px;font-weight:600">${escapeHtml(header)}</h1>`);
160
172
  }
@@ -171,9 +183,15 @@ export function createSimpleRenderer(branding) {
171
183
  return parts.join("");
172
184
  },
173
185
  async renderText(input) {
174
- const { header, sections, footer } = templateContent(input.variables);
186
+ const { badge, header, sections, footer } = templateContent(input.variables);
175
187
  const brandingFooter = brandingFooterLines(branding, input.locale).join("\n");
176
- return [header, ...(sections ?? []).map(sectionText), footer, brandingFooter]
188
+ return [
189
+ badge ? badgeText(badge) : undefined,
190
+ header,
191
+ ...(sections ?? []).map(sectionText),
192
+ footer,
193
+ brandingFooter,
194
+ ]
177
195
  .filter((block) => block !== undefined && block !== "")
178
196
  .join("\n\n");
179
197
  },
@@ -8,6 +8,7 @@ export declare const tenantSecretEntity: import("@cosmicdrift/kumiko-types/field
8
8
  readonly type: "text";
9
9
  readonly description?: string;
10
10
  readonly maxLength?: number;
11
+ readonly minLength?: number;
11
12
  readonly required?: boolean;
12
13
  readonly searchable?: boolean;
13
14
  readonly sortable?: boolean;
@@ -1,8 +1,10 @@
1
1
  // kumiko-lint-ignore app-feature-structure Phase-3 conversion tracked in #2312 (feature.ts > 300 lines)
2
2
  import { computeRevisionEtag } from "@cosmicdrift/kumiko-framework/api";
3
3
  import { defineFeature, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
4
+ import { RateLimitError } from "@cosmicdrift/kumiko-framework/errors";
4
5
  import { LEGAL_ROUTES } from "../legal-pages/index.js";
5
6
  import { cachedSecurePageResponse } from "../page-render/index.js";
7
+ import { rateLimitedTextResponse } from "../shared/index.js";
6
8
  import { SEO_CONFIG_KEYS, SEO_DEFAULT_PATHS } from "./constants.js";
7
9
  import { seoConfigQuery } from "./handlers/seo-config.query.js";
8
10
  import { buildLlmsTxt } from "./llms-txt.js";
@@ -62,7 +64,10 @@ async function gatherEntries(opts, systemQuery, origin, host) {
62
64
  });
63
65
  }
64
66
  }
65
- catch {
67
+ catch (error) {
68
+ // A rate limit must not look like "pages removed" to a crawler.
69
+ if (error instanceof RateLimitError)
70
+ throw error;
66
71
  // managed-pages unreachable/not mounted — degrade to callback-only entries.
67
72
  }
68
73
  }
@@ -113,7 +118,15 @@ export function createSeoFeature(opts) {
113
118
  anonymous: true,
114
119
  handler: async (c, { systemQuery }) => {
115
120
  const { origin, host } = requestHost(c);
116
- const entries = await gatherEntries(opts, systemQuery, origin, host);
121
+ let entries;
122
+ try {
123
+ entries = await gatherEntries(opts, systemQuery, origin, host);
124
+ }
125
+ catch (error) {
126
+ if (error instanceof RateLimitError)
127
+ return rateLimitedTextResponse(error);
128
+ throw error;
129
+ }
117
130
  const xml = buildSitemapXml(entries);
118
131
  const etag = computeRevisionEtag([host, xml]);
119
132
  return cachedSecurePageResponse(c.req.raw, {
@@ -133,10 +146,19 @@ export function createSeoFeature(opts) {
133
146
  const tenantId = opts.managedPages
134
147
  ? ((await opts.managedPages.resolveApexTenant(host)) ?? SYSTEM_TENANT_ID)
135
148
  : SYSTEM_TENANT_ID;
136
- const [entries, seoConfig] = await Promise.all([
137
- gatherEntries(opts, systemQuery, origin, host),
138
- readSeoConfig(systemQuery, tenantId),
139
- ]);
149
+ let entries;
150
+ let seoConfig;
151
+ try {
152
+ [entries, seoConfig] = await Promise.all([
153
+ gatherEntries(opts, systemQuery, origin, host),
154
+ readSeoConfig(systemQuery, tenantId),
155
+ ]);
156
+ }
157
+ catch (error) {
158
+ if (error instanceof RateLimitError)
159
+ return rateLimitedTextResponse(error);
160
+ throw error;
161
+ }
140
162
  const sections = entries.length > 0
141
163
  ? [
142
164
  {
@@ -3,6 +3,7 @@ export declare const userSessionEntity: import("@cosmicdrift/kumiko-types/fields
3
3
  readonly type: "text";
4
4
  readonly description?: string;
5
5
  readonly maxLength?: number;
6
+ readonly minLength?: number;
6
7
  readonly required?: boolean;
7
8
  readonly searchable?: boolean;
8
9
  readonly sortable?: boolean;
@@ -24,6 +25,7 @@ export declare const userSessionEntity: import("@cosmicdrift/kumiko-types/fields
24
25
  readonly type: "text";
25
26
  readonly description?: string;
26
27
  readonly maxLength?: number;
28
+ readonly minLength?: number;
27
29
  readonly required?: boolean;
28
30
  readonly searchable?: boolean;
29
31
  readonly sortable?: boolean;
@@ -105,6 +107,7 @@ export declare const userSessionEntity: import("@cosmicdrift/kumiko-types/fields
105
107
  readonly type: "text";
106
108
  readonly description?: string;
107
109
  readonly maxLength?: number;
110
+ readonly minLength?: number;
108
111
  readonly required?: boolean;
109
112
  readonly searchable?: boolean;
110
113
  readonly sortable?: boolean;
@@ -126,6 +129,7 @@ export declare const userSessionEntity: import("@cosmicdrift/kumiko-types/fields
126
129
  readonly type: "text";
127
130
  readonly description?: string;
128
131
  readonly maxLength?: number;
132
+ readonly minLength?: number;
129
133
  readonly required?: boolean;
130
134
  readonly searchable?: boolean;
131
135
  readonly sortable?: boolean;
@@ -0,0 +1,2 @@
1
+ import type { OwnershipMap } from "@cosmicdrift/kumiko-framework/engine";
2
+ export declare function assertNoWhereRuleInOwnershipWrite(factoryName: string, writeOwnership: OwnershipMap | undefined): void;
@@ -0,0 +1,18 @@
1
+ // Boot validation rejects `{ kind: "where" }` in access.write too (fw#2626);
2
+ // failing at factory time names the option instead of an entity scope.
3
+ export function assertNoWhereRuleInOwnershipWrite(factoryName, writeOwnership) {
4
+ // skip: no write ownership declared, nothing to validate
5
+ if (!writeOwnership)
6
+ return;
7
+ const hasWhereRule = Object.values(writeOwnership).some((rule) => rule !== "all" && rule.kind === "where");
8
+ // skip: no where-rule present, the option is valid
9
+ if (!hasWhereRule)
10
+ return;
11
+ throw new Error(`${factoryName}({ ownership }): ownership.write must not contain a ` +
12
+ '`{ kind: "where" }` rule — where-rules are evaluated only at the SQL ' +
13
+ "layer (the read path, via buildOwnershipClause). Write paths that " +
14
+ "consult access.write (userCanCreateFieldRow/userCanWriteFieldRow) can't " +
15
+ "evaluate them, so such a rule can only ever deny — boot validation " +
16
+ "rejects it too (fw#2626). Use a `from()` rule for ownership.write, or " +
17
+ "leave it unset.");
18
+ }
@@ -1,17 +1,18 @@
1
1
  export { collectErasureFailure, throwIfErasureFailed } from "./assert-erased.js";
2
+ export { assertNoWhereRuleInOwnershipWrite } from "./assert-no-where-rule-in-ownership-write.js";
2
3
  export { type ChunkedMigrationOptions, type ChunkedMigrationResult, type ChunkedMigrationStopReason, type MigrationRowOutcome, runChunkedMigration, } from "./chunked-entity-migration.js";
3
4
  export { classifyStoredEnvelope, type StoredEnvelopeClassification, } from "./classify-stored-envelope.js";
4
5
  export { decryptStoredPii } from "./decrypt-stored-pii.js";
5
6
  export { encryptForDirectWrite } from "./encrypt-for-direct-write.js";
6
7
  export { entitiesOf } from "./entities-of.js";
7
8
  export { isWithinGracePeriod } from "./grace-period.js";
8
- export { hasWhereRule } from "./has-where-rule.js";
9
9
  export { isIdentityV3Hash, verifyIdentityV3Hash } from "./identity-v3-hash.js";
10
10
  export { isTenantDb } from "./is-tenant-db.js";
11
11
  export { createLockoutCounter, type LockoutCounterState } from "./lockout-counter.js";
12
12
  export { mapWithConcurrency } from "./map-with-concurrency.js";
13
13
  export { denyUnlessJoinRowParentVisible, joinRowParentIsVisible, parentRowIsVisible, } from "./parent-visibility.js";
14
14
  export { hashPassword, verifyDummyPassword, verifyPassword } from "./password-hashing.js";
15
+ export { rateLimitedTextResponse } from "./rate-limited-text-response.js";
15
16
  export { redactBotTokens, redactEmailAddresses, redactErrorText, redactUrls, } from "./redact.js";
16
17
  export { type RowBoundGrantResult, redeemRowBoundGrant, signRowBoundGrant, } from "./row-bound-grant.js";
17
18
  export { runInSubTransaction } from "./run-in-sub-transaction.js";
@@ -1,17 +1,18 @@
1
1
  export { collectErasureFailure, throwIfErasureFailed } from "./assert-erased.js";
2
+ export { assertNoWhereRuleInOwnershipWrite } from "./assert-no-where-rule-in-ownership-write.js";
2
3
  export { runChunkedMigration, } from "./chunked-entity-migration.js";
3
4
  export { classifyStoredEnvelope, } from "./classify-stored-envelope.js";
4
5
  export { decryptStoredPii } from "./decrypt-stored-pii.js";
5
6
  export { encryptForDirectWrite } from "./encrypt-for-direct-write.js";
6
7
  export { entitiesOf } from "./entities-of.js";
7
8
  export { isWithinGracePeriod } from "./grace-period.js";
8
- export { hasWhereRule } from "./has-where-rule.js";
9
9
  export { isIdentityV3Hash, verifyIdentityV3Hash } from "./identity-v3-hash.js";
10
10
  export { isTenantDb } from "./is-tenant-db.js";
11
11
  export { createLockoutCounter } from "./lockout-counter.js";
12
12
  export { mapWithConcurrency } from "./map-with-concurrency.js";
13
13
  export { denyUnlessJoinRowParentVisible, joinRowParentIsVisible, parentRowIsVisible, } from "./parent-visibility.js";
14
14
  export { hashPassword, verifyDummyPassword, verifyPassword } from "./password-hashing.js";
15
+ export { rateLimitedTextResponse } from "./rate-limited-text-response.js";
15
16
  export { redactBotTokens, redactEmailAddresses, redactErrorText, redactUrls, } from "./redact.js";
16
17
  export { redeemRowBoundGrant, signRowBoundGrant, } from "./row-bound-grant.js";
17
18
  export { runInSubTransaction } from "./run-in-sub-transaction.js";
@@ -0,0 +1,2 @@
1
+ import type { RateLimitError } from "@cosmicdrift/kumiko-framework/errors";
2
+ export declare function rateLimitedTextResponse(error: RateLimitError): Response;
@@ -0,0 +1,9 @@
1
+ export function rateLimitedTextResponse(error) {
2
+ return new Response("rate limited", {
3
+ status: 429,
4
+ headers: {
5
+ "content-type": "text/plain; charset=utf-8",
6
+ "retry-after": String(error.details.retryAfterSeconds),
7
+ },
8
+ });
9
+ }
@@ -4,6 +4,7 @@ export declare const tagEntity: EntityDefinition<{
4
4
  readonly type: "text";
5
5
  readonly description?: string;
6
6
  readonly maxLength?: number;
7
+ readonly minLength?: number;
7
8
  readonly required?: boolean;
8
9
  readonly searchable?: boolean;
9
10
  readonly sortable?: boolean;
@@ -25,6 +26,7 @@ export declare const tagEntity: EntityDefinition<{
25
26
  readonly type: "text";
26
27
  readonly description?: string;
27
28
  readonly maxLength?: number;
29
+ readonly minLength?: number;
28
30
  readonly required?: boolean;
29
31
  readonly searchable?: boolean;
30
32
  readonly sortable?: boolean;
@@ -46,6 +48,7 @@ export declare const tagEntity: EntityDefinition<{
46
48
  readonly type: "text";
47
49
  readonly description?: string;
48
50
  readonly maxLength?: number;
51
+ readonly minLength?: number;
49
52
  readonly required?: boolean;
50
53
  readonly searchable?: boolean;
51
54
  readonly sortable?: boolean;
@@ -69,6 +72,7 @@ export declare function createTagAssignmentEntity(access?: EntityDefinition["acc
69
72
  readonly type: "text";
70
73
  readonly description?: string;
71
74
  readonly maxLength?: number;
75
+ readonly minLength?: number;
72
76
  readonly required?: boolean;
73
77
  readonly searchable?: boolean;
74
78
  readonly sortable?: boolean;
@@ -90,6 +94,7 @@ export declare function createTagAssignmentEntity(access?: EntityDefinition["acc
90
94
  readonly type: "text";
91
95
  readonly description?: string;
92
96
  readonly maxLength?: number;
97
+ readonly minLength?: number;
93
98
  readonly required?: boolean;
94
99
  readonly searchable?: boolean;
95
100
  readonly sortable?: boolean;
@@ -111,6 +116,7 @@ export declare function createTagAssignmentEntity(access?: EntityDefinition["acc
111
116
  readonly type: "text";
112
117
  readonly description?: string;
113
118
  readonly maxLength?: number;
119
+ readonly minLength?: number;
114
120
  readonly required?: boolean;
115
121
  readonly searchable?: boolean;
116
122
  readonly sortable?: boolean;
@@ -134,6 +140,7 @@ export declare const tagAssignmentEntity: EntityDefinition<{
134
140
  readonly type: "text";
135
141
  readonly description?: string;
136
142
  readonly maxLength?: number;
143
+ readonly minLength?: number;
137
144
  readonly required?: boolean;
138
145
  readonly searchable?: boolean;
139
146
  readonly sortable?: boolean;
@@ -155,6 +162,7 @@ export declare const tagAssignmentEntity: EntityDefinition<{
155
162
  readonly type: "text";
156
163
  readonly description?: string;
157
164
  readonly maxLength?: number;
165
+ readonly minLength?: number;
158
166
  readonly required?: boolean;
159
167
  readonly searchable?: boolean;
160
168
  readonly sortable?: boolean;
@@ -176,6 +184,7 @@ export declare const tagAssignmentEntity: EntityDefinition<{
176
184
  readonly type: "text";
177
185
  readonly description?: string;
178
186
  readonly maxLength?: number;
187
+ readonly minLength?: number;
179
188
  readonly required?: boolean;
180
189
  readonly searchable?: boolean;
181
190
  readonly sortable?: boolean;
@@ -18,7 +18,7 @@
18
18
  // entityList/entityEdit catalog screens (legacy QNs stay for TagManager).
19
19
  // Deferred: optional host-projection decoration (`wireTagsFor`), search indexing.
20
20
  import { defineEntityDetailHandler, defineEntityListHandler, defineEntityUpdateHandler, defineFeature, } from "@cosmicdrift/kumiko-framework/engine";
21
- import { hasWhereRule } from "../shared/index.js";
21
+ import { assertNoWhereRuleInOwnershipWrite } from "../shared/index.js";
22
22
  import { DEFAULT_TAG_ACCESS, TAGS_FEATURE_NAME } from "./constants.js";
23
23
  import { createTagAssignmentEntity, tagEntity } from "./entity.js";
24
24
  import { createTagAssignmentExecutor } from "./executor.js";
@@ -109,15 +109,7 @@ export function createTagsFeature(opts = {}) {
109
109
  "assignment row. Omit `parents` to keep every registered entity " +
110
110
  "admissible as a host instead.");
111
111
  }
112
- if (hasWhereRule(opts.ownership?.write)) {
113
- throw new Error("createTagsFeature({ ownership }): ownership.write must not contain a " +
114
- '`{ kind: "where" }` rule — where-rules are evaluated only at the SQL ' +
115
- "layer (the read path, via buildOwnershipClause). Write paths that " +
116
- "consult access.write (userCanCreateFieldRow/userCanWriteFieldRow) can't " +
117
- "evaluate them, so such a rule can only ever deny — boot validation " +
118
- "rejects it too (fw#2626). Use a `from()` rule for ownership.write, or " +
119
- "leave it unset.");
120
- }
112
+ assertNoWhereRuleInOwnershipWrite("createTagsFeature", opts.ownership?.write);
121
113
  const access = resolveAccess(opts);
122
114
  return defineFeature(TAGS_FEATURE_NAME, (r) => registerTags(r, access, opts.toggleable, opts.ownership, opts.parents));
123
115
  }