import { createHash, randomUUID } from "node:crypto"; import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises"; import { join } from "node:path"; const STATE_SCHEMA_VERSION = "nodedc.module-foundry.data-product-consumer/v1"; const PLAN_SCHEMA_VERSION = "nodedc.module-foundry.data-product-consumer-plan/v1"; const ACCEPTANCE_SCHEMA_VERSION = "nodedc.module-foundry.data-product-consumer-acceptance/v1"; const PRESENTATION_PATCH_SCHEMA_VERSION = "nodedc.foundry.presentation-patch/v1"; const identifier = /^[A-Za-z0-9._:-]{1,160}$/; const cursorPattern = /^(?:0|[1-9]\d*)$/; function consumerError(code, statusCode = 400) { return Object.assign(new Error(code), { statusCode }); } function targetIdentity(target) { const applicationId = String(target?.application?.id || target?.applicationId || ""); const pageId = String(target?.page?.id || target?.pageId || ""); const binding = target?.binding || {}; const bindingId = String(binding.id || target?.bindingId || ""); if (!/^[0-9a-f-]{36}$/i.test(applicationId) || !/^[a-z0-9-]+$/i.test(pageId) || !identifier.test(bindingId)) { throw consumerError("data_product_consumer_target_invalid"); } return { applicationId, pageId, bindingId }; } function targetKey(target) { const identity = targetIdentity(target); return `${identity.applicationId}/${identity.pageId}/${identity.bindingId}`; } function stateFileName(target) { return `${createHash("sha256").update(targetKey(target), "utf8").digest("hex")}.json`; } function factKey(fact) { return `${fact.semanticType}\u0000${fact.sourceId}`; } function safeErrorCode(error) { const value = String(error?.message || "data_product_consumer_failed"); return /^[a-z][a-z0-9_]{2,120}$/.test(value) ? value : "data_product_consumer_failed"; } function safeStatusFromFact(fact, policy, nowMs) { const statusContract = policy.statusContract || null; if (statusContract) { const sourceStatus = fact?.attributes?.[statusContract.attribute]; if (typeof sourceStatus !== "string" || !statusContract.allowedValues.includes(sourceStatus)) { throw consumerError("data_product_consumer_fact_status_invalid", 502); } if (policy.freshness === "none" || policy.terminalStatuses.includes(sourceStatus)) { return sourceStatus; } const observedAt = Date.parse(String(fact?.observedAt || "")); if (Number.isFinite(observedAt) && nowMs - observedAt > policy.staleAfterMs) return "stale"; return sourceStatus; } const sourceStatus = [fact?.attributes?.operational_status, fact?.attributes?.status] .find((value) => typeof value === "string" && value.trim()); const normalized = String(sourceStatus || "active").trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").slice(0, 64) || "active"; if (policy.terminalStatuses.includes(normalized)) return normalized; if (policy.freshness === "none") return normalized; const observedAt = Date.parse(String(fact?.observedAt || "")); if (Number.isFinite(observedAt) && nowMs - observedAt > policy.staleAfterMs) return "stale"; return normalized; } function decorateFact(subject) { return { ...subject.fact, presentationStatus: subject.status }; } function safeTarget(target) { const identity = targetIdentity(target); const binding = target.binding || {}; return { ...identity, dataProductId: String(binding.dataProductId || ""), slotId: String(binding.slotId || ""), delivery: "snapshot+patch", semanticTypes: [...(binding.semanticTypes || [])], fieldProjection: [...(binding.fieldProjection || [])], dataClass: binding.dataClass === "restricted" ? "restricted" : "operational", }; } function planHash(value) { return `fcp1_${createHash("sha256").update(JSON.stringify(value), "utf8").digest("base64url")}`; } function emptyMetrics() { return { snapshotCommits: 0, patchCommits: 0, patchOperations: 0, replayedPatches: 0, snapshotRebaseRemovals: 0, canonicalRemovals: 0, staleTransitions: 0, reconnects: 0, }; } function safeStateSummary(record) { const state = record.state; const subjects = Object.values(state.subjects || {}); return { schemaVersion: STATE_SCHEMA_VERSION, consumerId: state.consumerId, target: state.target, enabled: state.enabled === true, runtimeState: state.runtimeState, product: state.product, policy: state.policy, cursor: state.cursor, snapshotGeneration: state.snapshotGeneration, subjectCount: subjects.length, subjects: subjects.slice(0, 20).map((subject) => ({ subjectId: subject.fact.sourceId, semanticType: subject.fact.semanticType, observedAt: subject.fact.observedAt, status: subject.status, })), viewerCount: record.listeners.size, upstreamStreamCount: record.stream ? 1 : 0, metrics: state.metrics, timestamps: state.timestamps, lastError: state.lastError || null, readerGrant: "target-scoped-server-only", readerGrantGeneration: Number.isSafeInteger(state.readerGrantGeneration) ? state.readerGrantGeneration : 1, }; } function activeReaderGrantGeneration(state) { const generation = Number(state?.readerGrantGeneration ?? 1); if (!Number.isSafeInteger(generation) || generation < 1 || generation > 2_147_483_647) { throw consumerError("data_product_reader_grant_generation_invalid", 500); } return generation; } function validateConsumerContract(value) { if (value === undefined) return null; const keys = value && typeof value === "object" && !Array.isArray(value) ? Object.keys(value).sort() : []; if (JSON.stringify(keys) !== JSON.stringify([ "deliveryMode", "fieldProjection", "fieldTypes", "geometryTypes", "ontologyRevision", "patchMode", "semanticTypes", "snapshotMode", "subjectIdentity", ])) throw consumerError("data_product_consumer_policy_invalid", 500); const semanticTypes = Array.isArray(value.semanticTypes) ? value.semanticTypes : []; const fieldProjection = Array.isArray(value.fieldProjection) ? value.fieldProjection : []; const geometryTypes = Array.isArray(value.geometryTypes) ? value.geometryTypes : []; const supportedGeometryTypes = new Set(["Point", "LineString", "Polygon", "MultiPolygon"]); const fieldTypes = value.fieldTypes && typeof value.fieldTypes === "object" && !Array.isArray(value.fieldTypes) ? value.fieldTypes : null; if ( !identifier.test(String(value.ontologyRevision || "")) || value.deliveryMode !== "snapshot+patch" || semanticTypes.length === 0 || semanticTypes.length > 8 || new Set(semanticTypes).size !== semanticTypes.length || semanticTypes.some((item) => !identifier.test(item)) || fieldProjection.length === 0 || fieldProjection.length > 32 || new Set(fieldProjection).size !== fieldProjection.length || fieldProjection.some((item) => !/^[A-Za-z][A-Za-z0-9_.-]{0,127}$/.test(item)) || !fieldTypes || JSON.stringify(Object.keys(fieldTypes)) !== JSON.stringify(fieldProjection) || Object.values(fieldTypes).some((type) => !["string", "number", "boolean", "string_array"].includes(type)) || geometryTypes.length > supportedGeometryTypes.size || new Set(geometryTypes).size !== geometryTypes.length || geometryTypes.some((type) => !supportedGeometryTypes.has(type)) || value.subjectIdentity !== "semantic-type+source-id" || value.snapshotMode !== "atomic-replace" || value.patchMode !== "atomic" ) throw consumerError("data_product_consumer_policy_invalid", 500); return { ontologyRevision: value.ontologyRevision, deliveryMode: "snapshot+patch", semanticTypes: [...semanticTypes], fieldProjection: [...fieldProjection], fieldTypes: { ...fieldTypes }, geometryTypes: [...geometryTypes], subjectIdentity: "semantic-type+source-id", snapshotMode: "atomic-replace", patchMode: "atomic", }; } function assertConsumerContract(contract, product, binding) { if (!contract) return; const productFieldTypes = Object.fromEntries(contract.fieldProjection.map((field) => [ field, product?.fieldContracts?.[field]?.type, ])); const productHasGeometry = Array.isArray(product.fields) && product.fields.includes("geometry"); const contractHasGeometry = contract.geometryTypes.length > 0; if ( product.ontologyRevision !== contract.ontologyRevision || product.deliveryMode !== contract.deliveryMode || JSON.stringify(product.semanticTypes) !== JSON.stringify(contract.semanticTypes) || JSON.stringify(binding.semanticTypes) !== JSON.stringify(contract.semanticTypes) || JSON.stringify(binding.fieldProjection) !== JSON.stringify(contract.fieldProjection) || !Array.isArray(product.fields) || contract.fieldProjection.some((field) => !product.fields.includes(field)) || JSON.stringify(productFieldTypes) !== JSON.stringify(contract.fieldTypes) || productHasGeometry !== contractHasGeometry || (contractHasGeometry && product?.fieldContracts?.geometry?.type !== "geometry") ) throw consumerError("data_product_consumer_contract_mismatch", 409); } function assertEnvelopeProduct(envelope, product, errorCode) { if (envelope?.dataProduct?.id !== product.id || envelope?.dataProduct?.version !== product.version) { throw consumerError(errorCode, 502); } } function assertFactContract(fact, policy) { const contract = policy.consumerContract; if (!contract) return; if (!contract.semanticTypes.includes(fact.semanticType)) { throw consumerError("data_product_consumer_fact_contract_invalid", 502); } const attributes = fact?.attributes && typeof fact.attributes === "object" && !Array.isArray(fact.attributes) ? fact.attributes : {}; for (const [field, type] of Object.entries(contract.fieldTypes)) { if (attributes[field] === undefined) continue; const value = attributes[field]; const valid = type === "string_array" ? Array.isArray(value) && value.every((item) => typeof item === "string") : typeof value === type && (type !== "number" || Number.isFinite(value)); if (!valid) throw consumerError("data_product_consumer_fact_contract_invalid", 502); } const hasGeometry = fact.geometry !== undefined && fact.geometry !== null; if (contract.geometryTypes.length === 0) { if (hasGeometry) throw consumerError("data_product_consumer_fact_contract_invalid", 502); return; } if (!hasGeometry || !contract.geometryTypes.includes(fact.geometry.type)) { throw consumerError("data_product_consumer_fact_contract_invalid", 502); } } function validatePolicy(policy, product) { if (!policy || policy.dataProductId !== product.id || policy.productVersion !== product.version) { throw consumerError("data_product_consumer_policy_not_found", 409); } const statusContract = policy.statusContract === undefined ? null : validateStatusContract(policy.statusContract); const freshness = policy.freshness ?? statusContract?.freshness ?? "observed-at"; if (!["none", "observed-at"].includes(freshness)) { throw consumerError("data_product_consumer_policy_invalid", 500); } if (statusContract && statusContract.freshness !== freshness) { throw consumerError("data_product_consumer_policy_invalid", 500); } const staleDisabled = freshness === "none"; if ( (staleDisabled && policy.staleAfterMs !== null) || (!staleDisabled && (!Number.isInteger(policy.staleAfterMs) || policy.staleAfterMs < 1_000 || policy.staleAfterMs > 7 * 24 * 60 * 60 * 1000)) ) { throw consumerError("data_product_consumer_policy_invalid", 500); } const terminalStatuses = Array.isArray(policy.terminalStatuses) ? policy.terminalStatuses : []; if (terminalStatuses.some((value) => typeof value !== "string" || !/^[a-z0-9_-]{1,64}$/.test(value))) { throw consumerError("data_product_consumer_policy_invalid", 500); } if (statusContract && terminalStatuses.some((value) => !statusContract.allowedValues.includes(value))) { throw consumerError("data_product_consumer_policy_invalid", 500); } if (policy.removeMode !== "canonical-tombstone-or-snapshot-rebase") { throw consumerError("data_product_consumer_policy_invalid", 500); } const consumerContract = validateConsumerContract(policy.consumerContract); const dataClass = policy.dataClass ?? "operational"; if (!["operational", "restricted"].includes(dataClass)) { throw consumerError("data_product_consumer_policy_invalid", 500); } return { id: String(policy.id || ""), version: String(policy.version || ""), dataProductId: product.id, productVersion: product.version, dataClass, freshness, staleAfterMs: policy.staleAfterMs, terminalStatuses: [...terminalStatuses], ...(statusContract ? { statusContract } : {}), ...(consumerContract ? { consumerContract } : {}), removeMode: policy.removeMode, }; } function validateStatusContract(value) { const keys = value && typeof value === "object" && !Array.isArray(value) ? Object.keys(value).sort() : []; if (JSON.stringify(keys) !== JSON.stringify(["allowedValues", "attribute", "freshness", "missing"])) { throw consumerError("data_product_consumer_policy_invalid", 500); } const attribute = String(value.attribute || ""); const allowedValues = Array.isArray(value.allowedValues) ? value.allowedValues : []; if ( !/^[A-Za-z][A-Za-z0-9_.-]{0,127}$/.test(attribute) || allowedValues.length === 0 || allowedValues.length > 32 || new Set(allowedValues).size !== allowedValues.length || allowedValues.some((item) => typeof item !== "string" || !/^[a-z0-9_-]{1,64}$/.test(item)) || value.missing !== "reject" || !["none", "observed-at"].includes(value.freshness) ) { throw consumerError("data_product_consumer_policy_invalid", 500); } return { attribute, allowedValues: [...allowedValues], missing: "reject", freshness: value.freshness, }; } function parseSseBlock(block) { const fields = { event: "message", id: "", data: [] }; for (const line of block.split("\n")) { if (!line || line.startsWith(":")) continue; const separator = line.indexOf(":"); const name = separator === -1 ? line : line.slice(0, separator); const value = separator === -1 ? "" : line.slice(separator + 1).replace(/^ /, ""); if (name === "event") fields.event = value; if (name === "id") fields.id = value; if (name === "data") fields.data.push(value); } return { event: fields.event, id: fields.id, data: fields.data.join("\n") }; } function waitForAbortableDelay(ms, signal) { if (signal.aborted) return Promise.resolve(); return new Promise((resolve) => { const timer = setTimeout(done, ms); function done() { clearTimeout(timer); signal.removeEventListener("abort", done); resolve(); } signal.addEventListener("abort", done, { once: true }); }); } export function createFoundryDataProductConsumerManager({ stateDir, dataPlaneUrl, resolveTarget, readReaderToken, inspectReaderGrant, ensureReaderGrant, revokeReaderGrant, resolvePolicy, sanitizeSnapshot, sanitizePatch, fetchImpl = fetch, openStream, now = () => Date.now(), idleStopMs = 2_000, staleSweepMs = 5_000, reconnectMinMs = 500, reconnectMaxMs = 15_000, }) { const records = new Map(); const ready = mkdir(stateDir, { recursive: true }); let closed = false; async function writeState(record) { await ready; const targetPath = join(stateDir, stateFileName(record.state.target)); const tempPath = `${targetPath}.${randomUUID()}.tmp`; await writeFile(tempPath, `${JSON.stringify(record.state, null, 2)}\n`, { encoding: "utf8", mode: 0o600 }); await rename(tempPath, targetPath); } async function readState(target) { await ready; try { const parsed = JSON.parse(await readFile(join(stateDir, stateFileName(target)), "utf8")); if (parsed?.schemaVersion !== STATE_SCHEMA_VERSION || targetKey(parsed.target) !== targetKey(target)) { throw consumerError("data_product_consumer_state_invalid", 500); } return parsed; } catch (error) { if (error?.code === "ENOENT") return null; throw error; } } async function recordFor(target) { const key = targetKey(target); if (records.has(key)) return records.get(key); const persisted = await readState(target); const record = { key, target, state: persisted, listeners: new Map(), stream: null, idleTimer: null, writing: Promise.resolve(), }; records.set(key, record); return record; } function serialize(record, action) { const pending = record.writing.then(action, action); record.writing = pending.catch(() => undefined); return pending; } function emit(record, event) { for (const listener of record.listeners.values()) { try { listener(event); } catch { /* one browser cannot stop the shared consumer */ } } } async function catalog(target, { generation = 1, ensureGeneration = generation } = {}) { if (!dataPlaneUrl) throw consumerError("data_product_runtime_not_configured", 503); let product = null; let readerGrantAction = "reuse"; let readerGrantGeneration = generation; if (inspectReaderGrant) { const inspection = await inspectReaderGrant(target, { generation }); product = inspection?.product || null; readerGrantAction = inspection?.readerGrantAction === "ensure" ? "ensure" : "reuse"; readerGrantGeneration = readerGrantAction === "ensure" ? ensureGeneration : generation; } else { const token = await readReaderToken(target.application.id, target.page.id, target.binding.id, generation); let response; try { response = await fetchImpl(new URL("/internal/data-plane/v1/reader/data-products", `${dataPlaneUrl}/`), { headers: { authorization: `Bearer ${token}`, accept: "application/json" }, signal: AbortSignal.timeout(10_000), }); } catch { throw consumerError("data_product_consumer_catalog_unavailable", 503); } if (response.status === 401 || response.status === 403) throw consumerError("data_product_access_denied", 403); if (!response.ok) throw consumerError("data_product_consumer_catalog_unavailable", 503); const payload = await response.json().catch(() => null); product = Array.isArray(payload?.dataProducts) ? payload.dataProducts.find((candidate) => candidate?.id === target.binding.dataProductId) : null; } if (!product || product.active === false) throw consumerError("data_product_not_found", 404); if (product.deliveryMode !== "snapshot+patch" || typeof product.version !== "string") { throw consumerError("data_product_consumer_delivery_not_supported", 409); } if (target.binding.semanticTypes.some((semanticType) => !product.semanticTypes?.includes(semanticType))) { throw consumerError("data_product_consumer_semantic_scope_mismatch", 409); } const policy = validatePolicy(resolvePolicy(product), product); if ((target.binding.dataClass ?? "operational") !== policy.dataClass) { throw consumerError("data_product_consumer_data_class_mismatch", 409); } assertConsumerContract(policy.consumerContract, product, target.binding); if (policy.statusContract && !target.binding.fieldProjection.includes(policy.statusContract.attribute)) { throw consumerError("data_product_consumer_status_field_not_projected", 409); } return { product, policy, readerGrantAction, readerGrantGeneration, }; } async function plan(input) { const target = await resolveTarget(input.applicationId, input.pageId, input.bindingId); const record = await recordFor(target); const currentReaderGrantGeneration = activeReaderGrantGeneration(record.state); const ensureReaderGrantGeneration = record.state ? Math.min(2_147_483_647, currentReaderGrantGeneration + 1) : currentReaderGrantGeneration; const { product, policy, readerGrantAction, readerGrantGeneration } = await catalog(target, { generation: currentReaderGrantGeneration, ensureGeneration: ensureReaderGrantGeneration, }); if (readerGrantAction === "ensure" && record.state && currentReaderGrantGeneration === 2_147_483_647) { throw consumerError("data_product_reader_grant_generation_exhausted", 409); } const safe = safeTarget(target); const configuration = { target: safe, product: { id: product.id, version: product.version }, policy, readerGrant: "target-scoped-server-only", readerGrantAction, readerGrantGeneration, }; const existingMatches = record.state && JSON.stringify(record.state.target) === JSON.stringify(safe) && record.state.product?.id === product.id && record.state.product?.version === product.version && record.state.policy?.id === policy.id && record.state.policy?.version === policy.version; const predecessorReaderGrantGeneration = Number.isSafeInteger(record.state?.predecessorReaderGrantGeneration) ? record.state.predecessorReaderGrantGeneration : null; const action = !record.state ? "create" : existingMatches && predecessorReaderGrantGeneration ? "finalize-reader-grant-rotation" : existingMatches ? (record.state.enabled ? "refresh" : "resume") : "replace"; return { schemaVersion: PLAN_SCHEMA_VERSION, planId: planHash({ configuration, action }), action, configuration, current: record.state ? { enabled: record.state.enabled === true, cursor: record.state.cursor, product: record.state.product } : null, effects: [ ...(readerGrantAction === "ensure" ? [record.state ? "ensure-successor-target-scoped-reader-grant" : "ensure-target-scoped-reader-grant"] : []), "persist-consumer-state", ...(action === "finalize-reader-grant-rotation" ? [] : ["bootstrap-scoped-snapshot"]), ...(predecessorReaderGrantGeneration ? ["revoke-predecessor-reader-grant"] : []), "share-one-upstream-stream-per-active-binding", ], destructive: false, }; } function initialState(target, product, policy, readerGrantGeneration) { const timestamp = new Date(now()).toISOString(); return { schemaVersion: STATE_SCHEMA_VERSION, consumerId: createHash("sha256").update(targetKey(target), "utf8").digest("hex").slice(0, 32), target: safeTarget(target), enabled: true, runtimeState: "bootstrapping", product: { id: product.id, version: product.version }, policy, readerGrantGeneration, predecessorReaderGrantGeneration: null, cursor: "0", snapshotGeneration: 0, subjects: {}, metrics: emptyMetrics(), timestamps: { createdAt: timestamp, updatedAt: timestamp, lastSnapshotAt: null, lastPatchAt: null, lastConnectedAt: null }, lastError: null, }; } function upstreamUrl(record, resource, after = "") { const url = new URL(`/internal/data-plane/v1/data-products/${encodeURIComponent(record.state.target.dataProductId)}/${resource}`, `${dataPlaneUrl}/`); if (after) url.searchParams.set("after", after); return url; } async function fetchSnapshot(record) { const token = await readReaderToken( record.state.target.applicationId, record.state.target.pageId, record.state.target.bindingId, activeReaderGrantGeneration(record.state), ); let response; try { response = await fetchImpl(upstreamUrl(record, "snapshot"), { headers: { authorization: `Bearer ${token}`, accept: "application/json" }, signal: AbortSignal.timeout(10_000), }); } catch { throw consumerError("data_product_runtime_unavailable", 503); } if (response.status === 401 || response.status === 403) throw consumerError("data_product_access_denied", 403); if (response.status === 404) throw consumerError("data_product_not_found", 404); if (!response.ok) throw consumerError("data_product_runtime_unavailable", 503); const payload = await response.json().catch(() => null); return sanitizeSnapshot(payload, record.target.binding); } async function bootstrap(record, { notify = true } = {}) { return serialize(record, async () => { record.state.runtimeState = "bootstrapping"; record.state.timestamps.updatedAt = new Date(now()).toISOString(); await writeState(record); try { const snapshot = await fetchSnapshot(record); assertEnvelopeProduct(snapshot, record.state.product, "data_product_snapshot_product_mismatch"); const previousKeys = new Set(Object.keys(record.state.subjects || {})); const subjects = {}; for (const fact of snapshot.facts) { assertFactContract(fact, record.state.policy); const key = factKey(fact); subjects[key] = { fact, status: safeStatusFromFact(fact, record.state.policy, now()), lastAppliedCursor: snapshot.cursor, }; previousKeys.delete(key); } const timestamp = new Date(now()).toISOString(); record.state = { ...record.state, enabled: true, runtimeState: record.listeners.size ? "connecting" : "idle", product: snapshot.dataProduct, cursor: snapshot.cursor, snapshotGeneration: record.state.snapshotGeneration + 1, subjects, metrics: { ...record.state.metrics, snapshotCommits: record.state.metrics.snapshotCommits + 1, snapshotRebaseRemovals: record.state.metrics.snapshotRebaseRemovals + previousKeys.size, }, timestamps: { ...record.state.timestamps, updatedAt: timestamp, lastSnapshotAt: timestamp }, lastError: null, }; await writeState(record); if (notify) emit(record, { event: "nodedc.data-product.resync-required.v1", data: { schemaVersion: "nodedc.data-product.resync-required/v1", dataProductId: record.state.product.id }, }); return snapshot; } catch (error) { const timestamp = new Date(now()).toISOString(); record.state.runtimeState = "error"; record.state.lastError = { code: safeErrorCode(error), at: timestamp }; record.state.timestamps.updatedAt = timestamp; await writeState(record); throw error; } }); } async function apply(input) { const planned = await plan(input); if (input.planId !== planned.planId) throw consumerError("data_product_consumer_plan_mismatch", 409); const target = await resolveTarget(input.applicationId, input.pageId, input.bindingId); const record = await recordFor(target); const plannedGeneration = planned.configuration.readerGrantGeneration; if (planned.action === "finalize-reader-grant-rotation") { const predecessorGeneration = record.state?.predecessorReaderGrantGeneration; if (!revokeReaderGrant || !Number.isSafeInteger(predecessorGeneration)) { throw consumerError("data_product_reader_grant_rotation_invalid", 500); } await revokeReaderGrant(record.target, { generation: predecessorGeneration }); record.state.predecessorReaderGrantGeneration = null; record.state.timestamps.updatedAt = new Date(now()).toISOString(); record.state.lastError = null; await writeState(record); return { plan: planned, consumer: safeStateSummary(record) }; } if (planned.configuration.readerGrantAction === "ensure") { if (!ensureReaderGrant) throw consumerError("data_product_reader_grant_not_found", 403); await ensureReaderGrant(target, { generation: plannedGeneration }); } const { product, policy, readerGrantAction, readerGrantGeneration } = await catalog(target, { generation: plannedGeneration, ensureGeneration: plannedGeneration, }); if (readerGrantAction !== "reuse") throw consumerError("data_product_reader_grant_not_ready", 409); if (readerGrantGeneration !== plannedGeneration) throw consumerError("data_product_reader_grant_generation_mismatch", 409); const previousTarget = record.target; const previousState = record.state ? structuredClone(record.state) : null; const previousGeneration = previousState ? activeReaderGrantGeneration(previousState) : null; if (record.stream) await stopStreamAndWait(record); if (!record.state || planned.action === "replace") { record.target = target; record.state = initialState(target, product, policy, plannedGeneration); if (previousGeneration && previousGeneration !== plannedGeneration) { record.state.predecessorReaderGrantGeneration = previousGeneration; } } else { record.target = target; record.state.target = safeTarget(target); record.state.product = { id: product.id, version: product.version }; record.state.policy = policy; record.state.readerGrantGeneration = plannedGeneration; record.state.enabled = true; record.state.runtimeState = "bootstrapping"; record.state.lastError = null; } try { await writeState(record); await bootstrap(record, { notify: false }); } catch (error) { if (previousState) { record.target = previousTarget; record.state = previousState; record.state.runtimeState = record.listeners.size ? "connecting" : "idle"; record.state.timestamps.updatedAt = new Date(now()).toISOString(); await writeState(record); if (record.listeners.size) startStream(record); } throw error; } const predecessorGeneration = record.state.predecessorReaderGrantGeneration; if (Number.isSafeInteger(predecessorGeneration)) { if (!revokeReaderGrant) throw consumerError("data_product_reader_grant_rotation_not_configured", 503); await revokeReaderGrant(previousTarget, { generation: predecessorGeneration }); record.state.predecessorReaderGrantGeneration = null; record.state.timestamps.updatedAt = new Date(now()).toISOString(); await writeState(record); } if (record.listeners.size) startStream(record); return { plan: planned, consumer: safeStateSummary(record) }; } async function ensureProvisioned(target) { const record = await recordFor(target); if (record.state) { if (!record.state.enabled) throw consumerError("data_product_consumer_stopped", 409); record.target = target; return record; } const planned = await plan(targetIdentity(target)); await apply({ ...targetIdentity(target), planId: planned.planId }); return recordFor(target); } async function refreshPresentation(record) { if (!record.state?.enabled) return []; return serialize(record, async () => { const operations = []; for (const subject of Object.values(record.state.subjects || {})) { const status = safeStatusFromFact(subject.fact, record.state.policy, now()); if (status === subject.status) continue; subject.status = status; operations.push({ op: "upsert", sourceId: subject.fact.sourceId, semanticType: subject.fact.semanticType, status }); } if (!operations.length) return operations; const timestamp = new Date(now()).toISOString(); record.state.metrics.staleTransitions += operations.filter((operation) => operation.status === "stale").length; record.state.timestamps.updatedAt = timestamp; await writeState(record); emit(record, { event: "nodedc.foundry.presentation-patch.v1", data: { schemaVersion: PRESENTATION_PATCH_SCHEMA_VERSION, generatedAt: timestamp, operations }, }); return operations; }); } async function applyPatch(record, patch) { return serialize(record, async () => { const currentCursor = BigInt(record.state.cursor); const patchCursor = BigInt(patch.cursor); if (patchCursor <= currentCursor) { record.state.metrics.replayedPatches += 1; return { replayed: true }; } if (patch.previousCursor !== record.state.cursor) throw consumerError("resync_required", 409); const subjects = { ...record.state.subjects }; const emittedOperations = []; for (const operation of patch.operations) { if (operation.op === "upsert") { assertFactContract(operation.fact, record.state.policy); const key = factKey(operation.fact); const subject = { fact: operation.fact, status: safeStatusFromFact(operation.fact, record.state.policy, now()), lastAppliedCursor: patch.cursor, }; subjects[key] = subject; emittedOperations.push({ op: "upsert", fact: decorateFact(subject) }); } else if (operation.op === "remove") { const key = `${operation.semanticType}\u0000${operation.sourceId}`; if (subjects[key]) { delete subjects[key]; emittedOperations.push(operation); } } } const timestamp = new Date(now()).toISOString(); record.state = { ...record.state, runtimeState: "connected", cursor: patch.cursor, subjects, metrics: { ...record.state.metrics, patchCommits: record.state.metrics.patchCommits + 1, patchOperations: record.state.metrics.patchOperations + emittedOperations.length, canonicalRemovals: record.state.metrics.canonicalRemovals + emittedOperations.filter((operation) => operation.op === "remove").length, }, timestamps: { ...record.state.timestamps, updatedAt: timestamp, lastPatchAt: timestamp }, lastError: null, }; // Persist the new cursor and semantic state before acknowledging it to // any browser listener. A crash can replay a patch but cannot skip one. await writeState(record); if (emittedOperations.length) emit(record, { event: "nodedc.data-product.patch.v1", id: patch.cursor, data: { ...patch, operations: emittedOperations }, }); return { replayed: false }; }); } async function consumeOnce(record, signal) { const token = await readReaderToken( record.state.target.applicationId, record.state.target.pageId, record.state.target.bindingId, activeReaderGrantGeneration(record.state), ); const url = upstreamUrl(record, "stream", record.state.cursor); const response = openStream ? await openStream({ url, token, signal }) : await fetchImpl(url, { headers: { authorization: `Bearer ${token}`, accept: "text/event-stream", connection: "close" }, signal, }); if (response.status === 409) throw consumerError("resync_required", 409); if (response.status === 401 || response.status === 403) throw consumerError("data_product_access_denied", 403); if (!response.ok || !response.body || !String(response.headers.get("content-type") || "").startsWith("text/event-stream")) { throw consumerError("data_product_runtime_unavailable", 503); } const timestamp = new Date(now()).toISOString(); record.state.runtimeState = "connected"; record.state.timestamps.lastConnectedAt = timestamp; record.state.timestamps.updatedAt = timestamp; record.state.lastError = null; await writeState(record); const reader = response.body.getReader(); if (record.stream) record.stream.reader = reader; const decoder = new TextDecoder(); let pending = ""; let upstreamDone = false; try { while (!signal.aborted && record.listeners.size) { const next = await reader.read(); if (next.done) { upstreamDone = true; break; } pending += decoder.decode(next.value, { stream: true }).replace(/\r\n/g, "\n"); let separator; while ((separator = pending.indexOf("\n\n")) !== -1) { const event = parseSseBlock(pending.slice(0, separator)); pending = pending.slice(separator + 2); if (event.event !== "nodedc.data-product.patch.v1") continue; let payload = null; try { payload = JSON.parse(event.data); } catch { payload = null; } const patch = sanitizePatch(payload, record.target.binding); if (!patch) throw consumerError("data_product_patch_contract_invalid", 502); assertEnvelopeProduct(patch, record.state.product, "data_product_patch_product_mismatch"); await applyPatch(record, patch); } } } finally { if (!upstreamDone) await reader.cancel().catch(() => undefined); reader.releaseLock(); if (record.stream?.reader === reader) record.stream.reader = null; } } function startStream(record) { if (closed || record.stream || !record.state?.enabled || !record.listeners.size) return; if (record.idleTimer) clearTimeout(record.idleTimer); record.idleTimer = null; const controller = new AbortController(); record.stream = { controller, promise: null, reader: null }; record.stream.promise = (async () => { let delay = reconnectMinMs; while (!closed && !controller.signal.aborted && record.listeners.size && record.state.enabled) { try { record.state.runtimeState = "connecting"; await writeState(record); await consumeOnce(record, controller.signal); delay = reconnectMinMs; if (!controller.signal.aborted && record.listeners.size) throw consumerError("data_product_stream_closed", 503); } catch (error) { if (controller.signal.aborted || closed || !record.listeners.size) break; if (error?.message === "resync_required") { await bootstrap(record).catch(() => undefined); } else { const timestamp = new Date(now()).toISOString(); record.state.runtimeState = "reconnecting"; record.state.metrics.reconnects += 1; record.state.lastError = { code: safeErrorCode(error), at: timestamp }; record.state.timestamps.updatedAt = timestamp; await writeState(record); } await waitForAbortableDelay(delay, controller.signal); delay = Math.min(reconnectMaxMs, delay * 2); } } })().finally(async () => { if (record.stream?.controller === controller) record.stream = null; if (record.state?.enabled && !record.listeners.size) { record.state.runtimeState = "idle"; record.state.timestamps.updatedAt = new Date(now()).toISOString(); await writeState(record).catch(() => undefined); } }); } function stopStream(record) { if (record.idleTimer) clearTimeout(record.idleTimer); record.idleTimer = null; void record.stream?.reader?.cancel().catch(() => undefined); record.stream?.controller.abort(); } async function stopStreamAndWait(record) { const pending = record.stream?.promise; stopStream(record); if (!pending) return; await Promise.race([ pending.catch(() => undefined), new Promise((resolve) => setTimeout(resolve, 750)), ]); } async function snapshot(input) { const target = input?.binding ? input : await resolveTarget(input.applicationId, input.pageId, input.bindingId); const record = await ensureProvisioned(target); // A removed target grant revokes browser reads immediately even though the // last safe snapshot remains persisted for rollback/diagnostics. await readReaderToken( record.state.target.applicationId, record.state.target.pageId, record.state.target.bindingId, activeReaderGrantGeneration(record.state), ); await refreshPresentation(record); return { schemaVersion: "nodedc.data-product.snapshot/v1", dataProduct: record.state.product, generatedAt: record.state.timestamps.lastSnapshotAt || record.state.timestamps.updatedAt, cursor: record.state.cursor, facts: Object.values(record.state.subjects).map(decorateFact), }; } async function subscribe(input, listener) { const target = input?.binding ? input : await resolveTarget(input.applicationId, input.pageId, input.bindingId); const record = await ensureProvisioned(target); const after = String(input.after || ""); if (after && !cursorPattern.test(after)) throw consumerError("stream_cursor_invalid"); if (after && after !== record.state.cursor) return { resyncRequired: true, cursor: record.state.cursor, release() {} }; const leaseId = randomUUID(); record.listeners.set(leaseId, listener); listener({ event: "nodedc.data-product.ready.v1", data: { schemaVersion: "nodedc.data-product.ready/v1", dataProductId: record.state.product.id, cursor: record.state.cursor, emittedAt: new Date(now()).toISOString(), }, }); startStream(record); let released = false; return { resyncRequired: false, cursor: record.state.cursor, release() { if (released) return; released = true; record.listeners.delete(leaseId); if (!record.listeners.size && record.stream && !record.idleTimer) { record.idleTimer = setTimeout(() => { record.idleTimer = null; if (!record.listeners.size) stopStream(record); }, idleStopMs); record.idleTimer.unref?.(); } }, }; } async function status(input) { const target = await resolveTarget(input.applicationId, input.pageId, input.bindingId); const record = await recordFor(target); if (!record.state) return { found: false, target: safeTarget(target), readerGrant: "target-scoped-server-only" }; await refreshPresentation(record); return { found: true, consumer: safeStateSummary(record) }; } async function accept(input) { const target = await resolveTarget(input.applicationId, input.pageId, input.bindingId); const record = await ensureProvisioned(target); const timeoutMs = Math.min(30_000, Math.max(250, Number(input.timeoutMs ?? 5_000))); const minSubjectCount = Math.min(5_000, Math.max(0, Number(input.minSubjectCount ?? 1))); const minPatchCount = Math.min(1_000, Math.max(0, Number(input.minPatchCount ?? 0))); if (![timeoutMs, minSubjectCount, minPatchCount].every(Number.isInteger)) throw consumerError("data_product_consumer_acceptance_input_invalid"); const baselinePatchCount = record.state.metrics.patchCommits; const lease = await subscribe(target, () => undefined); const deadline = now() + timeoutMs; try { while (now() < deadline) { const patchDelta = record.state.metrics.patchCommits - baselinePatchCount; if (Object.keys(record.state.subjects).length >= minSubjectCount && patchDelta >= minPatchCount) break; await new Promise((resolve) => setTimeout(resolve, 50)); } await refreshPresentation(record); const subjectKeys = Object.keys(record.state.subjects); const duplicateCount = subjectKeys.length - new Set(subjectKeys).size; const patchDelta = record.state.metrics.patchCommits - baselinePatchCount; const upstreamStreamCount = record.stream ? 1 : 0; const checks = { bindingResolved: true, targetScopedReader: true, snapshotCommitted: record.state.metrics.snapshotCommits > 0, cursorPersisted: cursorPattern.test(record.state.cursor), minimumSubjects: subjectKeys.length >= minSubjectCount, minimumNewPatches: patchDelta >= minPatchCount, duplicateSubjects: duplicateCount === 0, singleUpstreamStream: upstreamStreamCount <= 1, secretAndEndpointExcluded: true, }; return { schemaVersion: ACCEPTANCE_SCHEMA_VERSION, accepted: Object.values(checks).every(Boolean), checks, observed: { subjectCount: subjectKeys.length, newPatchCount: patchDelta, cursor: record.state.cursor, runtimeState: record.state.runtimeState }, consumer: safeStateSummary(record), }; } finally { lease.release(); } } async function rollback(input) { const target = await resolveTarget(input.applicationId, input.pageId, input.bindingId); const record = await recordFor(target); if (!record.state) throw consumerError("data_product_consumer_not_found", 404); stopStream(record); record.listeners.clear(); record.state.enabled = false; record.state.runtimeState = "stopped"; record.state.timestamps.updatedAt = new Date(now()).toISOString(); record.state.lastError = null; await writeState(record); return { rolledBack: true, snapshotPreserved: true, consumer: safeStateSummary(record) }; } async function resumePersisted() { await ready; for (const entry of await readdir(stateDir, { withFileTypes: true })) { if (!entry.isFile() || !/^[0-9a-f]{64}\.json$/.test(entry.name)) continue; try { const state = JSON.parse(await readFile(join(stateDir, entry.name), "utf8")); if (state?.schemaVersion !== STATE_SCHEMA_VERSION || !state.target) continue; const target = await resolveTarget(state.target.applicationId, state.target.pageId, state.target.bindingId); const record = await recordFor(target); record.target = target; if (record.state?.enabled) { record.state.runtimeState = "idle"; record.state.timestamps.updatedAt = new Date(now()).toISOString(); await writeState(record); } } catch { // One damaged or obsolete target cannot stop Foundry from serving the // remaining applications. Its file remains available for diagnostics. } } } const staleTimer = setInterval(() => { for (const record of records.values()) void refreshPresentation(record).catch(() => undefined); }, staleSweepMs); staleTimer.unref?.(); async function shutdown() { if (closed) return; closed = true; clearInterval(staleTimer); const streams = []; for (const record of records.values()) { stopStream(record); record.listeners.clear(); if (record.stream?.promise) streams.push(record.stream.promise); } await Promise.race([ Promise.allSettled(streams), new Promise((resolve) => setTimeout(resolve, 750)), ]); } return { plan, apply, snapshot, subscribe, status, accept, rollback, resumePersisted, shutdown, }; } export const foundryDataProductConsumerSchemas = Object.freeze({ state: STATE_SCHEMA_VERSION, plan: PLAN_SCHEMA_VERSION, acceptance: ACCEPTANCE_SCHEMA_VERSION, presentationPatch: PRESENTATION_PATCH_SCHEMA_VERSION, });