| @@ -1,5 +1,6 @@ |
| 1 | 1 | /* oxlint-disable executor/no-error-constructor, executor/no-try-catch-or-throw -- boundary: one-shot migration CLI */ |
| 2 | 2 | |
| 3 | +import { createHash } from "node:crypto"; |
| 3 | 4 | import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; |
| 4 | 5 | import { resolve } from "node:path"; |
| 5 | 6 | import { pathToFileURL } from "node:url"; |
| @@ -220,14 +221,10 @@ const scrubJson = (value: unknown): unknown => { · 214 unmodified lines |
| 220 | 221 | return value; |
| 221 | 222 | }; |
| 222 | 223 | |
| 223 | | −const stableId = (...parts: readonly string[]): string => { |
| 224 | | − const text = parts.join("\u0000"); |
| 225 | | − let hash = 0; |
| 226 | | − for (let index = 0; index < text.length; index += 1) { |
| 227 | | − hash = (hash * 31 + text.charCodeAt(index)) >>> 0; |
| 228 | | − } |
| 229 | | − return `service_split_${hash.toString(36)}`; |
| 230 | | −}; |
| 224 | +// sha256-based: a 31-bit hash collides around ~50k rows (observed in prod); |
| 225 | +// 96 bits keeps re-run determinism with no realistic collision risk. |
| 226 | +const stableId = (...parts: readonly string[]): string => |
| 227 | + `service_split_${createHash("sha256").update(parts.join("\u0000")).digest("hex").slice(0, 24)}`; |
| 231 | 228 | |
| 232 | 229 | const operationKeyPrefix = (integration: string): string => |
| 233 | 230 | `${operationStorageKey(integration, "").split(".").slice(0, 2).join(".")}.%`; |
| @@ -280,8 +277,8 @@ export const applyOrg = async (sql: SqlClient, org: OrgPlan): Promise<void> => { · 46 unmodified lines |
| 280 | 277 | row.target.pluginId, |
| 281 | 278 | row.target.name, |
| 282 | 279 | row.target.description, |
| 283 | | − JSON.stringify(scrubJson(row.config)), |
| 284 | | − row.healthCheck ? JSON.stringify(scrubJson(row.healthCheck)) : null, |
| 280 | + scrubJson(row.config), |
| 281 | + row.healthCheck ? scrubJson(row.healthCheck) : null, |
| 285 | 282 | now, |
| 286 | 283 | stableId("integration", org.tenant, row.target.slug), |
| 287 | 284 | org.tenant, |
| @@ -391,13 +388,11 @@ export const applyOrg = async (sql: SqlClient, org: OrgPlan): Promise<void> => { · 103 unmodified lines |
| 391 | 388 | params.push( |
| 392 | 389 | integration.target.pluginId, |
| 393 | 390 | operationStorageKey(integration.target.slug, toolName), |
| 394 | | − JSON.stringify( |
| 395 | | − scrubJson({ |
| 396 | | − ...storageDataRecord(operation), |
| 397 | | − integration: integration.target.slug, |
| 398 | | − toolName, |
| 399 | | − }), |
| 400 | | − ), |
| 391 | + scrubJson({ |
| 392 | + ...storageDataRecord(operation), |
| 393 | + integration: integration.target.slug, |
| 394 | + toolName, |
| 395 | + }), |
| 401 | 396 | operation.created_at, |
| 402 | 397 | now, |
| 403 | 398 | stableId("operation", org.tenant, integration.target.slug, toolName), |