@open-mercato/core 0.10.1-develop.8004.1.a8a79ee43f → 0.10.1-develop.8022.1.b6753de892

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 (59) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/audit_logs/components/AuditLogsActions.js +80 -50
  3. package/dist/modules/audit_logs/components/AuditLogsActions.js.map +3 -3
  4. package/dist/modules/audit_logs/lib/rowPresentation.js +82 -0
  5. package/dist/modules/audit_logs/lib/rowPresentation.js.map +7 -0
  6. package/dist/modules/catalog/commands/variants.js +89 -16
  7. package/dist/modules/catalog/commands/variants.js.map +2 -2
  8. package/dist/modules/catalog/seed/examples.js +7 -0
  9. package/dist/modules/catalog/seed/examples.js.map +2 -2
  10. package/dist/modules/customers/api/interactions/route.js +2 -2
  11. package/dist/modules/customers/api/interactions/route.js.map +2 -2
  12. package/dist/modules/customers/components/detail/AssignRoleDialog.js +10 -21
  13. package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +2 -2
  14. package/dist/modules/customers/migrations/Migration20260825140000_customers.js +17 -10
  15. package/dist/modules/customers/migrations/Migration20260825140000_customers.js.map +2 -2
  16. package/dist/modules/entities/cli.js +138 -66
  17. package/dist/modules/entities/cli.js.map +3 -3
  18. package/dist/modules/promotions/commands/codes.js +18 -3
  19. package/dist/modules/promotions/commands/codes.js.map +2 -2
  20. package/dist/modules/query_index/cli.js +17 -1
  21. package/dist/modules/query_index/cli.js.map +2 -2
  22. package/dist/modules/query_index/lib/reindexer.js +23 -5
  23. package/dist/modules/query_index/lib/reindexer.js.map +2 -2
  24. package/dist/modules/sales/commands/configuration.js +30 -16
  25. package/dist/modules/sales/commands/configuration.js.map +2 -2
  26. package/dist/modules/sales/components/documents/AddressesSection.js +36 -15
  27. package/dist/modules/sales/components/documents/AddressesSection.js.map +2 -2
  28. package/dist/modules/sales/seed/examples.js +35 -6
  29. package/dist/modules/sales/seed/examples.js.map +2 -2
  30. package/package.json +7 -7
  31. package/src/modules/audit_logs/components/AuditLogsActions.tsx +85 -48
  32. package/src/modules/audit_logs/i18n/de.json +16 -0
  33. package/src/modules/audit_logs/i18n/en.json +16 -0
  34. package/src/modules/audit_logs/i18n/es.json +16 -0
  35. package/src/modules/audit_logs/i18n/ko.json +16 -0
  36. package/src/modules/audit_logs/i18n/pl.json +16 -0
  37. package/src/modules/audit_logs/lib/rowPresentation.ts +92 -0
  38. package/src/modules/catalog/commands/variants.ts +96 -17
  39. package/src/modules/catalog/seed/examples.ts +7 -0
  40. package/src/modules/customers/api/interactions/route.ts +2 -2
  41. package/src/modules/customers/components/detail/AssignRoleDialog.tsx +9 -12
  42. package/src/modules/customers/i18n/de.json +2 -1
  43. package/src/modules/customers/i18n/en.json +2 -1
  44. package/src/modules/customers/i18n/es.json +2 -1
  45. package/src/modules/customers/i18n/ko.json +2 -1
  46. package/src/modules/customers/i18n/pl.json +2 -1
  47. package/src/modules/customers/migrations/Migration20260825140000_customers.ts +21 -10
  48. package/src/modules/entities/cli.ts +144 -65
  49. package/src/modules/promotions/commands/codes.ts +19 -4
  50. package/src/modules/query_index/cli.ts +17 -1
  51. package/src/modules/query_index/lib/reindexer.ts +55 -6
  52. package/src/modules/sales/commands/configuration.ts +35 -16
  53. package/src/modules/sales/components/documents/AddressesSection.tsx +51 -20
  54. package/src/modules/sales/i18n/de.json +3 -0
  55. package/src/modules/sales/i18n/en.json +3 -0
  56. package/src/modules/sales/i18n/es.json +3 -0
  57. package/src/modules/sales/i18n/ko.json +3 -0
  58. package/src/modules/sales/i18n/pl.json +4 -1
  59. package/src/modules/sales/seed/examples.ts +41 -6
@@ -406,6 +406,18 @@ function formatValueForColumn(prop, value) {
406
406
  if (!isJson) return value;
407
407
  return JSON.stringify(value);
408
408
  }
409
+ async function updateEncryptionColumns(connection, table, primaryKey, row, updates, scope) {
410
+ const columns = Object.keys(updates);
411
+ const assignments = columns.map((column) => `"${column}" = ?`).join(", ");
412
+ const unchanged = columns.map((column) => `"${column}" IS NOT DISTINCT FROM ?`).join(" AND ");
413
+ const result = await connection.execute(
414
+ `UPDATE ${table} SET ${assignments} WHERE "${primaryKey}" = ? AND tenant_id = ? AND organization_id IS NOT DISTINCT FROM ? AND ${unchanged} RETURNING "${primaryKey}"`,
415
+ [...Object.values(updates), row[primaryKey], scope.tenantId, scope.organizationId, ...columns.map((column) => row[column])]
416
+ );
417
+ if (!Array.isArray(result) || result.length !== 1) {
418
+ failCli("[internal] A row changed concurrently during encryption maintenance. No stale values were written; retry after stopping writers.");
419
+ }
420
+ }
409
421
  const rotateEncryptionKey = {
410
422
  command: "rotate-encryption-key",
411
423
  async run(rest) {
@@ -417,6 +429,9 @@ const rotateEncryptionKey = {
417
429
  if (oldKeyArgument && oldKeyEnvironment && normalizeKeyInput(oldKeyArgument) !== oldKeyEnvironment) {
418
430
  failCli(`--old-key and ${OLD_ENCRYPTION_KEY_ENV} contain different values. Remove --old-key and retry.`);
419
431
  }
432
+ if (oldKeyArgument && parseBooleanToken(String(args["allow-unsafe-old-key"])) !== true) {
433
+ failCli(`--old-key is deprecated because process listings expose it. Use ${OLD_ENCRYPTION_KEY_ENV}; the compatibility bridge requires --allow-unsafe-old-key.`);
434
+ }
420
435
  if (oldKeyArgument) {
421
436
  console.warn(
422
437
  `Passing key material through --old-key can expose it in shell history and process listings. Set ${OLD_ENCRYPTION_KEY_ENV} through your secret manager instead.`
@@ -613,11 +628,7 @@ const rotateEncryptionKey = {
613
628
  }
614
629
  if (!Object.keys(updates).length) continue;
615
630
  if (!dryRun) {
616
- const setSql = Object.keys(updates).map((col) => `"${col}" = ?`).join(", ");
617
- await conn.execute(
618
- `update ${qualifiedTable} set ${setSql} where "${pk}" = ?`,
619
- [...Object.values(updates), row[pk]]
620
- );
631
+ await updateEncryptionColumns(conn, qualifiedTable, pk, row, updates, scope);
621
632
  }
622
633
  updated += 1;
623
634
  }
@@ -685,6 +696,9 @@ const decryptDatabase = {
685
696
  failCli(`--confirm value "${confirm}" does not match --tenant "${tenantIdArg}". Aborting.`);
686
697
  }
687
698
  }
699
+ if (deactivateMaps && !dryRun && args["confirm-offline-maintenance"] !== tenantIdArg) {
700
+ failCli("--deactivate-maps requires all application and worker writers stopped. Confirm offline maintenance with --confirm-offline-maintenance <tenantUuid>; restart every replica before resuming writes.");
701
+ }
688
702
  const { resolve } = await createRequestContainer();
689
703
  const em = resolve("em");
690
704
  const conn = em?.getConnection?.();
@@ -695,23 +709,10 @@ const decryptDatabase = {
695
709
  failCli("TENANT_DATA_ENCRYPTION is disabled; aborting. Data may already be decrypted.");
696
710
  }
697
711
  const metaByEntityId = buildEntityMetaRegistry(em);
698
- const resolveDecryptScopes = async (tenantId, organizationId) => {
699
- if (organizationId) return [{ tenantId, organizationId }];
700
- const rows = await conn.execute(
701
- `SELECT DISTINCT organization_id FROM encryption_maps WHERE tenant_id = ? AND deleted_at IS NULL`,
702
- [tenantId]
703
- );
704
- const orgIds = /* @__PURE__ */ new Set();
705
- for (const row of Array.isArray(rows) ? rows : []) {
706
- orgIds.add(row.organization_id ?? null);
707
- }
708
- orgIds.add(null);
709
- return Array.from(orgIds).map((orgId) => ({ tenantId, organizationId: orgId }));
710
- };
711
712
  const mapWhere = { tenantId: tenantIdArg, deletedAt: null, isActive: true };
712
- if (organizationIdArg) mapWhere.organizationId = organizationIdArg;
713
+ if (organizationIdArg) mapWhere.$or = [{ organizationId: organizationIdArg }, { organizationId: null }];
713
714
  if (entityIdArg) mapWhere.entityId = entityIdArg;
714
- const allMaps = await em.find(EncryptionMap, mapWhere);
715
+ const allMaps = await em.find(EncryptionMap, mapWhere, { orderBy: { organizationId: "asc", createdAt: "asc", id: "asc" } });
715
716
  const systemScopedEntityIds = await getSystemScopedEntityIds();
716
717
  const maps = allMaps.filter((map) => {
717
718
  if (!systemScopedEntityIds.has(String(map.entityId))) return true;
@@ -725,6 +726,38 @@ const decryptDatabase = {
725
726
  return;
726
727
  }
727
728
  const resolvedMaps = resolveMapsOrFail(maps, metaByEntityId);
729
+ const resolveProcessingMaps = async (executeQuery = (query, parameters) => conn.execute(query, parameters)) => {
730
+ const groups = /* @__PURE__ */ new Map();
731
+ for (const entry of resolvedMaps) {
732
+ const key = JSON.stringify([entry.mapMeta.tenantId, entry.mapMeta.entityId]);
733
+ groups.set(key, [...groups.get(key) ?? [], entry]);
734
+ }
735
+ const result = [];
736
+ for (const entries of groups.values()) {
737
+ const template = entries[0];
738
+ const { entityId, tenantId, meta } = template.mapMeta;
739
+ const table = meta.schema ? `"${meta.schema}"."${meta.tableName}"` : `"${meta.tableName}"`;
740
+ const scopes = new Set(organizationIdArg ? [organizationIdArg] : [null]);
741
+ if (!organizationIdArg) {
742
+ const rows = await executeQuery(`SELECT DISTINCT organization_id FROM ${table} WHERE tenant_id = ?`, [tenantId]);
743
+ for (const row of Array.isArray(rows) ? rows : []) scopes.add(row.organization_id ?? null);
744
+ }
745
+ for (const organizationId of scopes) {
746
+ const hasExactPolicy = organizationId !== null && allMaps.some((map) => map.entityId === entityId && map.tenantId === tenantId && map.organizationId === organizationId);
747
+ const applicable = organizationId === null ? [...entries].sort((left, right) => Number(Boolean(left.map.organizationId)) - Number(Boolean(right.map.organizationId))) : entries.filter(({ map }) => hasExactPolicy ? map.organizationId === organizationId : !map.organizationId);
748
+ const fields = /* @__PURE__ */ new Map();
749
+ for (const entry of applicable) {
750
+ for (const field of entry.mapMeta.fields) {
751
+ const existing = fields.get(field.field);
752
+ fields.set(field.field, existing ? { ...existing, hashField: existing.hashField ?? field.hashField } : field);
753
+ }
754
+ }
755
+ if (fields.size) result.push({ mapMeta: { ...template.mapMeta, fields: [...fields.values()] }, scope: { tenantId, organizationId } });
756
+ }
757
+ }
758
+ return result;
759
+ };
760
+ const processingMaps = await resolveProcessingMaps();
728
761
  const kms = createKmsService();
729
762
  const dekCache = /* @__PURE__ */ new Map();
730
763
  const getDek = async (tenantId) => {
@@ -741,7 +774,7 @@ const decryptDatabase = {
741
774
  let malformedPayloadCountSampled = 0;
742
775
  const uninspectableEntityIds = [];
743
776
  const failedSampleScopes = [];
744
- for (const { map, mapMeta } of resolvedMaps) {
777
+ for (const { mapMeta, scope } of processingMaps) {
745
778
  const { entityId, meta, fields, tenantId } = mapMeta;
746
779
  const dek = await getDek(tenantId).catch(() => null);
747
780
  if (!dek) {
@@ -749,25 +782,25 @@ const decryptDatabase = {
749
782
  uninspectableEntityIds.push(entityId);
750
783
  continue;
751
784
  }
752
- const scopes = await resolveDecryptScopes(tenantId, map.organizationId ? String(map.organizationId) : null);
753
785
  const pk = Array.isArray(meta?.primaryKeys) && meta.primaryKeys.length ? meta.primaryKeys[0] : "id";
754
786
  const tableName = meta.tableName;
755
787
  const schema = meta?.schema;
756
788
  const qualifiedTable = schema ? `"${schema}"."${tableName}"` : `"${tableName}"`;
757
- const fieldCols = fields.flatMap((f) => {
758
- const r = resolveProperty(meta, f.field);
759
- return r.columnName ? [r.columnName] : [];
760
- });
789
+ const scopes = [scope];
790
+ const fieldCols = fields.flatMap((field) => [
791
+ resolveProperty(meta, field.field).columnName,
792
+ field.hashField ? resolveProperty(meta, field.hashField).columnName : null
793
+ ].filter((column) => Boolean(column)));
761
794
  const colList = Array.from(/* @__PURE__ */ new Set([pk, ...fieldCols]));
762
- for (const scope of scopes) {
795
+ for (const scope2 of scopes) {
763
796
  let sampleRows;
764
797
  try {
765
798
  sampleRows = await conn.execute(
766
799
  `SELECT ${colList.map((c) => `"${c}"`).join(", ")} FROM ${qualifiedTable} WHERE tenant_id = ? AND organization_id IS NOT DISTINCT FROM ? LIMIT 100`,
767
- [scope.tenantId, scope.organizationId]
800
+ [scope2.tenantId, scope2.organizationId]
768
801
  );
769
802
  } catch (error) {
770
- const scopeLabel = scope.organizationId ?? "global";
803
+ const scopeLabel = scope2.organizationId ?? "global";
771
804
  const errorMessage = error instanceof Error ? error.message : String(error);
772
805
  console.warn(`\u26A0 Could not sample ${entityId} for organization ${scopeLabel}: ${errorMessage}`);
773
806
  failedSampleScopes.push(`${entityId} (organization_id=${scopeLabel})`);
@@ -784,8 +817,9 @@ const decryptDatabase = {
784
817
  try {
785
818
  decryptWithAesGcmStrict(String(rawValue), dek.key);
786
819
  rowHasEncrypted = true;
787
- } catch (e) {
788
- if (e instanceof TenantDataEncryptionError && e.code === TenantDataEncryptionErrorCode.MALFORMED_PAYLOAD) {
820
+ } catch (error) {
821
+ if (isEncryptedPayloadShape(rawValue)) throw error;
822
+ if (error instanceof TenantDataEncryptionError && error.code === TenantDataEncryptionErrorCode.MALFORMED_PAYLOAD) {
789
823
  malformedPayloadCountSampled++;
790
824
  }
791
825
  }
@@ -826,26 +860,26 @@ const decryptDatabase = {
826
860
  }
827
861
  deksByTenantId.set(mapMeta.tenantId, dek);
828
862
  }
829
- for (const { map, mapMeta } of resolvedMaps) {
863
+ for (const { mapMeta, scope } of processingMaps) {
830
864
  const { entityId, meta, fields, tenantId } = mapMeta;
831
865
  const dek = deksByTenantId.get(tenantId);
832
866
  const pk = Array.isArray(meta?.primaryKeys) && meta.primaryKeys.length ? meta.primaryKeys[0] : "id";
833
867
  const tableName = meta.tableName;
834
868
  const schema = meta?.schema;
835
869
  const qualifiedTable = schema ? `"${schema}"."${tableName}"` : `"${tableName}"`;
836
- const fieldCols = fields.flatMap((f) => {
837
- const r = resolveProperty(meta, f.field);
838
- return r.columnName ? [r.columnName] : [];
839
- });
870
+ const fieldCols = fields.flatMap((field) => [
871
+ resolveProperty(meta, field.field).columnName,
872
+ field.hashField ? resolveProperty(meta, field.hashField).columnName : null
873
+ ].filter((column) => Boolean(column)));
840
874
  const colList = Array.from(/* @__PURE__ */ new Set([pk, ...fieldCols]));
841
875
  totalEntitiesProcessed++;
842
- const scopes = await resolveDecryptScopes(tenantId, map.organizationId ? String(map.organizationId) : null);
843
- for (const scope of scopes) {
876
+ const scopes = [scope];
877
+ for (const scope2 of scopes) {
844
878
  let lastId = null;
845
879
  let scopeMalformedCount = 0;
846
880
  while (true) {
847
881
  let selectSql = `SELECT ${colList.map((c) => `"${c}"`).join(", ")} FROM ${qualifiedTable} WHERE tenant_id = ? AND organization_id IS NOT DISTINCT FROM ?`;
848
- const selectParams = [scope.tenantId, scope.organizationId];
882
+ const selectParams = [scope2.tenantId, scope2.organizationId];
849
883
  if (lastId !== null) {
850
884
  selectSql += ` AND "${pk}" > ?`;
851
885
  selectParams.push(lastId);
@@ -881,19 +915,20 @@ const decryptDatabase = {
881
915
  }
882
916
  updates[col] = valueToWrite;
883
917
  rowDecrypted = true;
884
- } catch (e) {
885
- if (e instanceof TenantDataEncryptionError) {
886
- if (e.code === TenantDataEncryptionErrorCode.AUTH_FAILED) {
887
- } else if (e.code === TenantDataEncryptionErrorCode.MALFORMED_PAYLOAD) {
918
+ } catch (error) {
919
+ if (error instanceof TenantDataEncryptionError) {
920
+ if (error.code === TenantDataEncryptionErrorCode.AUTH_FAILED) {
921
+ if (isEncryptedPayloadShape(rawValue)) throw error;
922
+ } else if (error.code === TenantDataEncryptionErrorCode.MALFORMED_PAYLOAD) {
888
923
  scopeMalformedCount++;
889
924
  const locationKey = `${tableName}:${col}`;
890
925
  malformedByLocation.set(locationKey, (malformedByLocation.get(locationKey) ?? 0) + 1);
891
926
  console.warn(`\u26A0 MALFORMED_PAYLOAD for ${entityId} field "${col}" row ${row[pk]}; skipping field.`);
892
927
  } else {
893
- throw e;
928
+ throw error;
894
929
  }
895
930
  } else {
896
- throw e;
931
+ throw error;
897
932
  }
898
933
  }
899
934
  }
@@ -916,11 +951,7 @@ const decryptDatabase = {
916
951
  }
917
952
  if (Object.keys(updates).length > 0) {
918
953
  if (!dryRun) {
919
- const setSql = Object.keys(updates).map((col) => `"${col}" = ?`).join(", ");
920
- await conn.execute(
921
- `UPDATE ${qualifiedTable} SET ${setSql} WHERE "${pk}" = ?`,
922
- [...Object.values(updates), row[pk]]
923
- );
954
+ await updateEncryptionColumns(conn, qualifiedTable, pk, row, updates, scope2);
924
955
  }
925
956
  totalRowsUpdated++;
926
957
  }
@@ -940,7 +971,7 @@ const decryptDatabase = {
940
971
  const batchDurationMs = Date.now() - batchStart;
941
972
  if (debug) {
942
973
  console.log(
943
- `[debug] Batch ${entityId} org=${scope.organizationId ?? "null"}: ${batch.length} rows in ${batchDurationMs}ms, scopeMalformed=${scopeMalformedCount}`
974
+ `[debug] Batch ${entityId} org=${scope2.organizationId ?? "null"}: ${batch.length} rows in ${batchDurationMs}ms, scopeMalformed=${scopeMalformedCount}`
944
975
  );
945
976
  if (batchDurationMs > 3e4) {
946
977
  console.warn("\u26A0 Batch took >30s; consider reducing --batch-size.");
@@ -978,27 +1009,68 @@ ${prefix}Decryption summary:`);
978
1009
  `${totalMalformedPayloadCount} field value(s) could not be decrypted because their ciphertext was malformed. Encryption maps were left active.`
979
1010
  );
980
1011
  }
1012
+ const retiring = resolvedMaps.filter(({ map }) => !organizationIdArg || Boolean(map.organizationId));
981
1013
  if (deactivateMaps && !dryRun) {
982
- let deactivateSql = `UPDATE encryption_maps SET is_active = false, deleted_at = now() WHERE tenant_id = ? AND deleted_at IS NULL`;
983
- const deactivateParams = [tenantIdArg];
984
- if (organizationIdArg) {
985
- deactivateSql += ` AND (organization_id = ? OR organization_id IS NULL)`;
986
- deactivateParams.push(organizationIdArg);
987
- }
988
- if (entityIdArg) {
989
- deactivateSql += ` AND entity_id = ?`;
990
- deactivateParams.push(entityIdArg);
1014
+ if (retiring.length) {
1015
+ const policySignature = (policies) => JSON.stringify(policies.map((map) => [map.id, map.entityId, map.tenantId, map.organizationId, map.fieldsJson]).sort((left, right) => String(left[0]).localeCompare(String(right[0]))));
1016
+ const selectedPolicySignature = policySignature(allMaps);
1017
+ await em.transactional(async (transaction) => {
1018
+ const executeProtected = (query, parameters = []) => transaction.getConnection().execute(query, parameters, "all", transaction.getTransactionContext());
1019
+ const tables = Array.from(new Set(resolvedMaps.map(({ mapMeta }) => {
1020
+ const { meta } = mapMeta;
1021
+ return meta.schema ? `"${meta.schema}"."${meta.tableName}"` : `"${meta.tableName}"`;
1022
+ }))).sort((left, right) => left.localeCompare(right));
1023
+ await executeProtected(`LOCK TABLE encryption_maps, ${tables.join(", ")} IN SHARE ROW EXCLUSIVE MODE`);
1024
+ const lockedPolicies = await transaction.find(EncryptionMap, mapWhere, { refresh: true });
1025
+ if (policySignature(lockedPolicies) !== selectedPolicySignature) {
1026
+ failCli("[internal] Encryption policies changed during maintenance. Maps remain active; retry against the current policies.");
1027
+ }
1028
+ for (const { mapMeta, scope } of await resolveProcessingMaps(executeProtected)) {
1029
+ const { meta, fields, entityId, tenantId } = mapMeta;
1030
+ const table = meta.schema ? `"${meta.schema}"."${meta.tableName}"` : `"${meta.tableName}"`;
1031
+ const columns = fields.map((field) => resolveProperty(meta, field.field).columnName).filter((column) => Boolean(column));
1032
+ const scopes = [scope];
1033
+ for (const scope2 of scopes) {
1034
+ const primaryKey = meta.primaryKeys?.[0] ?? "id";
1035
+ const selectedColumns = [.../* @__PURE__ */ new Set([primaryKey, ...columns])];
1036
+ let lastVerifiedId = null;
1037
+ while (true) {
1038
+ let query = `SELECT ${selectedColumns.map((column) => `"${column}"`).join(", ")} FROM ${table} WHERE tenant_id = ? AND organization_id IS NOT DISTINCT FROM ?`;
1039
+ const parameters = [scope2.tenantId, scope2.organizationId];
1040
+ if (lastVerifiedId !== null) {
1041
+ query += ` AND "${primaryKey}" > ?`;
1042
+ parameters.push(lastVerifiedId);
1043
+ }
1044
+ query += ` ORDER BY "${primaryKey}" LIMIT ?`;
1045
+ parameters.push(batchSize);
1046
+ const rows = await executeProtected(query, parameters);
1047
+ const batch = Array.isArray(rows) ? rows : [];
1048
+ if (!batch.length) break;
1049
+ if (batch.some((row) => columns.some((column) => isEncryptedPayloadShape(row[column])))) {
1050
+ failCli("[internal] Encrypted values remain during the protected final sweep. Encryption maps remain active; stop writers and retry.");
1051
+ }
1052
+ lastVerifiedId = String(batch[batch.length - 1][primaryKey]);
1053
+ }
1054
+ }
1055
+ }
1056
+ const ids = retiring.map(({ map }) => map.id);
1057
+ if (ids.some((id) => typeof id !== "string" || !id)) failCli("[internal] Cannot retire an encryption map without its identifier.");
1058
+ await executeProtected(`UPDATE encryption_maps SET is_active = false, deleted_at = now() WHERE tenant_id = ? AND id IN (${ids.map(() => "?").join(", ")}) AND deleted_at IS NULL`, [tenantIdArg, ...ids]);
1059
+ });
1060
+ console.warn("\u26A0 Restart all application replicas before resuming writes \u2014 in-process map caches may still be active.");
991
1061
  }
992
- await conn.execute(deactivateSql, deactivateParams);
993
- console.warn("\u26A0 Restart all application replicas \u2014 in-process map caches may still be active.");
994
- if (isTenantDataEncryptionEnabled()) {
995
- console.warn("\u26A0 Env TENANT_DATA_ENCRYPTION is still true \u2014 new writes will be re-encrypted until env is updated and replicas restarted.");
1062
+ if (organizationIdArg && resolvedMaps.some(({ map }) => !map.organizationId)) {
1063
+ console.warn("\u26A0 Tenant-wide fallback encryption maps remain active after organization-only decryption. Run a complete tenant sweep before retiring them.");
996
1064
  }
997
1065
  }
998
1066
  if (deactivateMaps && dryRun) {
999
- console.log(`[dry-run] Would deactivate ${maps.length} EncryptionMap record(s).`);
1067
+ console.log(`[dry-run] Would deactivate ${retiring.length} EncryptionMap record(s).`);
1068
+ }
1069
+ if (!dryRun && organizationIdArg) {
1070
+ console.log(`
1071
+ \u2705 Organization-scoped decryption complete. Keep tenant encryption enabled; tenant-wide fallback policies may still protect other organizations. Restart replicas before resuming writes.`);
1000
1072
  }
1001
- if (!dryRun) {
1073
+ if (!dryRun && !organizationIdArg) {
1002
1074
  console.log(`
1003
1075
  \u2705 Decryption complete. Required next steps:`);
1004
1076
  console.log(` 1. Set TENANT_DATA_ENCRYPTION=false in your environment / secrets`);