import { createHash, randomBytes } from "node:crypto"; const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/; const TOKEN_PREFIX = "ndc_edprb_"; const SECRET_LIKE_KEY = /(token|secret|password|authorization|access[_-]?token|refresh[_-]?token|api[_-]?key)/i; const REQUEST_KEYS = new Set(["source", "allowedDataProductIds", "expiresAt"]); const MANAGED_REQUEST_KEYS = new Set([ "source", "allowedDataProductIds", "expiresAt", "generation", "capabilityDigest", ]); const MANAGED_CONSUMER_REQUEST_KEYS = new Set([ "allowedDataProductIds", "expiresAt", "generation", "capabilityDigest", ]); const MANAGED_CONSUMER_PLAN_KEYS = new Set(["allowedDataProductIds"]); const SOURCE_KEYS = new Set(["tenantId", "connectionId", "providerId"]); const SHA256_DIGEST = /^[a-f0-9]{64}$/; export function createReaderToken() { return `${TOKEN_PREFIX}${randomBytes(32).toString("base64url")}`; } export function hashReaderToken(token) { return createHash("sha256").update(String(token), "utf8").digest("hex"); } export function normalizeReaderBindingRequest(value, { now = new Date(), maxTtlDays = 90 } = {}) { if (!isPlainObject(value) || !isPlainObject(value.source)) throw readerError("reader_binding_request_invalid"); if (containsSecretLikeKey(value)) throw readerError("reader_binding_request_secret_material_forbidden"); if (!hasOnlyKeys(value, REQUEST_KEYS) || !hasOnlyKeys(value.source, SOURCE_KEYS)) { throw readerError("reader_binding_request_fields_invalid"); } const tenantId = identifier(value.source.tenantId); const connectionId = identifier(value.source.connectionId); const providerId = identifier(value.source.providerId); const allowedDataProductIds = uniqueIdentifiers(value.allowedDataProductIds); if (!tenantId || !connectionId || !providerId || !allowedDataProductIds.length) { throw readerError("reader_binding_scope_invalid"); } const expiresAt = new Date(String(value.expiresAt || "")); const maxExpiresAt = new Date(now.getTime() + maxTtlDays * 24 * 60 * 60 * 1000); if (Number.isNaN(expiresAt.getTime()) || expiresAt <= now || expiresAt > maxExpiresAt) { throw readerError("reader_binding_expiry_invalid"); } return Object.freeze({ tenantId, connectionId, providerId, allowedDataProductIds, expiresAt: expiresAt.toISOString() }); } export function normalizeManagedReaderBindingRequest(value) { if (!isPlainObject(value) || !isPlainObject(value.source)) { throw readerError("managed_reader_binding_request_invalid"); } if (containsSecretLikeKey(value)) { throw readerError("managed_reader_binding_secret_material_forbidden"); } if (!hasOnlyKeys(value, MANAGED_REQUEST_KEYS) || !hasOnlyKeys(value.source, SOURCE_KEYS)) { throw readerError("managed_reader_binding_request_fields_invalid"); } const tenantId = identifier(value.source.tenantId); const connectionId = identifier(value.source.connectionId); const providerId = identifier(value.source.providerId); const allowedDataProductIds = uniqueIdentifiers(value.allowedDataProductIds); if (!tenantId || !connectionId || !providerId || !allowedDataProductIds.length) { throw readerError("managed_reader_binding_scope_invalid"); } if (value.expiresAt !== null) { throw readerError("managed_reader_binding_must_be_durable"); } const generation = Number(value.generation); if (!Number.isSafeInteger(generation) || generation < 1 || generation > 2_147_483_647) { throw readerError("managed_reader_binding_generation_invalid"); } const capabilityDigest = String(value.capabilityDigest || "").toLowerCase(); if (!SHA256_DIGEST.test(capabilityDigest)) { throw readerError("managed_reader_binding_capability_digest_invalid"); } return Object.freeze({ tenantId, connectionId, providerId, allowedDataProductIds: Object.freeze([...allowedDataProductIds].sort()), expiresAt: null, generation, capabilityDigest, }); } export function normalizeManagedConsumerReaderPlanRequest(value) { if (!isPlainObject(value) || containsSecretLikeKey(value) || !hasOnlyKeys(value, MANAGED_CONSUMER_PLAN_KEYS)) { throw readerError("managed_consumer_reader_plan_request_invalid"); } const allowedDataProductIds = uniqueIdentifiers(value.allowedDataProductIds); if (!allowedDataProductIds.length) throw readerError("managed_consumer_reader_scope_invalid"); return Object.freeze({ allowedDataProductIds: Object.freeze([...allowedDataProductIds].sort()) }); } export function normalizeManagedConsumerReaderBindingRequest(value) { if (!isPlainObject(value) || containsSecretLikeKey(value) || !hasOnlyKeys(value, MANAGED_CONSUMER_REQUEST_KEYS)) { throw readerError("managed_consumer_reader_binding_request_invalid"); } const allowedDataProductIds = uniqueIdentifiers(value.allowedDataProductIds); if (!allowedDataProductIds.length) throw readerError("managed_consumer_reader_scope_invalid"); if (value.expiresAt !== null) throw readerError("managed_consumer_reader_must_be_durable"); const generation = Number(value.generation); if (!Number.isSafeInteger(generation) || generation < 1 || generation > 2_147_483_647) { throw readerError("managed_consumer_reader_generation_invalid"); } const capabilityDigest = String(value.capabilityDigest || "").toLowerCase(); if (!SHA256_DIGEST.test(capabilityDigest)) { throw readerError("managed_consumer_reader_capability_digest_invalid"); } return Object.freeze({ allowedDataProductIds: Object.freeze([...allowedDataProductIds].sort()), expiresAt: null, generation, capabilityDigest, }); } export function readerBindingRequestHash(policy) { const canonical = JSON.stringify({ tenantId: policy.tenantId, connectionId: policy.connectionId, providerId: policy.providerId, allowedDataProductIds: [...policy.allowedDataProductIds].sort(), expiresAt: null, generation: policy.generation, capabilityDigest: policy.capabilityDigest, }); return createHash("sha256").update(canonical, "utf8").digest("hex"); } export function consumerReaderBindingRequestHash(policy) { return createHash("sha256").update(JSON.stringify({ allowedDataProductIds: [...policy.allowedDataProductIds].sort(), expiresAt: null, generation: policy.generation, capabilityDigest: policy.capabilityDigest, }), "utf8").digest("hex"); } export function assertReaderProduct(binding, dataProductId, now = new Date()) { const expired = binding?.expiresAt !== null && new Date(binding?.expiresAt) <= now; if (!isPlainObject(binding) || binding.active !== true || expired) { throw readerError("reader_binding_inactive", 401); } const normalized = identifier(dataProductId); if (!normalized || !uniqueIdentifiers(binding.allowedDataProductIds).includes(normalized)) { throw readerError("reader_binding_data_product_forbidden", 403); } return normalized; } export function safeReaderBinding(binding) { return { id: binding.id, ...(binding.bindingKey ? { bindingKey: binding.bindingKey } : {}), ...(Number.isInteger(Number(binding.generation)) ? { generation: Number(binding.generation) } : {}), tenantId: binding.tenantId, connectionId: binding.connectionId, providerId: binding.providerId, allowedDataProductIds: uniqueIdentifiers(binding.allowedDataProductIds), active: binding.active === true, expiresAt: binding.expiresAt === null ? null : new Date(binding.expiresAt).toISOString(), createdAt: binding.createdAt ? new Date(binding.createdAt).toISOString() : undefined, rotatedAt: binding.rotatedAt ? new Date(binding.rotatedAt).toISOString() : undefined, revokedAt: binding.revokedAt ? new Date(binding.revokedAt).toISOString() : undefined, }; } export function safeManagedConsumerReaderBinding(binding) { return { id: binding.id, bindingKey: binding.bindingKey, generation: Number(binding.generation), allowedDataProductIds: uniqueIdentifiers(binding.allowedDataProductIds), active: binding.active === true, expiresAt: binding.expiresAt === null ? null : new Date(binding.expiresAt).toISOString(), createdAt: binding.createdAt ? new Date(binding.createdAt).toISOString() : undefined, rotatedAt: binding.rotatedAt ? new Date(binding.rotatedAt).toISOString() : undefined, revokedAt: binding.revokedAt ? new Date(binding.revokedAt).toISOString() : undefined, sourceScope: "resolved-server-side", }; } function readerError(code, status = 400) { return Object.assign(new Error(code), { status, code }); } function identifier(value) { const normalized = typeof value === "string" ? value.trim() : ""; return IDENTIFIER.test(normalized) ? normalized : ""; } function uniqueIdentifiers(value) { if (!Array.isArray(value)) return []; return [...new Set(value.map(identifier).filter(Boolean))]; } function containsSecretLikeKey(value) { if (Array.isArray(value)) return value.some(containsSecretLikeKey); if (!isPlainObject(value)) return false; return Object.entries(value).some(([key, child]) => SECRET_LIKE_KEY.test(key) || containsSecretLikeKey(child)); } function isPlainObject(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function hasOnlyKeys(value, allowed) { return Object.keys(value).every((key) => allowed.has(key)); }