const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const { pathToFileURL } = require('node:url'); const packageRoot = path.resolve(__dirname, '..'); const packageJson = require(path.join(packageRoot, 'package.json')); const nodeSpecs = [ ['NdcDataProductPublish', 'ndcDataProductPublish', 'NDC Data Product Publish'], ['NdcDataProductRead', 'ndcDataProductRead', 'NDC Data Product Read'], ['NdcFoundryBinding', 'ndcFoundryBinding', 'NDC Foundry Binding'], ]; const credentialSpecs = [ ['NdcDataProductWriterApi', 'ndcDataProductWriterApi', 'opaque'], ['NdcDataProductReaderApi', 'ndcDataProductReaderApi', 'opaque'], ['NdcFoundryBindingApi', 'ndcFoundryBindingApi', 'opaque'], ['NdcProviderRotatingAccessApi', 'ndcProviderRotatingAccessApi', 'rotating'], ]; const forbiddenNodeParameter = /(url|provider|tenant|connection|token|secret|password|credential)/i; const forbiddenBindingKey = /(provider|tenant|connection|endpoint|url|credential|token|secret|payload)/i; const forbiddenProductBrand = /n8n(?:-nodes-ndc)?/i; const ndcNodeIcon = { light: 'file:../../icons/ndc.svg', dark: 'file:../../icons/ndc.dark.svg', }; async function main() { assert.equal(packageJson.name, 'n8n-nodes-ndc'); assert.equal(packageJson.dependencies, undefined, 'private NDC nodes must not add runtime supply-chain dependencies'); for (const lifecycle of ['preinstall', 'install', 'postinstall']) { assert.equal(packageJson.scripts?.[lifecycle], undefined, `${lifecycle} lifecycle script is forbidden`); } assert.equal(packageJson.n8n.nodes.length, nodeSpecs.length, 'every registered custom node must be covered by policy'); assert.deepEqual( [...packageJson.n8n.nodes].sort(), nodeSpecs .map(([className]) => `dist/nodes/${className}/${className}.node.js`) .sort(), 'registered custom nodes and policy specs must stay in lockstep', ); const nodes = new Map(); for (const [className, internalName, displayName] of nodeSpecs) { const modulePath = path.join(packageRoot, 'dist', 'nodes', className, `${className}.node.js`); const NodeClass = require(modulePath)[className]; const node = new NodeClass(); nodes.set(className, node); assert.match(node.description.displayName, /^NDC /); assert.match(node.description.defaults.name, /^NDC /); assert.equal(node.description.displayName, displayName); assert.equal(node.description.defaults.name, displayName); assert.deepEqual(node.description.icon, ndcNodeIcon); assert.match(node.description.name, /^ndc[A-Z]/); assert.equal(node.description.name, internalName); assert.equal(`${packageJson.name}.${node.description.name}`, `${packageJson.name}.${internalName}`); assert.notEqual( node.description.usableAsTool, true, `${className} must not generate an additional *Tool runtime type`, ); for (const property of node.description.properties) { assert.doesNotMatch(property.name, forbiddenNodeParameter, `${className}.${property.name} is transport-specific`); } assert.equal(node.description.credentials.length, 1); assert.match(node.description.credentials[0].name, /^ndc[A-Z]/); assertProductSurfaceHasNdcBrand(node.description, className); } const credentials = new Map(); for (const [className, internalName, kind] of credentialSpecs) { const modulePath = path.join(packageRoot, 'dist', 'credentials', `${className}.credentials.js`); const CredentialClass = require(modulePath)[className]; const credential = new CredentialClass(); credentials.set(className, credential); assert.match(credential.displayName, /^NDC /); assert.equal(credential.name, internalName); assert.deepEqual(credential.icon, { light: 'file:../icons/ndc.svg', dark: 'file:../icons/ndc.dark.svg', }); if (kind === 'opaque') { assert.deepEqual(credential.properties.map((property) => property.name), ['capability']); assert.equal(credential.properties[0].typeOptions.password, true); assert.equal(credential.authenticate.properties.headers.Authorization, '=Bearer {{$credentials.capability}}'); } else { assert.deepEqual( credential.properties.map((property) => property.name), ['refreshToken', 'accessToken', 'accessExpiresAt'], ); assert.equal(credential.properties[0].typeOptions.password, true); assert.equal(credential.properties[1].type, 'hidden'); assert.equal(credential.properties[1].typeOptions.password, true); assert.equal(credential.properties[1].typeOptions.expirable, true); assert.equal(credential.authenticate.properties.headers.Authorization, '=Bearer {{$credentials.accessToken}}'); assert.equal(credential.test.request.url, 'https://api.geliospro.com/api/v1/auth'); } assertProductSurfaceHasNdcBrand({ displayName: credential.displayName, documentationUrl: credential.documentationUrl, properties: credential.properties, }, className); } await assertProviderRotatingCredential(credentials.get('NdcProviderRotatingAccessApi')); for (const icon of ['ndc.svg', 'ndc.dark.svg']) { const iconPath = path.join(packageRoot, 'dist', 'icons', icon); assert.equal(fs.existsSync(iconPath), true, `${icon} was not copied`); assert.match(fs.readFileSync(iconPath, 'utf8'), /aria-label="NDC"/, `${icon} must expose the NDC brand`); } const contracts = require(path.join(packageRoot, 'dist', 'nodes', 'shared', 'contracts.js')); const constants = require(path.join(packageRoot, 'dist', 'nodes', 'shared', 'constants.js')); const http = require(path.join(packageRoot, 'dist', 'nodes', 'shared', 'http.js')); const externalContract = await import(pathToFileURL( path.resolve(packageRoot, '..', 'external-provider-contract', 'src', 'index.mjs'), ).href); const items = [ { json: { sourceId: 'fleet.unit.42', semanticType: 'map.moving_object', observedAt: '2026-07-15T12:00:00.000Z', attributes: { speedKph: 12 }, geometry: { type: 'Point', coordinates: [37.61, 55.75] }, }, }, ]; const publish = contracts.buildPublishPayload( items, 'execution-42', 'workflow-7', 'node-3', 'fleet.positions.current.v1', 0, ); assert.deepEqual(externalContract.validateDataProductPublish(publish), { ok: true, errors: [] }); assert.deepEqual( publish, contracts.buildPublishPayload(items, 'execution-42', 'workflow-7', 'node-3', 'fleet.positions.current.v1', 0), 'same execution chunk must produce the same idempotency envelope', ); assert.notEqual( publish.batch.idempotencyKey, contracts.buildPublishPayload(items, 'execution-42', 'workflow-7', 'node-3', 'fleet.positions.current.v2', 0).batch.idempotencyKey, ); const zoneItems = [{ json: { sourceId: 'gelios-zone-42', semanticType: 'map.zone', observedAt: '2026-07-15T12:10:00.000Z', attributes: { display_name: 'Zone 42' }, geometry: { type: 'Polygon', coordinates: [[[37.60, 55.74], [37.62, 55.74], [37.62, 55.76], [37.60, 55.74]]], }, }, }]; const replacement = contracts.buildPublishPayload( zoneItems, 'execution-43', 'workflow-7', 'node-zones', 'map.zones.current.v1', 0, 'replace', ); assert.equal(externalContract.validateDataProductPublish(replacement).ok, true); assert.equal(replacement.batch.mode, 'replace'); assert.equal(replacement.batch.generationAt, zoneItems[0].json.observedAt); const replacementBatch = contracts.buildPublishPayload( [{ json: { generationAt: zoneItems[0].json.observedAt, facts: zoneItems.map((item) => item.json) } }], 'execution-44', 'workflow-7', 'node-zones', 'map.zones.current.v1', 0, 'replace', ); assert.deepEqual(replacementBatch.facts, replacement.facts); assert.equal(replacementBatch.batch.generationAt, zoneItems[0].json.observedAt); const emptyReplacement = contracts.buildPublishPayload( [{ json: { generationAt: '2026-07-15T13:00:00.000Z', facts: [] } }], 'execution-45', 'workflow-7', 'node-zones', 'map.zones.current.v1', 0, 'replace', ); assert.deepEqual(emptyReplacement.facts, []); assert.equal(externalContract.validateDataProductPublish(emptyReplacement).ok, true); assert.throws( () => contracts.buildPublishPayload( [{ json: { generationAt: '2026-07-15T13:00:00.000Z', facts: zoneItems.map((item) => item.json) } }], 'execution-46', 'workflow-7', 'node-zones', 'map.zones.current.v1', 0, 'replace', ), /replace_generationAt_observedAt_mismatch/, ); assert.throws( () => contracts.buildPublishPayload( [{ json: { ...items[0].json, attributes: { accessToken: 'forbidden' } } }], 'execution-42', 'workflow-7', 'node-3', 'fleet.positions.current.v1', 0, ), /secret_material_forbidden/, ); const foundry = contracts.buildFoundryBindingPayload({ applicationId: '11111111-1111-4111-8111-111111111111', pageId: 'map', binding: { id: 'fleet-live-points', dataProductId: 'fleet.positions.current.v1', slotId: 'points', semanticTypes: ['map.moving_object'], fieldProjection: ['coordinates.longitude', 'coordinates.latitude'], }, }); assertNoForbiddenKeys(foundry, forbiddenBindingKey); assert.equal(externalContract.validateFoundryBindingUpsert(foundry).ok, true); assert.equal(foundry.schemaVersion, externalContract.FOUNDRY_BINDING_UPSERT_SCHEMA_VERSION); assert.equal(foundry.binding.dataProductId, 'fleet.positions.current.v1'); assert.equal(foundry.binding.slotId, 'points'); assert.match(foundry.idempotencyKey, /^foundry-binding-[a-f0-9]{32}$/); assert.equal(constants.DATA_PRODUCT_WRITER_CATALOG_PATH, '/internal/data-plane/v1/writer/data-products'); assert.equal(constants.DATA_PRODUCT_READER_CATALOG_PATH, '/internal/data-plane/v1/reader/data-products'); assert.equal(constants.DATA_PRODUCT_BASE_PATH, '/internal/data-plane/v1/data-products'); assert.equal(constants.DATA_PLANE_DEFAULT_BASE_URL, 'http://external-data-plane:18106'); assert.equal(constants.FOUNDRY_DEFAULT_BASE_URL, 'http://nodedc-module-foundry:3333'); const previousDefaultProbe = process.env.NDC_DATA_PLANE_BASE_URL; delete process.env.NDC_DATA_PLANE_BASE_URL; assert.equal( http.serviceBaseUrl(constants.DATA_PLANE_BASE_URL_ENV, constants.DATA_PLANE_DEFAULT_BASE_URL), 'http://external-data-plane:18106', ); restoreEnvironment('NDC_DATA_PLANE_BASE_URL', previousDefaultProbe); const readProperties = nodes.get('NdcDataProductRead').description.properties.map((property) => property.name); assert.equal(readProperties.includes('mode'), true, 'read mode must expose snapshot/history explicitly'); assert.equal(readProperties.includes('cursor'), true, 'history endpoint must expose its opaque continuation cursor'); assert.equal(readProperties.includes('sourceIds'), true, 'history filtering remains provider-neutral'); const pageSizeProperty = nodes.get('NdcDataProductRead').description.properties.find((property) => property.name === 'pageSize'); assert.equal(pageSizeProperty.typeOptions.minValue, 1); assert.equal(pageSizeProperty.typeOptions.maxValue, 5000, 'bounded snapshot v1 must not expose more than 5000 facts'); assert.equal(pageSizeProperty.default <= pageSizeProperty.typeOptions.maxValue, true); const slotProperty = nodes.get('NdcFoundryBinding').description.properties.find((property) => property.name === 'slotId'); assert.equal(slotProperty.default, 'points'); await assertNodeHttpContracts(nodes, externalContract, constants); console.log('n8n-nodes-ndc package policy: ok'); } async function assertProviderRotatingCredential(credential) { let requestCount = 0; let releaseRequest; const request = new Promise((resolve) => { releaseRequest = resolve; }); const helper = { helpers: { async httpRequest(options) { requestCount += 1; assert.equal(options.method, 'POST'); assert.equal(options.url, 'https://api.geliospro.com/api/v1/auth/refresh'); assert.deepEqual(options.body, { refresh_token: 'refresh-old-single-flight' }); assert.equal(options.headers.Authorization, undefined); return request; }, }, }; const credentials = { refreshToken: 'refresh-old-single-flight', accessToken: '' }; const first = credential.preAuthentication.call(helper, credentials); const second = credential.preAuthentication.call(helper, credentials); await new Promise((resolve) => setImmediate(resolve)); assert.equal(requestCount, 1, 'rotating refresh must be single-flight inside the one-service L2 runtime'); releaseRequest({ access_token: 'access-new', refresh_token: 'refresh-new', token_type: 'Bearer', expires_in: 3600, }); const [firstTokens, secondTokens] = await Promise.all([first, second]); assert.deepEqual(firstTokens, secondTokens); assert.equal(firstTokens.accessToken, 'access-new'); assert.equal(firstTokens.refreshToken, 'refresh-new'); assert.match(firstTokens.accessExpiresAt, /^\d{4}-\d{2}-\d{2}T/); const replay = await credential.preAuthentication.call(helper, credentials); assert.deepEqual(replay, firstTokens); assert.equal(requestCount, 1, 'a stale concurrent caller must reuse the recent rotation result'); const leakedRefresh = 'refresh-token-must-not-leak'; await assert.rejects( credential.preAuthentication.call({ helpers: { async httpRequest() { throw new Error(`remote rejected ${leakedRefresh}`); }, }, }, { refreshToken: leakedRefresh, accessToken: '' }), (error) => error?.message === 'provider_access_refresh_failed' && !error.message.includes(leakedRefresh), ); } function assertProductSurfaceHasNdcBrand(surface, className) { const visibleStrings = []; collectVisibleStrings(surface, visibleStrings); for (const value of visibleStrings) { assert.doesNotMatch(value, forbiddenProductBrand, `${className} leaks a technical package/runtime brand: ${value}`); } } function collectVisibleStrings(value, output, key = '') { if (typeof value === 'string') { if (!['name', 'type', 'value'].includes(key)) output.push(value); return; } if (Array.isArray(value)) { for (const item of value) collectVisibleStrings(item, output, key); return; } if (!value || typeof value !== 'object') return; for (const [childKey, childValue] of Object.entries(value)) { if (['credentials', 'icon'].includes(childKey)) continue; collectVisibleStrings(childValue, output, childKey); } } async function assertNodeHttpContracts(nodes, externalContract, constants) { const previousDataPlaneUrl = process.env.NDC_DATA_PLANE_BASE_URL; const previousFoundryUrl = process.env.NDC_FOUNDRY_BASE_URL; process.env.NDC_DATA_PLANE_BASE_URL = 'http://data-plane.test/'; process.env.NDC_FOUNDRY_BASE_URL = 'http://foundry.test/'; try { const publishCalls = []; const publishContext = executionContext({ parameters: { dataProductId: 'fleet.positions.current.v1', sequence: 0 }, input: [{ json: canonicalFact() }], response: { ok: true, publishedFactCount: 1 }, calls: publishCalls, }); const publishResult = await nodes.get('NdcDataProductPublish').execute.call(publishContext); assert.equal(publishCalls[0].credentialName, 'ndcDataProductWriterApi'); assert.equal(publishCalls[0].options.method, 'POST'); assert.equal( publishCalls[0].options.url, 'http://data-plane.test/internal/data-plane/v1/data-products/fleet.positions.current.v1/publish', ); assert.equal(externalContract.validateDataProductPublish(publishCalls[0].options.body).ok, true); assert.equal(publishResult[0][0].json.publishedFactCount, 1); const replacementCalls = []; const replacementContext = executionContext({ typeVersion: 2, parameters: { dataProductId: 'map.zones.current.v1', publishMode: 'replace', sequence: 0 }, input: [{ json: { generationAt: '2026-07-15T12:10:00.000Z', facts: [{ sourceId: 'gelios-zone-42', semanticType: 'map.zone', observedAt: '2026-07-15T12:10:00.000Z', attributes: { display_name: 'Zone 42' }, geometry: { type: 'Polygon', coordinates: [[[37.60, 55.74], [37.62, 55.74], [37.62, 55.76], [37.60, 55.74]]] }, }], }, }], response: { ok: true, publishedFactCount: 1, currentRemovedCount: 0 }, calls: replacementCalls, }); await nodes.get('NdcDataProductPublish').execute.call(replacementContext); assert.equal(replacementCalls[0].options.body.batch.mode, 'replace'); assert.equal(externalContract.validateDataProductPublish(replacementCalls[0].options.body).ok, true); const readCalls = []; const readContext = executionContext({ parameters: { dataProductId: 'fleet.positions.current.v1', pageSize: 1000 }, response: { schemaVersion: 'nodedc.data-product.snapshot/v1', dataProduct: { id: 'fleet.positions.current.v1', version: '1.0.0' }, generatedAt: '2026-07-15T12:00:01.000Z', cursor: '1', facts: [{ ...canonicalFact(), receivedAt: '2026-07-15T12:00:00.100Z' }], }, calls: readCalls, }); const readResult = await nodes.get('NdcDataProductRead').execute.call(readContext); assert.equal(readCalls[0].credentialName, 'ndcDataProductReaderApi'); assert.equal(readCalls[0].options.method, 'GET'); assert.equal( readCalls[0].options.url, 'http://data-plane.test/internal/data-plane/v1/data-products/fleet.positions.current.v1/snapshot', ); assert.deepEqual(readCalls[0].options.qs, { limit: 1000 }); assert.equal(readResult[0][0].json.sourceId, 'fleet.unit.42'); const historyCalls = []; const historyResponse = { schemaVersion: 'nodedc.data-product.history/v1', dataProduct: { id: 'fleet.positions.current.v1', version: '1.0.0' }, generatedAt: '2026-07-15T12:05:00.000Z', query: { from: '2026-07-15T12:00:00.000Z', to: '2026-07-15T12:05:00.000Z', resolutionMs: 60000, sourceIds: ['fleet.unit.42'], order: 'asc', }, facts: [{ ...canonicalFact(), receivedAt: '2026-07-15T12:00:00.100Z', bucketStart: '2026-07-15T12:00:00.000Z' }], nextCursor: 'opaque_cursor', }; const historyContext = executionContext({ parameters: { mode: 'history', dataProductId: 'fleet.positions.current.v1', from: '2026-07-15T12:00:00.000Z', to: '2026-07-15T12:05:00.000Z', resolutionMs: 60000, sourceIds: 'fleet.unit.42', limit: 1000, cursor: '', }, response: historyResponse, calls: historyCalls, }); const historyResult = await nodes.get('NdcDataProductRead').execute.call(historyContext); assert.equal(historyCalls[0].credentialName, 'ndcDataProductReaderApi'); assert.equal(historyCalls[0].options.method, 'GET'); assert.equal( historyCalls[0].options.url, 'http://data-plane.test/internal/data-plane/v1/data-products/fleet.positions.current.v1/history', ); assert.deepEqual(historyCalls[0].options.qs, { from: '2026-07-15T12:00:00.000Z', to: '2026-07-15T12:05:00.000Z', resolutionMs: 60000, sourceIds: 'fleet.unit.42', limit: 1000, }); assert.deepEqual(historyResult[0][0].json, historyResponse); const foundryCalls = []; const foundryContext = executionContext({ parameters: { applicationId: '11111111-1111-4111-8111-111111111111', pageId: 'map', bindingId: 'fleet-live-points', dataProductId: 'fleet.positions.current.v1', slotId: 'points', semanticTypes: ['map.moving_object'], fieldProjection: ['coordinates.longitude'], }, response: { ok: true, binding: { id: 'fleet-live-points' } }, calls: foundryCalls, }); await nodes.get('NdcFoundryBinding').execute.call(foundryContext); assert.equal(foundryCalls[0].credentialName, 'ndcFoundryBindingApi'); assert.equal(foundryCalls[0].options.method, 'POST'); assert.equal(foundryCalls[0].options.url, 'http://foundry.test/internal/foundry/v1/data-product-bindings'); assertNoForbiddenKeys(foundryCalls[0].options.body, forbiddenBindingKey); const catalogCalls = []; const catalogContext = { helpers: { async httpRequestWithAuthentication(credentialName, options) { catalogCalls.push({ credentialName, options }); return { dataProducts: [{ id: 'fleet.positions.current.v1', version: '1.0.0' }] }; }, }, }; const writerOptions = await nodes.get('NdcDataProductPublish').methods.loadOptions.getDataProducts.call(catalogContext); const readerOptions = await nodes.get('NdcDataProductRead').methods.loadOptions.getDataProducts.call(catalogContext); assert.deepEqual(writerOptions, [{ name: 'fleet.positions.current.v1 ยท 1.0.0', value: 'fleet.positions.current.v1' }]); assert.deepEqual(readerOptions, writerOptions); assert.equal(catalogCalls[0].options.url, `http://data-plane.test${constants.DATA_PRODUCT_WRITER_CATALOG_PATH}`); assert.equal(catalogCalls[1].options.url, `http://data-plane.test${constants.DATA_PRODUCT_READER_CATALOG_PATH}`); } finally { restoreEnvironment('NDC_DATA_PLANE_BASE_URL', previousDataPlaneUrl); restoreEnvironment('NDC_FOUNDRY_BASE_URL', previousFoundryUrl); } } function executionContext({ parameters, input = [{ json: {} }], response, calls, typeVersion = 1 }) { return { getNodeParameter(name, _index, defaultValue) { return Object.prototype.hasOwnProperty.call(parameters, name) ? parameters[name] : defaultValue; }, getInputData() { return input; }, getExecutionId() { return 'execution-42'; }, getWorkflow() { return { id: 'workflow-7' }; }, getNode() { return { id: 'node-3', typeVersion }; }, helpers: { async httpRequestWithAuthentication(credentialName, options) { calls.push({ credentialName, options }); return response; }, }, }; } function canonicalFact() { return { sourceId: 'fleet.unit.42', semanticType: 'map.moving_object', observedAt: '2026-07-15T12:00:00.000Z', attributes: { speedKph: 12 }, geometry: { type: 'Point', coordinates: [37.61, 55.75] }, }; } function restoreEnvironment(name, value) { if (value === undefined) delete process.env[name]; else process.env[name] = value; } function assertNoForbiddenKeys(value, pattern, pathPrefix = 'body') { if (Array.isArray(value)) { value.forEach((entry, index) => assertNoForbiddenKeys(entry, pattern, `${pathPrefix}[${index}]`)); return; } if (!value || typeof value !== 'object') return; for (const [key, child] of Object.entries(value)) { assert.doesNotMatch(key, pattern, `${pathPrefix}.${key} contains transport material`); assertNoForbiddenKeys(child, pattern, `${pathPrefix}.${key}`); } } main().catch((error) => { console.error(error); process.exitCode = 1; });