@cosmicdrift/kumiko-bundled-features 0.192.0 → 0.193.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 (24) hide show
  1. package/package.json +7 -7
  2. package/src/auth-email-password/__tests__/invite-flow-kms.integration.test.ts +43 -1
  3. package/src/file-derivatives/__tests__/public-variant-route.integration.test.ts +237 -0
  4. package/src/file-derivatives/feature.ts +142 -21
  5. package/src/file-derivatives/handlers/public-variant.query.ts +122 -0
  6. package/src/file-derivatives/index.ts +3 -2
  7. package/src/file-derivatives/presets.ts +7 -0
  8. package/src/form-draft/__tests__/cleanup.integration.test.ts +30 -0
  9. package/src/form-draft/__tests__/discard-file-refs.integration.test.ts +26 -1
  10. package/src/form-draft/__tests__/feature-optional-config.integration.test.ts +90 -0
  11. package/src/form-draft/db/queries/cleanup.ts +15 -14
  12. package/src/form-draft/db/queries/owned-file-refs.ts +13 -2
  13. package/src/form-draft/feature.ts +7 -1
  14. package/src/form-draft/handlers/__tests__/cleanup.job.test.ts +55 -0
  15. package/src/form-draft/handlers/cleanup.job.ts +57 -11
  16. package/src/form-draft/handlers/discard.write.ts +1 -0
  17. package/src/form-draft/lookup.ts +2 -0
  18. package/src/form-draft-user-data/__tests__/hooks-delete-failure.integration.test.ts +60 -0
  19. package/src/form-draft-user-data/hooks.ts +12 -1
  20. package/src/template-resolver/handlers/by-slug.query.ts +1 -0
  21. package/src/template-resolver/handlers/collection-shared.ts +2 -0
  22. package/src/template-resolver/web/__tests__/editor-read-only.test.tsx +25 -0
  23. package/src/template-resolver/web/client-plugin.tsx +22 -0
  24. package/src/user-data-rights-defaults/hooks/tenant-invitation.userdata-hook.ts +21 -6
@@ -256,10 +256,30 @@ type TextBlock = {
256
256
  readonly locale: string;
257
257
  readonly title: string | null;
258
258
  readonly content: string | null;
259
+ readonly contentFormat: string;
259
260
  readonly folder: string | null;
260
261
  readonly updatedAt: string;
261
262
  };
262
263
 
264
+ // The collection's declared contentFormat ("plain" | "rich" | "markdown",
265
+ // see ContentCollectionDefinition.contentFormat) uses a different vocabulary
266
+ // than the stored entry's contentFormat ("plain" | "html" | "markdown" |
267
+ // "mjml", see CONTENT_FORMATS) — "rich" always stores HTML. Without this,
268
+ // handleSave omitting contentFormat let the write schema's
269
+ // contentFormatSchema.default("markdown") silently stamp every save as
270
+ // markdown, clobbering e.g. a "plain" entry. Undefined (no collection, or
271
+ // the collection didn't declare a format) keeps whatever was already saved
272
+ // instead of guessing.
273
+ function resolveWriteContentFormat(
274
+ collectionContentFormat: string | undefined,
275
+ loadedContentFormat: string | undefined,
276
+ ): string | undefined {
277
+ if (collectionContentFormat === "plain") return "plain";
278
+ if (collectionContentFormat === "rich") return "html";
279
+ if (collectionContentFormat === "markdown") return "markdown";
280
+ return loadedContentFormat;
281
+ }
282
+
263
283
  type SetResponse = { readonly slug: string; readonly locale: string; readonly isNew: boolean };
264
284
 
265
285
  function TextBlockEditor({
@@ -337,12 +357,14 @@ function TextBlockEditor({
337
357
  setSaveError(null);
338
358
  setSavedMsg(null);
339
359
  try {
360
+ const resolvedContentFormat = resolveWriteContentFormat(contentFormat, loaded?.contentFormat);
340
361
  const result = await dispatcher.write<SetResponse>(writeHandler, {
341
362
  slug,
342
363
  locale,
343
364
  title,
344
365
  content: content.length > 0 ? content : null,
345
366
  folder: loaded?.folder ?? null,
367
+ ...(resolvedContentFormat !== undefined && { contentFormat: resolvedContentFormat }),
346
368
  ...(tenantIdOverride !== undefined && { tenantIdOverride }),
347
369
  });
348
370
  if (result.isSuccess) {
@@ -98,13 +98,28 @@ export const tenantInvitationDeleteHook: UserDataDeleteHook = async (ctx, strate
98
98
  }
99
99
  }
100
100
 
101
- const inviterRows = await selectMany<Record<string, unknown>>(ctx.db, tenantInvitationsTable, {
102
- tenantId: ctx.tenantId,
103
- invitedBy: ctx.userId,
104
- });
105
- for (const row of inviterRows) {
101
+ // `invitedBy` has no `lookupable`/blind-index column — it's ciphertext
102
+ // under an active KMS, so `{ invitedBy: ctx.userId }` would compare a
103
+ // plaintext userId against an encrypted column and always match zero
104
+ // rows. Load every invitation in the tenant instead (small per-tenant
105
+ // row count) and decrypt `invitedBy` per row to find this user's own
106
+ // invites-sent. See tenant/handlers/invitations.query.ts for the same
107
+ // decrypt-on-read pattern.
108
+ const tenantInvitations = await selectMany<Record<string, unknown>>(
109
+ ctx.db,
110
+ tenantInvitationsTable,
111
+ { tenantId: ctx.tenantId },
112
+ );
113
+ for (const row of tenantInvitations) {
106
114
  const id = row["id"]; // @cast-boundary db-row
107
- if (typeof id !== "string") continue;
115
+ const invitedByRaw = row["invitedBy"];
116
+ if (typeof id !== "string" || typeof invitedByRaw !== "string") continue;
117
+ const invitedBy = await decryptStoredPii(
118
+ invitedByRaw,
119
+ "invitedBy",
120
+ "user-data-rights:invitation-hook",
121
+ );
122
+ if (invitedBy !== ctx.userId) continue;
108
123
  await crud.update({ id, changes: { invitedBy: INVITED_BY_ANONYMIZED } }, systemUser, tdb, {
109
124
  skipOptimisticLock: true,
110
125
  });