@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/audit_logs/components/AuditLogsActions.js +80 -50
- package/dist/modules/audit_logs/components/AuditLogsActions.js.map +3 -3
- package/dist/modules/audit_logs/lib/rowPresentation.js +82 -0
- package/dist/modules/audit_logs/lib/rowPresentation.js.map +7 -0
- package/dist/modules/catalog/commands/variants.js +89 -16
- package/dist/modules/catalog/commands/variants.js.map +2 -2
- package/dist/modules/catalog/seed/examples.js +7 -0
- package/dist/modules/catalog/seed/examples.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +2 -2
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/components/detail/AssignRoleDialog.js +10 -21
- package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260825140000_customers.js +17 -10
- package/dist/modules/customers/migrations/Migration20260825140000_customers.js.map +2 -2
- package/dist/modules/entities/cli.js +138 -66
- package/dist/modules/entities/cli.js.map +3 -3
- package/dist/modules/promotions/commands/codes.js +18 -3
- package/dist/modules/promotions/commands/codes.js.map +2 -2
- package/dist/modules/query_index/cli.js +17 -1
- package/dist/modules/query_index/cli.js.map +2 -2
- package/dist/modules/query_index/lib/reindexer.js +23 -5
- package/dist/modules/query_index/lib/reindexer.js.map +2 -2
- package/dist/modules/sales/commands/configuration.js +30 -16
- package/dist/modules/sales/commands/configuration.js.map +2 -2
- package/dist/modules/sales/components/documents/AddressesSection.js +36 -15
- package/dist/modules/sales/components/documents/AddressesSection.js.map +2 -2
- package/dist/modules/sales/seed/examples.js +35 -6
- package/dist/modules/sales/seed/examples.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/audit_logs/components/AuditLogsActions.tsx +85 -48
- package/src/modules/audit_logs/i18n/de.json +16 -0
- package/src/modules/audit_logs/i18n/en.json +16 -0
- package/src/modules/audit_logs/i18n/es.json +16 -0
- package/src/modules/audit_logs/i18n/ko.json +16 -0
- package/src/modules/audit_logs/i18n/pl.json +16 -0
- package/src/modules/audit_logs/lib/rowPresentation.ts +92 -0
- package/src/modules/catalog/commands/variants.ts +96 -17
- package/src/modules/catalog/seed/examples.ts +7 -0
- package/src/modules/customers/api/interactions/route.ts +2 -2
- package/src/modules/customers/components/detail/AssignRoleDialog.tsx +9 -12
- package/src/modules/customers/i18n/de.json +2 -1
- package/src/modules/customers/i18n/en.json +2 -1
- package/src/modules/customers/i18n/es.json +2 -1
- package/src/modules/customers/i18n/ko.json +2 -1
- package/src/modules/customers/i18n/pl.json +2 -1
- package/src/modules/customers/migrations/Migration20260825140000_customers.ts +21 -10
- package/src/modules/entities/cli.ts +144 -65
- package/src/modules/promotions/commands/codes.ts +19 -4
- package/src/modules/query_index/cli.ts +17 -1
- package/src/modules/query_index/lib/reindexer.ts +55 -6
- package/src/modules/sales/commands/configuration.ts +35 -16
- package/src/modules/sales/components/documents/AddressesSection.tsx +51 -20
- package/src/modules/sales/i18n/de.json +3 -0
- package/src/modules/sales/i18n/en.json +3 -0
- package/src/modules/sales/i18n/es.json +3 -0
- package/src/modules/sales/i18n/ko.json +3 -0
- package/src/modules/sales/i18n/pl.json +4 -1
- 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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
758
|
-
|
|
759
|
-
|
|
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
|
|
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
|
-
[
|
|
800
|
+
[scope2.tenantId, scope2.organizationId]
|
|
768
801
|
);
|
|
769
802
|
} catch (error) {
|
|
770
|
-
const scopeLabel =
|
|
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 (
|
|
788
|
-
if (
|
|
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 {
|
|
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((
|
|
837
|
-
|
|
838
|
-
|
|
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 =
|
|
843
|
-
for (const
|
|
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 = [
|
|
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 (
|
|
885
|
-
if (
|
|
886
|
-
if (
|
|
887
|
-
|
|
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
|
|
928
|
+
throw error;
|
|
894
929
|
}
|
|
895
930
|
} else {
|
|
896
|
-
throw
|
|
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
|
-
|
|
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=${
|
|
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
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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
|
-
|
|
993
|
-
|
|
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 ${
|
|
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`);
|