From cad3153516626610e65203a12981ea9fea2347cd Mon Sep 17 00:00:00 2001 From: AdriGeorge Date: Mon, 23 Feb 2026 13:44:52 +0200 Subject: [PATCH 1/6] chore: add provider identity and tc --- src/components/httpRoutes/rootEndpoint.ts | 19 ++++++++++++-- src/components/httpRoutes/utils.ts | 31 +++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 src/components/httpRoutes/utils.ts diff --git a/src/components/httpRoutes/rootEndpoint.ts b/src/components/httpRoutes/rootEndpoint.ts index 4f3a60684..73bbc41b5 100644 --- a/src/components/httpRoutes/rootEndpoint.ts +++ b/src/components/httpRoutes/rootEndpoint.ts @@ -2,6 +2,7 @@ import express from 'express' import { HTTP_LOGGER } from '../../utils/logging/common.js' import { getConfiguration } from '../../utils/index.js' import { getAllServiceEndpoints } from './index.js' +import { getProviderIdentityAndTC } from './utils.js' export const rootEndpointRoutes = express.Router() rootEndpointRoutes.get('/', async (req, res) => { @@ -10,12 +11,26 @@ rootEndpointRoutes.get('/', async (req, res) => { HTTP_LOGGER.warn(`Supported networks not defined`) } const keyManager = req.oceanNode.getKeyManager() - res.json({ + const rootResponse: Record = { nodeId: keyManager.getPeerId().toString(), chainIds: config.supportedNetworks ? Object.keys(config.supportedNetworks) : [], providerAddress: keyManager.getEthAddress(), serviceEndpoints: getAllServiceEndpoints(), software: 'Ocean-Node', version: '0.0.1' - }) + } + + const { license, proofOfIdentity } = getProviderIdentityAndTC() + if (license) { + rootResponse.license = license + } else { + HTTP_LOGGER.warn('LICENSE not present or invalid JSON object') + } + if (proofOfIdentity) { + rootResponse.proofOfIdentity = proofOfIdentity + } else { + HTTP_LOGGER.warn('PROOF_OF_IDENTITY not present or invalid JSON object') + } + + res.json(rootResponse) }) diff --git a/src/components/httpRoutes/utils.ts b/src/components/httpRoutes/utils.ts new file mode 100644 index 000000000..a90cf8d60 --- /dev/null +++ b/src/components/httpRoutes/utils.ts @@ -0,0 +1,31 @@ +function parseObj(envKey: string): Record | null { + const rawValue = process.env[envKey] + if (!rawValue) { + return null + } + + try { + const parsedValue = JSON.parse(rawValue) + if ( + typeof parsedValue === 'object' && + parsedValue !== null && + !Array.isArray(parsedValue) + ) { + return parsedValue as Record + } + } catch { + return null + } + + return null +} + +export function getProviderIdentityAndTC(): { + license: Record | null + proofOfIdentity: Record | null +} { + return { + license: parseObj('LICENSE'), + proofOfIdentity: parseObj('PROOF_OF_IDENTITY') + } +} From d3badc85ff0b4300afe8b2474024d75e1a1129c9 Mon Sep 17 00:00:00 2001 From: AdriGeorge Date: Tue, 24 Feb 2026 09:51:23 +0200 Subject: [PATCH 2/6] chore: type owner info --- src/components/httpRoutes/rootEndpoint.ts | 17 +++--- src/components/httpRoutes/utils.ts | 67 +++++++++++++++++++---- 2 files changed, 63 insertions(+), 21 deletions(-) diff --git a/src/components/httpRoutes/rootEndpoint.ts b/src/components/httpRoutes/rootEndpoint.ts index 73bbc41b5..254bd654b 100644 --- a/src/components/httpRoutes/rootEndpoint.ts +++ b/src/components/httpRoutes/rootEndpoint.ts @@ -2,7 +2,7 @@ import express from 'express' import { HTTP_LOGGER } from '../../utils/logging/common.js' import { getConfiguration } from '../../utils/index.js' import { getAllServiceEndpoints } from './index.js' -import { getProviderIdentityAndTC } from './utils.js' +import { getNodeOwnerInfo } from './utils.js' export const rootEndpointRoutes = express.Router() rootEndpointRoutes.get('/', async (req, res) => { @@ -20,16 +20,13 @@ rootEndpointRoutes.get('/', async (req, res) => { version: '0.0.1' } - const { license, proofOfIdentity } = getProviderIdentityAndTC() - if (license) { - rootResponse.license = license + const ownerInfo = getNodeOwnerInfo() + if (ownerInfo) { + rootResponse.ownerInfo = ownerInfo } else { - HTTP_LOGGER.warn('LICENSE not present or invalid JSON object') - } - if (proofOfIdentity) { - rootResponse.proofOfIdentity = proofOfIdentity - } else { - HTTP_LOGGER.warn('PROOF_OF_IDENTITY not present or invalid JSON object') + HTTP_LOGGER.warn( + 'NODE_OWNER_INFO not present or invalid. Expected JSON object with optional NODE_IMPRINT, NODE_TC, NODE_PRIVACY_POLICY, NODE_PROOF_OF_IDENTITY entries.' + ) } res.json(rootResponse) diff --git a/src/components/httpRoutes/utils.ts b/src/components/httpRoutes/utils.ts index a90cf8d60..022dfd0d8 100644 --- a/src/components/httpRoutes/utils.ts +++ b/src/components/httpRoutes/utils.ts @@ -1,11 +1,32 @@ -function parseObj(envKey: string): Record | null { - const rawValue = process.env[envKey] - if (!rawValue) { +export type NodeInfoType = 'text' | 'url' + +export type NodeInfoEntry = { + type: NodeInfoType + value: string +} + +export type NodeOwnerInfo = { + NODE_IMPRINT?: NodeInfoEntry + NODE_TC?: NodeInfoEntry + NODE_PRIVACY_POLICY?: NodeInfoEntry + NODE_PROOF_OF_IDENTITY?: NodeInfoEntry +} + +const OWNER_INFO_ENV_KEY = 'NODE_OWNER_INFO' +const OWNER_INFO_KEYS: (keyof NodeOwnerInfo)[] = [ + 'NODE_IMPRINT', + 'NODE_TC', + 'NODE_PRIVACY_POLICY', + 'NODE_PROOF_OF_IDENTITY' +] + +function parseObj(envValue: string): Record | null { + if (!envValue) { return null } try { - const parsedValue = JSON.parse(rawValue) + const parsedValue = JSON.parse(envValue) if ( typeof parsedValue === 'object' && parsedValue !== null && @@ -20,12 +41,36 @@ function parseObj(envKey: string): Record | null { return null } -export function getProviderIdentityAndTC(): { - license: Record | null - proofOfIdentity: Record | null -} { - return { - license: parseObj('LICENSE'), - proofOfIdentity: parseObj('PROOF_OF_IDENTITY') +function isNodeInfoEntry(value: unknown): value is NodeInfoEntry { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return false } + + const candidate = value as Record + return ( + (candidate.type === 'text' || candidate.type === 'url') && + typeof candidate.value === 'string' + ) +} + +export function getNodeOwnerInfo(): NodeOwnerInfo | null { + const rawValue = process.env[OWNER_INFO_ENV_KEY] + if (!rawValue) { + return null + } + + const parsedOwnerInfo = parseObj(rawValue) + if (!parsedOwnerInfo) { + return null + } + + const result: NodeOwnerInfo = {} + OWNER_INFO_KEYS.forEach((key) => { + const value = parsedOwnerInfo[key] + if (isNodeInfoEntry(value)) { + result[key] = value + } + }) + + return Object.keys(result).length > 0 ? result : null } From bd5a1366962e4d1aa2bb4e67e399bc0d756d7a57 Mon Sep 17 00:00:00 2001 From: AdriGeorge Date: Tue, 24 Feb 2026 09:54:23 +0200 Subject: [PATCH 3/6] chore: env md --- docs/env.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/env.md b/docs/env.md index 2b74ad8c9..92eb68b98 100644 --- a/docs/env.md +++ b/docs/env.md @@ -35,6 +35,7 @@ Environmental variables are also tracked in `ENVIRONMENT_VARIABLES` within `src/ - `AUTHORIZED_PUBLISHERS_LIST`: AccessList contract addresses (per chain). If present, Node will only index assets published by the accounts present on the given access lists. Example: `"{ \"8996\": [\"0x967da4048cD07aB37855c090aAF366e4ce1b9F48\",\"0x388C818CA8B9251b393131C08a736A67ccB19297\"] }"` - `VALIDATE_UNSIGNED_DDO`: If set to `false`, the node will not validate unsigned DDOs and will request a signed message with the publisher address, nonce and signature. Default is `true`. Example: `false` - `JWT_SECRET`: Secret used to sign JWT tokens. Default is `ocean-node-secret`. Example: `"my-secret-jwt-token"` +- `NODE_OWNER_INFO`: Optional JSON object returned by the root endpoint as `ownerInfo`. Supported optional keys are `NODE_IMPRINT`, `NODE_TC`, `NODE_PRIVACY_POLICY`, `NODE_PROOF_OF_IDENTITY`, and each value must be an object with `type` (`text` or `url`) and `value` (string). Example: `"{\"NODE_IMPRINT\":{\"type\":\"text\",\"value\":\"Ocean Node Inc\"},\"NODE_TC\":{\"type\":\"url\",\"value\":\"https://example.com/terms\"},\"NODE_PRIVACY_POLICY\":{\"type\":\"url\",\"value\":\"https://example.com/privacy\"},\"NODE_PROOF_OF_IDENTITY\":{\"type\":\"url\",\"value\":\"https://example.com/proof.json\"}}"` ## Database From a98bce889033aa1f45d48f3cbc4437fbd75bc2da Mon Sep 17 00:00:00 2001 From: AdriGeorge Date: Tue, 24 Feb 2026 15:01:51 +0200 Subject: [PATCH 4/6] chore: structure --- docs/env.md | 2 +- src/components/httpRoutes/rootEndpoint.ts | 2 +- src/components/httpRoutes/utils.ts | 68 ++++++++++++++--------- 3 files changed, 44 insertions(+), 28 deletions(-) diff --git a/docs/env.md b/docs/env.md index 92eb68b98..a2b911b73 100644 --- a/docs/env.md +++ b/docs/env.md @@ -35,7 +35,7 @@ Environmental variables are also tracked in `ENVIRONMENT_VARIABLES` within `src/ - `AUTHORIZED_PUBLISHERS_LIST`: AccessList contract addresses (per chain). If present, Node will only index assets published by the accounts present on the given access lists. Example: `"{ \"8996\": [\"0x967da4048cD07aB37855c090aAF366e4ce1b9F48\",\"0x388C818CA8B9251b393131C08a736A67ccB19297\"] }"` - `VALIDATE_UNSIGNED_DDO`: If set to `false`, the node will not validate unsigned DDOs and will request a signed message with the publisher address, nonce and signature. Default is `true`. Example: `false` - `JWT_SECRET`: Secret used to sign JWT tokens. Default is `ocean-node-secret`. Example: `"my-secret-jwt-token"` -- `NODE_OWNER_INFO`: Optional JSON object returned by the root endpoint as `ownerInfo`. Supported optional keys are `NODE_IMPRINT`, `NODE_TC`, `NODE_PRIVACY_POLICY`, `NODE_PROOF_OF_IDENTITY`, and each value must be an object with `type` (`text` or `url`) and `value` (string). Example: `"{\"NODE_IMPRINT\":{\"type\":\"text\",\"value\":\"Ocean Node Inc\"},\"NODE_TC\":{\"type\":\"url\",\"value\":\"https://example.com/terms\"},\"NODE_PRIVACY_POLICY\":{\"type\":\"url\",\"value\":\"https://example.com/privacy\"},\"NODE_PROOF_OF_IDENTITY\":{\"type\":\"url\",\"value\":\"https://example.com/proof.json\"}}"` +- `NODE_OWNER_INFO`: Optional JSON object returned by the root endpoint as `ownerInfo`. Supported optional keys are `imprint`, `termsAndConditions`, `privacyPolicy`. `imprint` must include `legalName`, `email`, `url` and can include any number of additional custom keys. `termsAndConditions` and `privacyPolicy` must include `url` and can include any number of additional custom keys. Example: `"{\"imprint\":{\"legalName\":\"Example Ocean Services GmbH\",\"email\":\"contact@example.com\",\"url\":\"https://example.com/imprint\",\"address\":\"Example Street 1, 12345 Bucharest, Romania\",\"registryId\":\"HRB-12345\"},\"termsAndConditions\":{\"url\":\"https://example.com/terms\",\"version\":\"2026-01\"},\"privacyPolicy\":{\"url\":\"https://example.com/privacy\",\"lastUpdated\":\"2026-02-01\"}}"` ## Database diff --git a/src/components/httpRoutes/rootEndpoint.ts b/src/components/httpRoutes/rootEndpoint.ts index 254bd654b..2e94dba7f 100644 --- a/src/components/httpRoutes/rootEndpoint.ts +++ b/src/components/httpRoutes/rootEndpoint.ts @@ -25,7 +25,7 @@ rootEndpointRoutes.get('/', async (req, res) => { rootResponse.ownerInfo = ownerInfo } else { HTTP_LOGGER.warn( - 'NODE_OWNER_INFO not present or invalid. Expected JSON object with optional NODE_IMPRINT, NODE_TC, NODE_PRIVACY_POLICY, NODE_PROOF_OF_IDENTITY entries.' + 'NODE_OWNER_INFO not present or invalid. Expected JSON object with optional imprint, termsAndConditions, privacyPolicy entries.' ) } diff --git a/src/components/httpRoutes/utils.ts b/src/components/httpRoutes/utils.ts index 022dfd0d8..4b43fc16a 100644 --- a/src/components/httpRoutes/utils.ts +++ b/src/components/httpRoutes/utils.ts @@ -1,26 +1,21 @@ -export type NodeInfoType = 'text' | 'url' - -export type NodeInfoEntry = { - type: NodeInfoType - value: string +export type NodeOwnerInfoSection = { + url: string + [key: string]: any } export type NodeOwnerInfo = { - NODE_IMPRINT?: NodeInfoEntry - NODE_TC?: NodeInfoEntry - NODE_PRIVACY_POLICY?: NodeInfoEntry - NODE_PROOF_OF_IDENTITY?: NodeInfoEntry + imprint?: NodeOwnerInfoSection & { + legalName: string + email: string + } + termsAndConditions?: NodeOwnerInfoSection + privacyPolicy?: NodeOwnerInfoSection + [key: string]: NodeOwnerInfoSection | undefined } const OWNER_INFO_ENV_KEY = 'NODE_OWNER_INFO' -const OWNER_INFO_KEYS: (keyof NodeOwnerInfo)[] = [ - 'NODE_IMPRINT', - 'NODE_TC', - 'NODE_PRIVACY_POLICY', - 'NODE_PROOF_OF_IDENTITY' -] -function parseObj(envValue: string): Record | null { +function parseObj(envValue: string): Record | null { if (!envValue) { return null } @@ -32,7 +27,7 @@ function parseObj(envValue: string): Record | null { parsedValue !== null && !Array.isArray(parsedValue) ) { - return parsedValue as Record + return parsedValue as Record } } catch { return null @@ -41,18 +36,28 @@ function parseObj(envValue: string): Record | null { return null } -function isNodeInfoEntry(value: unknown): value is NodeInfoEntry { - if (!value || typeof value !== 'object' || Array.isArray(value)) { +function isObjectRecord(value: any): value is Record { + return !!value && typeof value === 'object' && !Array.isArray(value) +} + +function isImprint(value: any): value is NonNullable { + if (!isObjectRecord(value)) { return false } - - const candidate = value as Record return ( - (candidate.type === 'text' || candidate.type === 'url') && - typeof candidate.value === 'string' + typeof value.legalName === 'string' && + typeof value.email === 'string' && + typeof value.url === 'string' ) } +function isUrlContainer(value: any): value is NodeOwnerInfoSection { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return false + } + return typeof (value as Record).url === 'string' +} + export function getNodeOwnerInfo(): NodeOwnerInfo | null { const rawValue = process.env[OWNER_INFO_ENV_KEY] if (!rawValue) { @@ -65,9 +70,20 @@ export function getNodeOwnerInfo(): NodeOwnerInfo | null { } const result: NodeOwnerInfo = {} - OWNER_INFO_KEYS.forEach((key) => { - const value = parsedOwnerInfo[key] - if (isNodeInfoEntry(value)) { + if (isImprint(parsedOwnerInfo.imprint)) { + result.imprint = parsedOwnerInfo.imprint + } + if (isUrlContainer(parsedOwnerInfo.termsAndConditions)) { + result.termsAndConditions = parsedOwnerInfo.termsAndConditions + } + if (isUrlContainer(parsedOwnerInfo.privacyPolicy)) { + result.privacyPolicy = parsedOwnerInfo.privacyPolicy + } + Object.entries(parsedOwnerInfo).forEach(([key, value]) => { + if ( + !['imprint', 'termsAndConditions', 'privacyPolicy'].includes(key) && + isUrlContainer(value) + ) { result[key] = value } }) From 964cb5b76df2d9e35cf3aaf7880cbe4ec331b2cd Mon Sep 17 00:00:00 2001 From: AdriGeorge Date: Tue, 24 Feb 2026 15:50:42 +0200 Subject: [PATCH 5/6] fix: env type --- docs/env.md | 2 +- src/components/httpRoutes/utils.ts | 70 +----------------------------- 2 files changed, 3 insertions(+), 69 deletions(-) diff --git a/docs/env.md b/docs/env.md index a2b911b73..be728fb95 100644 --- a/docs/env.md +++ b/docs/env.md @@ -35,7 +35,7 @@ Environmental variables are also tracked in `ENVIRONMENT_VARIABLES` within `src/ - `AUTHORIZED_PUBLISHERS_LIST`: AccessList contract addresses (per chain). If present, Node will only index assets published by the accounts present on the given access lists. Example: `"{ \"8996\": [\"0x967da4048cD07aB37855c090aAF366e4ce1b9F48\",\"0x388C818CA8B9251b393131C08a736A67ccB19297\"] }"` - `VALIDATE_UNSIGNED_DDO`: If set to `false`, the node will not validate unsigned DDOs and will request a signed message with the publisher address, nonce and signature. Default is `true`. Example: `false` - `JWT_SECRET`: Secret used to sign JWT tokens. Default is `ocean-node-secret`. Example: `"my-secret-jwt-token"` -- `NODE_OWNER_INFO`: Optional JSON object returned by the root endpoint as `ownerInfo`. Supported optional keys are `imprint`, `termsAndConditions`, `privacyPolicy`. `imprint` must include `legalName`, `email`, `url` and can include any number of additional custom keys. `termsAndConditions` and `privacyPolicy` must include `url` and can include any number of additional custom keys. Example: `"{\"imprint\":{\"legalName\":\"Example Ocean Services GmbH\",\"email\":\"contact@example.com\",\"url\":\"https://example.com/imprint\",\"address\":\"Example Street 1, 12345 Bucharest, Romania\",\"registryId\":\"HRB-12345\"},\"termsAndConditions\":{\"url\":\"https://example.com/terms\",\"version\":\"2026-01\"},\"privacyPolicy\":{\"url\":\"https://example.com/privacy\",\"lastUpdated\":\"2026-02-01\"}}"` +- `NODE_OWNER_INFO`: Optional JSON object returned by the root endpoint as `ownerInfo`. Example: `"{\"imprint\":{\"legalName\":\"Example Ocean Services GmbH\"},\"termsAndConditions\":{\"url\":\"https://example.com/terms\"},\"anyCustomSection\":{\"foo\":\"bar\"}}"` ## Database diff --git a/src/components/httpRoutes/utils.ts b/src/components/httpRoutes/utils.ts index 4b43fc16a..80ea9ebcf 100644 --- a/src/components/httpRoutes/utils.ts +++ b/src/components/httpRoutes/utils.ts @@ -1,18 +1,3 @@ -export type NodeOwnerInfoSection = { - url: string - [key: string]: any -} - -export type NodeOwnerInfo = { - imprint?: NodeOwnerInfoSection & { - legalName: string - email: string - } - termsAndConditions?: NodeOwnerInfoSection - privacyPolicy?: NodeOwnerInfoSection - [key: string]: NodeOwnerInfoSection | undefined -} - const OWNER_INFO_ENV_KEY = 'NODE_OWNER_INFO' function parseObj(envValue: string): Record | null { @@ -36,57 +21,6 @@ function parseObj(envValue: string): Record | null { return null } -function isObjectRecord(value: any): value is Record { - return !!value && typeof value === 'object' && !Array.isArray(value) -} - -function isImprint(value: any): value is NonNullable { - if (!isObjectRecord(value)) { - return false - } - return ( - typeof value.legalName === 'string' && - typeof value.email === 'string' && - typeof value.url === 'string' - ) -} - -function isUrlContainer(value: any): value is NodeOwnerInfoSection { - if (!value || typeof value !== 'object' || Array.isArray(value)) { - return false - } - return typeof (value as Record).url === 'string' -} - -export function getNodeOwnerInfo(): NodeOwnerInfo | null { - const rawValue = process.env[OWNER_INFO_ENV_KEY] - if (!rawValue) { - return null - } - - const parsedOwnerInfo = parseObj(rawValue) - if (!parsedOwnerInfo) { - return null - } - - const result: NodeOwnerInfo = {} - if (isImprint(parsedOwnerInfo.imprint)) { - result.imprint = parsedOwnerInfo.imprint - } - if (isUrlContainer(parsedOwnerInfo.termsAndConditions)) { - result.termsAndConditions = parsedOwnerInfo.termsAndConditions - } - if (isUrlContainer(parsedOwnerInfo.privacyPolicy)) { - result.privacyPolicy = parsedOwnerInfo.privacyPolicy - } - Object.entries(parsedOwnerInfo).forEach(([key, value]) => { - if ( - !['imprint', 'termsAndConditions', 'privacyPolicy'].includes(key) && - isUrlContainer(value) - ) { - result[key] = value - } - }) - - return Object.keys(result).length > 0 ? result : null +export function getNodeOwnerInfo(): Record | null { + return parseObj(process.env[OWNER_INFO_ENV_KEY]) } From 770ad9382ef621af57c34df3041193a0f4108936 Mon Sep 17 00:00:00 2001 From: AdriGeorge Date: Tue, 24 Feb 2026 15:52:54 +0200 Subject: [PATCH 6/6] fix: log --- src/components/httpRoutes/rootEndpoint.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/httpRoutes/rootEndpoint.ts b/src/components/httpRoutes/rootEndpoint.ts index 2e94dba7f..c67a2f6a3 100644 --- a/src/components/httpRoutes/rootEndpoint.ts +++ b/src/components/httpRoutes/rootEndpoint.ts @@ -24,9 +24,7 @@ rootEndpointRoutes.get('/', async (req, res) => { if (ownerInfo) { rootResponse.ownerInfo = ownerInfo } else { - HTTP_LOGGER.warn( - 'NODE_OWNER_INFO not present or invalid. Expected JSON object with optional imprint, termsAndConditions, privacyPolicy entries.' - ) + HTTP_LOGGER.warn('NODE_OWNER_INFO not present or invalid') } res.json(rootResponse)