@cosmicdrift/kumiko-samples 0.352.0 → 0.354.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 (186) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +18 -2
  3. package/packages/bundled-features/src/agent-tools/agent-doc-lint.ts +66 -0
  4. package/packages/bundled-features/src/agent-tools/agent-manifest.ts +26 -7
  5. package/packages/bundled-features/src/agent-tools/changes.json +12 -0
  6. package/packages/bundled-features/src/agent-tools/feature.ts +13 -2
  7. package/packages/bundled-features/src/agent-tools/index.ts +6 -1
  8. package/packages/bundled-features/src/agent-tools/manifest-deny-qns.ts +25 -0
  9. package/packages/bundled-features/src/agent-tools/tool-catalog.ts +4 -1
  10. package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +1 -1
  11. package/packages/bundled-features/src/audit/changes.json +6 -0
  12. package/packages/bundled-features/src/audit/handlers/list.query.ts +20 -10
  13. package/packages/bundled-features/src/auth-email-password/changes.json +24 -0
  14. package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +1 -0
  15. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +3 -2
  16. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept.write.ts +3 -2
  17. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +21 -0
  18. package/packages/bundled-features/src/auth-email-password/handlers/invite-signup-complete.write.ts +4 -1
  19. package/packages/bundled-features/src/auth-email-password/handlers/login.write.ts +1 -0
  20. package/packages/bundled-features/src/auth-email-password/handlers/reset-password.write.ts +1 -0
  21. package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +7 -3
  22. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +53 -96
  23. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +1 -0
  24. package/packages/bundled-features/src/auth-email-password/handlers/verify-email.write.ts +1 -0
  25. package/packages/bundled-features/src/auth-email-password/index.ts +12 -0
  26. package/packages/bundled-features/src/auth-email-password/seeding.ts +38 -7
  27. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +83 -0
  28. package/packages/bundled-features/src/auth-email-password/signup-token-store.ts +9 -3
  29. package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +7 -7
  30. package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +15 -0
  31. package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +2 -2
  32. package/packages/bundled-features/src/auth-email-password/web/user-menu.tsx +1 -1
  33. package/packages/bundled-features/src/auth-mfa/changes.json +7 -0
  34. package/packages/bundled-features/src/auth-mfa/feature.ts +5 -0
  35. package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +1 -0
  36. package/packages/bundled-features/src/auth-mfa/handlers/verify.write.ts +1 -0
  37. package/packages/bundled-features/src/auth-mfa/screens.ts +2 -2
  38. package/packages/bundled-features/src/auth-mfa/web/mfa-recovery-codes-reveal.tsx +6 -8
  39. package/packages/bundled-features/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +18 -21
  40. package/packages/bundled-features/src/auth-mfa/web/mfa-verify-screen.tsx +2 -2
  41. package/packages/bundled-features/src/billing-foundation/billing-info-query.ts +7 -8
  42. package/packages/bundled-features/src/billing-foundation/changes.json +6 -0
  43. package/packages/bundled-features/src/billing-foundation/checkout-core.ts +14 -2
  44. package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts +7 -1
  45. package/packages/bundled-features/src/cap-overview/access.ts +2 -9
  46. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +5 -2
  47. package/packages/bundled-features/src/cap-overview/handlers/tenant-options.query.ts +6 -9
  48. package/packages/bundled-features/src/channel-in-app/changes.json +8 -1
  49. package/packages/bundled-features/src/channel-in-app/in-app-channel.ts +1 -1
  50. package/packages/bundled-features/src/channel-in-app/tables.ts +18 -12
  51. package/packages/bundled-features/src/compliance-profiles-ops/handlers/tenants-missing-profile.query.ts +5 -4
  52. package/packages/bundled-features/src/config/handlers/values.query.ts +7 -2
  53. package/packages/bundled-features/src/crypto-shredding/changes.json +12 -0
  54. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +154 -69
  55. package/packages/bundled-features/src/delivery/changes.json +6 -0
  56. package/packages/bundled-features/src/delivery/db/queries/address-opt-outs.ts +18 -12
  57. package/packages/bundled-features/src/delivery/delivery-service.ts +103 -16
  58. package/packages/bundled-features/src/delivery/public-names.ts +1 -1
  59. package/packages/bundled-features/src/delivery/unsubscribe.ts +81 -13
  60. package/packages/bundled-features/src/feature-toggles/feature.ts +2 -3
  61. package/packages/bundled-features/src/feature-toggles/toggle-runtime.ts +34 -5
  62. package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +31 -8
  63. package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +10 -1
  64. package/packages/bundled-features/src/files/changes.json +24 -0
  65. package/packages/bundled-features/src/files-tenant-data/changes.json +5 -0
  66. package/packages/bundled-features/src/files-tenant-data/handlers/sweep-orphaned-derivatives.job.ts +4 -0
  67. package/packages/bundled-features/src/files-tenant-data/hooks.ts +75 -25
  68. package/packages/bundled-features/src/folders/changes.json +6 -0
  69. package/packages/bundled-features/src/folders/feature.ts +2 -12
  70. package/packages/bundled-features/src/folders/handlers/delete-folder.write.ts +41 -9
  71. package/packages/bundled-features/src/form-draft/changes.json +7 -1
  72. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +4 -2
  73. package/packages/bundled-features/src/form-draft/handlers/cleanup.job.ts +18 -6
  74. package/packages/bundled-features/src/foundation-shared/index.ts +1 -0
  75. package/packages/bundled-features/src/foundation-shared/mail-host-policy.ts +16 -4
  76. package/packages/bundled-features/src/inbound-provider-imap/changes.json +6 -0
  77. package/packages/bundled-features/src/inbound-provider-imap/feature.ts +2 -0
  78. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +3 -4
  79. package/packages/bundled-features/src/jobs/changes.json +30 -0
  80. package/packages/bundled-features/src/jobs/constants.ts +1 -0
  81. package/packages/bundled-features/src/jobs/db/queries/retention.ts +12 -0
  82. package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +26 -2
  83. package/packages/bundled-features/src/jobs/feature.ts +7 -1
  84. package/packages/bundled-features/src/jobs/handlers/list.query.ts +45 -12
  85. package/packages/bundled-features/src/jobs/handlers/tenant-failures.query.ts +1 -1
  86. package/packages/bundled-features/src/jobs/handlers/tenant-runs.query.ts +94 -0
  87. package/packages/bundled-features/src/jobs/i18n.ts +1 -0
  88. package/packages/bundled-features/src/jobs/index.ts +2 -0
  89. package/packages/bundled-features/src/jobs/job-run-logger.ts +177 -10
  90. package/packages/bundled-features/src/jobs/tenant-job-failure-table.ts +27 -8
  91. package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +55 -0
  92. package/packages/bundled-features/src/ledger/changes.json +23 -0
  93. package/packages/bundled-features/src/ledger/entity.ts +17 -5
  94. package/packages/bundled-features/src/ledger/feature.ts +3 -0
  95. package/packages/bundled-features/src/ledger/handlers/confirm-schedule-period.write.ts +15 -3
  96. package/packages/bundled-features/src/ledger/handlers/create-transaction.write.ts +1 -0
  97. package/packages/bundled-features/src/ledger/handlers/reverse-transaction.write.ts +1 -0
  98. package/packages/bundled-features/src/ledger/schedule-subject-pair.ts +27 -0
  99. package/packages/bundled-features/src/ledger/schemas.ts +8 -2
  100. package/packages/bundled-features/src/mail-transport-smtp/feature.ts +4 -15
  101. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  102. package/packages/bundled-features/src/managed-pages/feature.ts +24 -11
  103. package/packages/bundled-features/src/notes-history/changes.json +13 -0
  104. package/packages/bundled-features/src/notes-history/entity.ts +4 -0
  105. package/packages/bundled-features/src/notes-history/feature.ts +2 -12
  106. package/packages/bundled-features/src/personal-access-tokens/handlers/create.write.ts +1 -0
  107. package/packages/bundled-features/src/personal-access-tokens/screens.ts +24 -3
  108. package/packages/bundled-features/src/seo/changes.json +5 -0
  109. package/packages/bundled-features/src/seo/feature.ts +23 -7
  110. package/packages/bundled-features/src/sessions/feature.ts +21 -8
  111. package/packages/bundled-features/src/sessions/handlers/detail.query.ts +34 -33
  112. package/packages/bundled-features/src/sessions/handlers/list.query.ts +68 -65
  113. package/packages/bundled-features/src/sessions/screens.ts +10 -8
  114. package/packages/bundled-features/src/shared/assert-no-where-rule-in-ownership-write.ts +25 -0
  115. package/packages/bundled-features/src/shared/encrypt-for-direct-write.ts +15 -8
  116. package/packages/bundled-features/src/shared/index.ts +4 -2
  117. package/packages/bundled-features/src/shared/parent-visibility.ts +31 -1
  118. package/packages/bundled-features/src/shared/rate-limited-text-response.ts +11 -0
  119. package/packages/bundled-features/src/shared/row-bound-grant.ts +18 -2
  120. package/packages/bundled-features/src/shared/session-locale-field.ts +7 -0
  121. package/packages/bundled-features/src/tags/feature.ts +2 -12
  122. package/packages/bundled-features/src/template-resolver/api.ts +3 -3
  123. package/packages/bundled-features/src/template-resolver/changes.json +5 -0
  124. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +2 -2
  125. package/packages/bundled-features/src/tenant/changes.json +19 -0
  126. package/packages/bundled-features/src/tenant/decrypt-tenant-names.ts +17 -0
  127. package/packages/bundled-features/src/tenant/feature.ts +8 -1
  128. package/packages/bundled-features/src/tenant/handlers/me.query.ts +4 -1
  129. package/packages/bundled-features/src/tenant/handlers/memberships.query.ts +7 -3
  130. package/packages/bundled-features/src/tenant/handlers/tenant-directory.query.ts +11 -5
  131. package/packages/bundled-features/src/tenant/handlers/toggle-enabled.write.ts +2 -0
  132. package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +1 -0
  133. package/packages/bundled-features/src/tenant/index.ts +1 -0
  134. package/packages/bundled-features/src/tenant/invitation-table.ts +6 -0
  135. package/packages/bundled-features/src/tenant/invited-membership.ts +45 -12
  136. package/packages/bundled-features/src/tenant/schema/tenant.ts +4 -6
  137. package/packages/bundled-features/src/tenant/screens.ts +1 -1
  138. package/packages/bundled-features/src/tenant-handover/changes.json +12 -0
  139. package/packages/bundled-features/src/tenant-handover/move-entity-graph.ts +12 -19
  140. package/packages/bundled-features/src/tenant-handover/transfer-graph.ts +14 -96
  141. package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +16 -0
  142. package/packages/bundled-features/src/tenant-lifecycle/changes.json +22 -0
  143. package/packages/bundled-features/src/tenant-lifecycle/constants.ts +8 -0
  144. package/packages/bundled-features/src/tenant-lifecycle/events.ts +6 -0
  145. package/packages/bundled-features/src/tenant-lifecycle/feature.ts +14 -1
  146. package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +105 -29
  147. package/packages/bundled-features/src/tenant-lifecycle/stages.ts +64 -10
  148. package/packages/bundled-features/src/tenant-lifecycle/testing.ts +58 -0
  149. package/packages/bundled-features/src/tier-engine/changes.json +5 -0
  150. package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +1 -0
  151. package/packages/bundled-features/src/user/changes.json +15 -0
  152. package/packages/bundled-features/src/user/handlers/detail.query.ts +1 -0
  153. package/packages/bundled-features/src/user/handlers/list.query.ts +8 -2
  154. package/packages/bundled-features/src/user/handlers/update.write.ts +1 -0
  155. package/packages/bundled-features/src/user/seeding.ts +34 -0
  156. package/packages/bundled-features/src/user-data-rights/boot-checks.ts +7 -0
  157. package/packages/bundled-features/src/user-data-rights/changes.json +6 -0
  158. package/packages/bundled-features/src/user-data-rights/deletion-token.ts +1 -1
  159. package/packages/bundled-features/src/user-data-rights/handlers/confirm-deletion-by-token.write.ts +2 -2
  160. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +1 -0
  161. package/packages/bundled-features/src/user-data-rights/index.ts +1 -0
  162. package/packages/bundled-features/src/user-data-rights/schema/download-attempt.ts +17 -9
  163. package/packages/bundled-features/src/user-profile/handlers/change-email.write.ts +8 -2
  164. package/packages/bundled-features/src/workflow-runner/changes.json +5 -0
  165. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +80 -14
  166. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +11 -1
  167. package/packages/bundled-features/src/workflow-runner/runner.ts +8 -2
  168. package/samples/apps/showcase/src/features/items/feature.ts +2 -1
  169. package/samples/apps/showcase/src/features/items/schema/item.ts +0 -1
  170. package/samples/apps/styleguide/src/features/widgets/demo-locale.ts +47 -0
  171. package/samples/apps/styleguide/src/features/widgets/feature.ts +131 -15
  172. package/samples/apps/styleguide/src/features/widgets/i18n.ts +40 -0
  173. package/samples/apps/styleguide/src/features/widgets/web/DashboardFilterEcho.tsx +5 -4
  174. package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +4 -0
  175. package/samples/apps/ui-walkthrough/package.json +3 -0
  176. package/samples/apps/use-all-bundled/README.md +1 -2
  177. package/samples/apps/use-all-bundled/feature-manifest.json +1 -1
  178. package/samples/apps/user-data-rights-demo/README.md +1 -3
  179. package/samples/recipes/legal-pages/README.md +3 -4
  180. package/samples/recipes/managed-pages/README.md +2 -2
  181. package/samples/recipes/public-share-token/README.md +4 -5
  182. package/samples/recipes/public-share-token/src/feature.ts +1 -1
  183. package/samples/recipes/security-baseline/README.md +6 -0
  184. package/samples/recipes/seed-migration/README.md +0 -2
  185. package/samples/recipes/tenant-settings/README.md +1 -1
  186. package/packages/bundled-features/src/shared/has-where-rule.ts +0 -11
@@ -1,5 +1,6 @@
1
1
  import { computeRevisionEtag, etagMatches } from "@cosmicdrift/kumiko-framework/api";
2
2
  import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { RateLimitError } from "@cosmicdrift/kumiko-framework/errors";
3
4
  import {
4
5
  type BrandingTokens,
5
6
  cachedSecurePageResponse,
@@ -7,7 +8,7 @@ import {
7
8
  renderSafeMarkdown,
8
9
  wrapInLayout,
9
10
  } from "../page-render/index.js";
10
- import type { SystemQueryFn } from "../shared/index.js";
11
+ import { rateLimitedTextResponse, type SystemQueryFn } from "../shared/index.js";
11
12
  import { BRANDING_KEYS, BRANDING_QUERY_QN, CUSTOM_CSS_KEY, coerceBranding } from "./branding.js";
12
13
  import { createBrandingQuery } from "./handlers/branding.query.js";
13
14
  import { bySlugQuery } from "./handlers/by-slug.query.js";
@@ -51,16 +52,18 @@ function brandingRevisionSeed(branding: BrandingTokens): string {
51
52
  ]);
52
53
  }
53
54
 
54
- // Never throws: a query failure or malformed result degrades to the
55
- // unbranded default (branding is decoration, not a hard dependency of the
56
- // page render).
55
+ // A query failure or malformed result degrades to the unbranded default
56
+ // (branding is decoration, not a hard dependency of the page render). A rate
57
+ // limit is rethrown so the route answers 429 instead of serving (and caching)
58
+ // an unbranded page.
57
59
  async function readBrandingViaSystemQuery(
58
60
  systemQuery: SystemQueryFn,
59
61
  tenantId: string,
60
62
  ): Promise<BrandingTokens> {
61
63
  try {
62
64
  return coerceBranding(await systemQuery(BRANDING_QUERY_QN, {}, tenantId));
63
- } catch {
65
+ } catch (error) {
66
+ if (error instanceof RateLimitError) throw error;
64
67
  return EMPTY_BRANDING;
65
68
  }
66
69
  }
@@ -207,12 +210,22 @@ export function createManagedPagesFeature(opts: ManagedPagesOptions): FeatureDef
207
210
  // von resolverTrust: "authoritative" zurecht abgelehnt; siehe
208
211
  // systemQuery's Doc in http-route.ts). Branding ist Deko → ein
209
212
  // Fehlschlag degradiert zum unbranded Default, blockt nie die Page.
210
- const [pageSettled, brandingResult] = await Promise.all([
211
- systemQuery(BY_SLUG_QN, { slug, lang }, tenantId)
212
- .then((data) => ({ ok: true as const, data: data as BySlugQueryResult }))
213
- .catch(() => ({ ok: false as const, data: null as BySlugQueryResult })),
214
- readBrandingViaSystemQuery(systemQuery, tenantId),
215
- ]);
213
+ let pageSettled: { ok: true; data: BySlugQueryResult } | { ok: false };
214
+ let brandingResult: BrandingTokens;
215
+ try {
216
+ [pageSettled, brandingResult] = await Promise.all([
217
+ systemQuery(BY_SLUG_QN, { slug, lang }, tenantId)
218
+ .then((data) => ({ ok: true as const, data: data as BySlugQueryResult }))
219
+ .catch((error: unknown) => {
220
+ if (error instanceof RateLimitError) throw error;
221
+ return { ok: false as const };
222
+ }),
223
+ readBrandingViaSystemQuery(systemQuery, tenantId),
224
+ ]);
225
+ } catch (error) {
226
+ if (error instanceof RateLimitError) return rateLimitedTextResponse(error);
227
+ throw error;
228
+ }
216
229
  if (!pageSettled.ok) return c.text("page unavailable", 503);
217
230
 
218
231
  const data = pageSettled.data;
@@ -1,4 +1,17 @@
1
1
  [
2
+ {
3
+ "version": "0.354.0",
4
+ "type": "breaking",
5
+ "title": "notes-history adds a composite index on (tenantId, entityType, entityId) for note entries, so the schema-drift check fails until the app regenerates",
6
+ "detail": "Review batch H2: framework parts of consumer-app findings.\n- `file-derivatives`: the `isPublic` predicate args carry `fileRefId`, so a predicate can reject a client-spoofed FileRef that claims another entity's `entityId`/`fieldName`.\n- `sessions`: `createSessionsFeature({ adminAccess: \"systemAdmin\" })` narrows the admin list/detail queries and both admin screens together (default `\"admin\"`, unchanged).\n- `user-profile`: `change-email` is open to every signed-in user (`openToAll`), so a tenant member whose only role is `TenantAdmin` can change their own email; the handler still re-checks the password.\n- `notes-history`: composite index on `(tenantId, entityType, entityId)` for note entries. Apps run `kumiko-schema generate` after the bump.\n- guards: `// kumiko-lint-ignore a,b reason` suppresses several guards on one line (`lineHasIgnoreTag`); `primitives-discipline` and `no-custom-primitives` honour it.\n- dev-server: `kumiko-build --check` verifies `.kumiko/` is current without writing (exit 1 on drift); `runCodegen` takes `checkOnly`.",
7
+ "migration": "Run `kumiko-schema generate` in apps that mount notes-history and commit the generated migration."
8
+ },
9
+ {
10
+ "version": "0.354.0",
11
+ "type": "fix",
12
+ "title": "read_note_mentions gets a tenant, subject index",
13
+ "migration": "Run `kumiko schema generate`; it emits an in-place CREATE INDEX with no DROP and no rebuild."
14
+ },
2
15
  {
3
16
  "version": "0.338.0",
4
17
  "type": "improvement",
@@ -91,6 +91,8 @@ export function createNoteEntryEntity(
91
91
  find: "none",
92
92
  }),
93
93
  },
94
+ // Every host detail screen reads its notes by tenant + host entity.
95
+ indexes: [{ columns: ["tenantId", "entityType", "entityId"] }],
94
96
  });
95
97
  }
96
98
 
@@ -122,6 +124,8 @@ export function createNoteMentionEntity() {
122
124
  }),
123
125
  subjectId: createTextField({ required: true, maxLength: 64, personal: "ref" }),
124
126
  },
127
+ // The forget cascade looks mentions up by subject within one tenant.
128
+ indexes: [{ columns: ["tenantId", "subjectId"] }],
125
129
  });
126
130
  }
127
131
 
@@ -20,7 +20,7 @@ import {
20
20
  type EntityDefinition,
21
21
  type FeatureRegistrar,
22
22
  } from "@cosmicdrift/kumiko-framework/engine";
23
- import { hasWhereRule } from "../shared/index.js";
23
+ import { assertNoWhereRuleInOwnershipWrite } from "../shared/index.js";
24
24
  import { DEFAULT_NOTES_HISTORY_ACCESS, NOTES_HISTORY_FEATURE_NAME } from "./constants.js";
25
25
  import { createNoteEntryEntity, noteMentionEntity } from "./entity.js";
26
26
  import { createNoteEntryExecutor } from "./executor.js";
@@ -123,17 +123,7 @@ export function createNotesHistoryFeature(
123
123
  ) {
124
124
  return notesHistoryFeature;
125
125
  }
126
- if (hasWhereRule(opts.ownership?.write)) {
127
- throw new Error(
128
- "createNotesHistoryFeature({ ownership }): ownership.write must not contain a " +
129
- '`{ kind: "where" }` rule — where-rules are evaluated only at the SQL ' +
130
- "layer (the read path, via buildOwnershipClause). Write paths that " +
131
- "consult access.write (userCanCreateFieldRow/userCanWriteFieldRow) can't " +
132
- "evaluate them, so such a rule can only ever deny — boot validation " +
133
- "rejects it too (fw#2626). Use a `from()` rule for ownership.write, or " +
134
- "leave it unset.",
135
- );
136
- }
126
+ assertNoWhereRuleInOwnershipWrite("createNotesHistoryFeature", opts.ownership?.write);
137
127
  // A mount that accepts no parents at all can never take a note write —
138
128
  // that's a config mistake, not a valid allowlist. Omit `parents` instead
139
129
  // of passing an empty array.
@@ -66,6 +66,7 @@ export function createPatCreateHandler(opts: CreatePatOptions = {}) {
66
66
  },
67
67
  },
68
68
  escapeHatch: {
69
+ grants: ["systemIdentity"],
69
70
  reason:
70
71
  "Reads the caller's own passwordHash (privileged-only field) via ctx.queryAs(SYSTEM, " +
71
72
  "user:findForAuth) to re-verify the current password before minting the token. Also " +
@@ -4,10 +4,25 @@ import {
4
4
  type ProjectionListScreenDefinition,
5
5
  type SecretMintScreenDefinition,
6
6
  } from "@cosmicdrift/kumiko-framework/engine";
7
- import { PAT_MINT_SCREEN_ID, PAT_SCREEN_ID, PatHandlers, PatQueries } from "./constants.js";
7
+ import { ACTION_FORM_ENTITY } from "@cosmicdrift/kumiko-framework/ui-types";
8
+ import { fieldOptionLabelKeyPrefix } from "@cosmicdrift/kumiko-headless";
9
+ import {
10
+ PAT_FEATURE,
11
+ PAT_MINT_SCREEN_ID,
12
+ PAT_SCREEN_ID,
13
+ PatHandlers,
14
+ PatQueries,
15
+ } from "./constants.js";
8
16
  import type { PatScopeConfig } from "./scopes.js";
9
17
 
10
18
  const PAT_STATUS_OPTION_KEY_PREFIX = "pat.list.status.";
19
+ // Same keys the mint form's `scopes` multiSelect options resolve through
20
+ // (see patScopeOptionTranslations), so list and form show identical labels.
21
+ const PAT_SCOPE_OPTION_KEY_PREFIX = fieldOptionLabelKeyPrefix(
22
+ PAT_FEATURE,
23
+ ACTION_FORM_ENTITY,
24
+ "scopes",
25
+ );
11
26
 
12
27
  // Both screens render only for tenants whose tier includes the feature; the
13
28
  // availability query is rejected by the feature gate otherwise.
@@ -29,11 +44,17 @@ export const patListScreen: ProjectionListScreenDefinition = {
29
44
  defaultSort: { field: "createdAt", dir: "desc" },
30
45
  columns: [
31
46
  { field: "name", label: i18nKey("pat.list.col.name") },
32
- { field: "prefix", label: i18nKey("pat.list.col.prefix") },
33
- { field: "scopes", label: i18nKey("pat.list.col.scopes") },
47
+ { field: "prefix", label: i18nKey("pat.list.col.prefix"), sortable: false },
48
+ {
49
+ field: "scopes",
50
+ label: i18nKey("pat.list.col.scopes"),
51
+ sortable: false,
52
+ renderer: { format: "enumOption", keyPrefix: PAT_SCOPE_OPTION_KEY_PREFIX },
53
+ },
34
54
  {
35
55
  field: "status",
36
56
  label: i18nKey("pat.list.col.status"),
57
+ sortable: false,
37
58
  renderer: { format: "enumOption", keyPrefix: PAT_STATUS_OPTION_KEY_PREFIX },
38
59
  },
39
60
  {
@@ -1,4 +1,9 @@
1
1
  [
2
+ {
3
+ "version": "0.353.0",
4
+ "type": "fix",
5
+ "title": "sitemap.xml and llms.txt answer 429 when the managed-pages read is rate limited instead of omitting the pages"
6
+ },
2
7
  {
3
8
  "version": "0.147.3",
4
9
  "type": "fix",
@@ -5,9 +5,10 @@ import {
5
5
  type FeatureDefinition,
6
6
  SYSTEM_TENANT_ID,
7
7
  } from "@cosmicdrift/kumiko-framework/engine";
8
+ import { RateLimitError } from "@cosmicdrift/kumiko-framework/errors";
8
9
  import { LEGAL_ROUTES } from "../legal-pages/index.js";
9
10
  import { cachedSecurePageResponse } from "../page-render/index.js";
10
- import type { SystemQueryFn } from "../shared/index.js";
11
+ import { rateLimitedTextResponse, type SystemQueryFn } from "../shared/index.js";
11
12
  import { SEO_CONFIG_KEYS, SEO_DEFAULT_PATHS } from "./constants.js";
12
13
  import { seoConfigQuery } from "./handlers/seo-config.query.js";
13
14
  import { buildLlmsTxt } from "./llms-txt.js";
@@ -125,7 +126,9 @@ async function gatherEntries(
125
126
  lastmod: page.updatedAt,
126
127
  });
127
128
  }
128
- } catch {
129
+ } catch (error) {
130
+ // A rate limit must not look like "pages removed" to a crawler.
131
+ if (error instanceof RateLimitError) throw error;
129
132
  // managed-pages unreachable/not mounted — degrade to callback-only entries.
130
133
  }
131
134
  }
@@ -188,7 +191,13 @@ export function createSeoFeature(opts: SeoOptions): FeatureDefinition {
188
191
  anonymous: true,
189
192
  handler: async (c, { systemQuery }) => {
190
193
  const { origin, host } = requestHost(c);
191
- const entries = await gatherEntries(opts, systemQuery, origin, host);
194
+ let entries: SitemapEntry[];
195
+ try {
196
+ entries = await gatherEntries(opts, systemQuery, origin, host);
197
+ } catch (error) {
198
+ if (error instanceof RateLimitError) return rateLimitedTextResponse(error);
199
+ throw error;
200
+ }
192
201
  const xml = buildSitemapXml(entries);
193
202
  const etag = computeRevisionEtag([host, xml]);
194
203
  return cachedSecurePageResponse(c.req.raw, {
@@ -209,10 +218,17 @@ export function createSeoFeature(opts: SeoOptions): FeatureDefinition {
209
218
  const tenantId = opts.managedPages
210
219
  ? ((await opts.managedPages.resolveApexTenant(host)) ?? SYSTEM_TENANT_ID)
211
220
  : SYSTEM_TENANT_ID;
212
- const [entries, seoConfig] = await Promise.all([
213
- gatherEntries(opts, systemQuery, origin, host),
214
- readSeoConfig(systemQuery, tenantId),
215
- ]);
221
+ let entries: SitemapEntry[];
222
+ let seoConfig: SeoConfigValues;
223
+ try {
224
+ [entries, seoConfig] = await Promise.all([
225
+ gatherEntries(opts, systemQuery, origin, host),
226
+ readSeoConfig(systemQuery, tenantId),
227
+ ]);
228
+ } catch (error) {
229
+ if (error instanceof RateLimitError) return rateLimitedTextResponse(error);
230
+ throw error;
231
+ }
216
232
  const sections =
217
233
  entries.length > 0
218
234
  ? [
@@ -4,18 +4,26 @@ import {
4
4
  type SessionStoreProvider,
5
5
  } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
6
6
  import { type DbConnection, deriveEntityTableMeta } from "@cosmicdrift/kumiko-framework/db";
7
- import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
7
+ import {
8
+ access,
9
+ defineFeature,
10
+ type FeatureDefinition,
11
+ } from "@cosmicdrift/kumiko-framework/engine";
8
12
  import { USER_SESSION_ENTITY_NAME } from "./constants.js";
9
13
  import { cleanupJob } from "./handlers/cleanup.job.js";
10
- import { detailQuery } from "./handlers/detail.query.js";
11
- import { listQuery } from "./handlers/list.query.js";
14
+ import { createDetailQuery } from "./handlers/detail.query.js";
15
+ import { createListQuery } from "./handlers/list.query.js";
12
16
  import { mineQuery } from "./handlers/mine.query.js";
13
17
  import { revokeWrite } from "./handlers/revoke.write.js";
14
18
  import { revokeAllForUserWrite } from "./handlers/revoke-all-for-user.write.js";
15
19
  import { revokeAllOthersWrite } from "./handlers/revoke-all-others.write.js";
16
20
  import { SESSIONS_I18N } from "./i18n.js";
17
21
  import { userSessionEntity } from "./schema/user-session.js";
18
- import { sessionDetailScreen, sessionListScreen, sessionMineScreen } from "./screens.js";
22
+ import {
23
+ createSessionDetailScreen,
24
+ createSessionListScreen,
25
+ sessionMineScreen,
26
+ } from "./screens.js";
19
27
  import {
20
28
  createSessionCallbacks,
21
29
  type SessionAllOthersRevoker,
@@ -45,6 +53,10 @@ export type SessionsFeatureOptions = {
45
53
  // Session JWT / store_user_sessions TTL. Was previously auth.sessions.expiresInMs
46
54
  // (#1372) — lives on the sessions feature now that sessionStore is the wiring.
47
55
  readonly expiresInMs?: number;
56
+ // Access preset for the admin list/detail queries and screens; they expose decrypted
57
+ // IP/user-agent of every session in the tenant. A preset name, not a role array, because
58
+ // dedupeOptions must stay primitive.
59
+ readonly adminAccess?: "admin" | "systemAdmin";
48
60
  };
49
61
 
50
62
  export type BindAutoRevokeOnPasswordChange = (revoker: SessionMassRevoker) => void;
@@ -89,6 +101,7 @@ export function createSessionsFeature(options?: SessionsFeatureOptions): Feature
89
101
  return defineFeature(
90
102
  "sessions",
91
103
  (r) => {
104
+ const adminRoles = access[options?.adminAccess ?? "admin"];
92
105
  r.describe(
93
106
  "Tracks signed-in clients in the `store_user_sessions` table (one row per JWT, keyed by the `sid`/`jti` claim) and exposes handlers for `mine` (list your sessions), `revoke`, and `revokeAllOthers`. Session creation and revocation on the hot auth path are handled by `createSessionCallbacks()`, wired into `buildServer({ auth: { ... } })` outside the dispatcher; the same callbacks are also registered as an auth-foundation sessionStore provider, resolvable generically via `resolveSessionStore()`. The feature also ships a manual-trigger cleanup job for pruning expired rows and an optional `autoRevokeOnPasswordChange` hook that mass-revokes all sessions for a user whenever their `passwordHash` changes.",
94
107
  );
@@ -162,8 +175,8 @@ export function createSessionsFeature(options?: SessionsFeatureOptions): Feature
162
175
 
163
176
  const queries = {
164
177
  mine: r.queryHandler(mineQuery),
165
- list: r.queryHandler(listQuery),
166
- detail: r.queryHandler(detailQuery),
178
+ list: r.queryHandler(createListQuery(adminRoles)),
179
+ detail: r.queryHandler(createDetailQuery(adminRoles)),
167
180
  };
168
181
 
169
182
  // Retention: chunked DELETE of expired/revoked rows. Manual trigger
@@ -222,8 +235,8 @@ export function createSessionsFeature(options?: SessionsFeatureOptions): Feature
222
235
 
223
236
  r.translations({ keys: SESSIONS_I18N });
224
237
 
225
- r.screen(sessionListScreen);
226
- r.screen(sessionDetailScreen);
238
+ r.screen(createSessionListScreen(adminRoles));
239
+ r.screen(createSessionDetailScreen(adminRoles));
227
240
  r.screen(sessionMineScreen);
228
241
  r.nav({
229
242
  id: "session-list",
@@ -16,36 +16,37 @@ const sessionRowSchema = z.object({
16
16
  userAgent: z.string().nullable(),
17
17
  });
18
18
 
19
- export const detailQuery = defineQueryHandler({
20
- name: "user-session:detail",
21
- schema: z.object({ id: z.uuid() }),
22
- access: { roles: access.admin },
23
- description:
24
- "Loads one session of the active tenant by its id with owner, timestamps and decrypted IP/user-agent; use it when an admin investigates a specific session found through the session list.",
25
- // Decrypted IP and user-agent.
26
- agent: { risk: "high" },
27
- outputSchema: sessionRowSchema.nullable(),
28
- handler: async (query, ctx) => {
29
- const row = await fetchOne<{
30
- id: string;
31
- userId: string;
32
- createdAt: unknown;
33
- expiresAt: unknown;
34
- revokedAt: unknown;
35
- ip: string | null;
36
- userAgent: string | null;
37
- }>(ctx.db, userSessionTable, { id: query.payload.id });
38
- if (!row) return null;
39
- return {
40
- id: row.id,
41
- userId: row.userId,
42
- createdAt: row.createdAt,
43
- expiresAt: row.expiresAt,
44
- revokedAt: row.revokedAt,
45
- ip: row.ip ? await decryptStoredPii(row.ip, "ip", "sessions:detail") : row.ip,
46
- userAgent: row.userAgent
47
- ? await decryptStoredPii(row.userAgent, "userAgent", "sessions:detail")
48
- : row.userAgent,
49
- };
50
- },
51
- });
19
+ export const createDetailQuery = (adminRoles: readonly string[] = access.admin) =>
20
+ defineQueryHandler({
21
+ name: "user-session:detail",
22
+ schema: z.object({ id: z.uuid() }),
23
+ access: { roles: adminRoles },
24
+ description:
25
+ "Loads one session of the active tenant by its id with owner, timestamps and decrypted IP/user-agent; use it when an admin investigates a specific session found through the session list.",
26
+ // Decrypted IP and user-agent.
27
+ agent: { risk: "high" },
28
+ outputSchema: sessionRowSchema.nullable(),
29
+ handler: async (query, ctx) => {
30
+ const row = await fetchOne<{
31
+ id: string;
32
+ userId: string;
33
+ createdAt: unknown;
34
+ expiresAt: unknown;
35
+ revokedAt: unknown;
36
+ ip: string | null;
37
+ userAgent: string | null;
38
+ }>(ctx.db, userSessionTable, { id: query.payload.id });
39
+ if (!row) return null;
40
+ return {
41
+ id: row.id,
42
+ userId: row.userId,
43
+ createdAt: row.createdAt,
44
+ expiresAt: row.expiresAt,
45
+ revokedAt: row.revokedAt,
46
+ ip: row.ip ? await decryptStoredPii(row.ip, "ip", "sessions:detail") : row.ip,
47
+ userAgent: row.userAgent
48
+ ? await decryptStoredPii(row.userAgent, "userAgent", "sessions:detail")
49
+ : row.userAgent,
50
+ };
51
+ },
52
+ });
@@ -25,68 +25,71 @@ function isSortableColumn(value: string): value is SortableColumn {
25
25
  // tenantId column. Includes revoked rows; UI shows revokedAt distinct.
26
26
  // No cursor — nextCursor is always null; `limit` only caps the page size,
27
27
  // it doesn't offset into a further one.
28
- export const listQuery = definePagedQueryHandler({
29
- name: "user-session:list",
30
- schema: z.object({
31
- limit: z.number().int().nonnegative().max(MAX_LIST_LIMIT).optional(),
32
- sort: z.string().optional(),
33
- sortDirection: z.enum(["asc", "desc"]).optional(),
34
- }),
35
- access: { roles: access.admin },
36
- description:
37
- "Lists every session in the active tenant, live and revoked, sortable by id, user, creation, expiry or revocation, for an admin auditing who is signed in.",
38
- outputSchema: z.object({
39
- rows: z.array(
40
- z.object({
41
- id: z.string(),
42
- userId: z.string(),
43
- createdAt: z.unknown(),
44
- expiresAt: z.unknown(),
45
- revokedAt: z.unknown(),
46
- ip: z.string().nullable(),
47
- userAgent: z.string().nullable(),
48
- }),
49
- ),
50
- nextCursor: z.string().nullable(),
51
- }),
52
- handler: async (query, ctx) => {
53
- const requestedSort = query.payload.sort;
54
- const sortColumn: SortableColumn =
55
- requestedSort !== undefined && isSortableColumn(requestedSort) ? requestedSort : "createdAt";
56
- const rows = await selectMany<{
57
- id: string;
58
- userId: string;
59
- createdAt: unknown;
60
- expiresAt: unknown;
61
- revokedAt: unknown;
62
- ip: string | null;
63
- userAgent: string | null;
64
- }>(ctx.db, userSessionTable, undefined, {
65
- // `id` as a tie-breaker keeps row order (and, with `limit` set, row
66
- // selection) deterministic across identical requests — sortColumn
67
- // alone isn't unique (e.g. many NULL revokedAt, or equal timestamps).
68
- orderBy:
69
- sortColumn === "id"
70
- ? { col: "id", direction: query.payload.sortDirection ?? "desc" }
71
- : [
72
- { col: sortColumn, direction: query.payload.sortDirection ?? "desc" },
73
- { col: "id", direction: "asc" },
74
- ],
75
- ...(query.payload.limit !== undefined && { limit: query.payload.limit }),
76
- });
77
- const decryptedRows = await Promise.all(
78
- rows.map(async (r) => ({
79
- id: r.id,
80
- userId: r.userId,
81
- createdAt: r.createdAt,
82
- expiresAt: r.expiresAt,
83
- revokedAt: r.revokedAt,
84
- ip: r.ip ? await decryptStoredPii(r.ip, "ip", "sessions:list") : r.ip,
85
- userAgent: r.userAgent
86
- ? await decryptStoredPii(r.userAgent, "userAgent", "sessions:list")
87
- : r.userAgent,
88
- })),
89
- );
90
- return { rows: decryptedRows, nextCursor: null };
91
- },
92
- });
28
+ export const createListQuery = (adminRoles: readonly string[] = access.admin) =>
29
+ definePagedQueryHandler({
30
+ name: "user-session:list",
31
+ schema: z.object({
32
+ limit: z.number().int().nonnegative().max(MAX_LIST_LIMIT).optional(),
33
+ sort: z.string().optional(),
34
+ sortDirection: z.enum(["asc", "desc"]).optional(),
35
+ }),
36
+ access: { roles: adminRoles },
37
+ description:
38
+ "Lists every session in the active tenant, live and revoked, sortable by id, user, creation, expiry or revocation, for an admin auditing who is signed in.",
39
+ outputSchema: z.object({
40
+ rows: z.array(
41
+ z.object({
42
+ id: z.string(),
43
+ userId: z.string(),
44
+ createdAt: z.unknown(),
45
+ expiresAt: z.unknown(),
46
+ revokedAt: z.unknown(),
47
+ ip: z.string().nullable(),
48
+ userAgent: z.string().nullable(),
49
+ }),
50
+ ),
51
+ nextCursor: z.string().nullable(),
52
+ }),
53
+ handler: async (query, ctx) => {
54
+ const requestedSort = query.payload.sort;
55
+ const sortColumn: SortableColumn =
56
+ requestedSort !== undefined && isSortableColumn(requestedSort)
57
+ ? requestedSort
58
+ : "createdAt";
59
+ const rows = await selectMany<{
60
+ id: string;
61
+ userId: string;
62
+ createdAt: unknown;
63
+ expiresAt: unknown;
64
+ revokedAt: unknown;
65
+ ip: string | null;
66
+ userAgent: string | null;
67
+ }>(ctx.db, userSessionTable, undefined, {
68
+ // `id` as a tie-breaker keeps row order (and, with `limit` set, row
69
+ // selection) deterministic across identical requests — sortColumn
70
+ // alone isn't unique (e.g. many NULL revokedAt, or equal timestamps).
71
+ orderBy:
72
+ sortColumn === "id"
73
+ ? { col: "id", direction: query.payload.sortDirection ?? "desc" }
74
+ : [
75
+ { col: sortColumn, direction: query.payload.sortDirection ?? "desc" },
76
+ { col: "id", direction: "asc" },
77
+ ],
78
+ ...(query.payload.limit !== undefined && { limit: query.payload.limit }),
79
+ });
80
+ const decryptedRows = await Promise.all(
81
+ rows.map(async (r) => ({
82
+ id: r.id,
83
+ userId: r.userId,
84
+ createdAt: r.createdAt,
85
+ expiresAt: r.expiresAt,
86
+ revokedAt: r.revokedAt,
87
+ ip: r.ip ? await decryptStoredPii(r.ip, "ip", "sessions:list") : r.ip,
88
+ userAgent: r.userAgent
89
+ ? await decryptStoredPii(r.userAgent, "userAgent", "sessions:list")
90
+ : r.userAgent,
91
+ })),
92
+ );
93
+ return { rows: decryptedRows, nextCursor: null };
94
+ },
95
+ });
@@ -13,9 +13,9 @@ import {
13
13
  SessionQueries,
14
14
  } from "./constants.js";
15
15
 
16
- const listAccess = { roles: access.admin };
17
-
18
- export const sessionListScreen: ProjectionListScreenDefinition = {
16
+ export const createSessionListScreen = (
17
+ adminRoles: readonly string[] = access.admin,
18
+ ): ProjectionListScreenDefinition => ({
19
19
  id: SESSION_LIST_SCREEN_ID,
20
20
  type: "projectionList",
21
21
  query: SessionQueries.list,
@@ -61,10 +61,12 @@ export const sessionListScreen: ProjectionListScreenDefinition = {
61
61
  rowClick: true,
62
62
  },
63
63
  ],
64
- access: listAccess,
65
- };
64
+ access: { roles: adminRoles },
65
+ });
66
66
 
67
- export const sessionDetailScreen: ProjectionDetailScreenDefinition = {
67
+ export const createSessionDetailScreen = (
68
+ adminRoles: readonly string[] = access.admin,
69
+ ): ProjectionDetailScreenDefinition => ({
68
70
  id: SESSION_DETAIL_SCREEN_ID,
69
71
  type: "projectionDetail",
70
72
  query: SessionQueries.detail,
@@ -97,8 +99,8 @@ export const sessionDetailScreen: ProjectionDetailScreenDefinition = {
97
99
  ip: i18nKey("sessions.detail.field.ip"),
98
100
  userAgent: i18nKey("sessions.detail.field.userAgent"),
99
101
  },
100
- access: listAccess,
101
- };
102
+ access: { roles: adminRoles },
103
+ });
102
104
 
103
105
  // Revoke is hidden on the current session — it would sign the user out mid-click; logout covers it.
104
106
  export const sessionMineScreen: ProjectionListScreenDefinition = {
@@ -0,0 +1,25 @@
1
+ import type { OwnershipMap } from "@cosmicdrift/kumiko-framework/engine";
2
+
3
+ // Boot validation rejects `{ kind: "where" }` in access.write too (fw#2626);
4
+ // failing at factory time names the option instead of an entity scope.
5
+ export function assertNoWhereRuleInOwnershipWrite(
6
+ factoryName: string,
7
+ writeOwnership: OwnershipMap | undefined,
8
+ ): void {
9
+ // skip: no write ownership declared, nothing to validate
10
+ if (!writeOwnership) return;
11
+ const hasWhereRule = Object.values(writeOwnership).some(
12
+ (rule) => rule !== "all" && rule.kind === "where",
13
+ );
14
+ // skip: no where-rule present, the option is valid
15
+ if (!hasWhereRule) return;
16
+ throw new Error(
17
+ `${factoryName}({ ownership }): ownership.write must not contain a ` +
18
+ '`{ kind: "where" }` rule — where-rules are evaluated only at the SQL ' +
19
+ "layer (the read path, via buildOwnershipClause). Write paths that " +
20
+ "consult access.write (userCanCreateFieldRow/userCanWriteFieldRow) can't " +
21
+ "evaluate them, so such a rule can only ever deny — boot validation " +
22
+ "rejects it too (fw#2626). Use a `from()` rule for ownership.write, or " +
23
+ "leave it unset.",
24
+ );
25
+ }