# n8n-nodes-ndc

Private NODE.DC node package for provider-neutral L2 workflows. It extends n8n
through the supported private-node mechanism and does not patch Engine or n8n
core.

Every custom node has an `NDC ` display-name prefix. Runtime types are package
qualified:

- `n8n-nodes-ndc.ndcDataProductPublish` — publishes canonical facts into an
  approved Data Product.
- `n8n-nodes-ndc.ndcDataProductRead` — reads the current snapshot or a bounded,
  cursor-paged history window of an approved Data Product.
- `n8n-nodes-ndc.ndcFoundryBinding` — creates or updates a declarative Foundry
  page-slot binding. This is a control-plane operation, not a runtime data
  transport.

## Security and scope

Workflow parameters contain no service URL, provider, tenant, connection, raw
secret, or credential selector. Product selection is loaded from the catalog
visible to the selected opaque capability. Service locations are operator-owned
runtime overrides (the package defaults to the canonical internal service
names, so the first deployment needs no extra Engine environment):

- `NDC_DATA_PLANE_BASE_URL`
- `NDC_FOUNDRY_BASE_URL`

The defaults are `http://external-data-plane:18106` and
`http://nodedc-module-foundry:3333`. They are provider-neutral Platform service
addresses, not workflow configuration.

The writer, reader, and Foundry credential types contain only one
password-protected opaque capability and are intentionally distinct. The
separate `NDC Provider Rotating Access API` credential keeps the Gelios REST access
and refresh pair inside native Engine Credentials. It exchanges the refresh
token only against the fixed Gelios refresh endpoint, persists both rotated
tokens through the supported expirable-credential lifecycle, and injects only
the current access token into provider requests. Concurrent refresh attempts in
the single-service L2 runtime are coalesced and briefly replay the same rotated
pair so a stale caller cannot immediately spend the invalidated predecessor
refresh token again.
Provider identity, product version, ontology revision, persistence policy, and
tenant scope are materialized by the receiving service from the grant. The node
never accepts them from a workflow.

## Fixed routes

- `GET /internal/data-plane/v1/writer/data-products`
- `GET /internal/data-plane/v1/reader/data-products`
- `POST /internal/data-plane/v1/data-products/:dataProductId/publish`
- `GET /internal/data-plane/v1/data-products/:dataProductId/snapshot`
- `GET /internal/data-plane/v1/data-products/:dataProductId/history`
- `GET /internal/foundry/v1/data-products`
- `POST /internal/foundry/v1/data-product-bindings`

There is no fallback to a legacy intake route. `NDC Foundry Binding` uses the
dedicated internal control-plane endpoint implemented by Foundry and fails
closed when that route or its scoped workload grant is unavailable. Its
canonical Map entity-stream slot defaults to `points`.

The binding command uses the versioned
`nodedc.foundry.binding-upsert/v1` schema. Its opaque `ndc_fndbg_*` workload
grant is separate from the short-lived Foundry MCP capability used by AI
Workspace and from the shared Platform service token. The receiving service
materializes actor, owner and exact application/page/binding/product scope from
that grant; none of those authorization claims are accepted from headers or
workflow data.

`NDC Data Product Read` supports the bounded
`nodedc.data-product.snapshot/v1` contract. Its snapshot `Page Size` parameter is a
safety ceiling, not pagination: the complete scoped current projection must fit
within 5000 entity keys. A larger product returns
`data_product_snapshot_limit_exceeded`; the node must not assemble independent
pages or start a patch stream from an incomplete snapshot. Large products need
stable partition Data Products.

History mode uses `nodedc.data-product.history/v1` and requires a closed
`from`/`to` interval. It supports provider-neutral `sourceIds`, a resolution
that is a multiple of the Data Product native sampling interval, a limit up to
5000 points and an opaque keyset continuation cursor. The response stays as one
envelope item so L2 logic can preserve `query` and `nextCursor` while iterating.
The workflow never selects a database, provider, tenant or connection.

## Publish input

Each incoming item must be either a canonical fact or `{ "fact": <fact> }`:

```json
{
  "sourceId": "fleet.unit.42",
  "semanticType": "map.moving_object",
  "observedAt": "2026-07-15T12:00:00.000Z",
  "attributes": {},
  "geometry": { "type": "Point", "coordinates": [37.61, 55.75] }
}
```

The node emits `nodedc.data-product.publish/v1`, enforces the 5000-fact and
64-KiB-per-attributes limits, rejects secret-like material in attributes, and
derives upsert run/idempotency identifiers from workflow execution context.
Complete-snapshot replacement instead derives one canonical identity from the
product, generation timestamp, sequence and normalized fact set. Replaying the
same immutable generation therefore reaches the Data Plane as the same request,
while changed content under an already-used generation remains a conflict.

## Build and verification

```sh
npm install
npm test
npm run lint
```

The package must be installed as an n8n private community package under the
runtime user's community-package root:

```text
/home/node/.n8n/nodes/node_modules/n8n-nodes-ndc
```

The parent `/home/node/.n8n/nodes/package.json` owns the pinned package
registration. `~/.n8n/custom` and `N8N_CUSTOM_EXTENSIONS` are deliberately not
used: n8n loads those through the `CUSTOM` namespace, which would destroy the
required `n8n-nodes-ndc.*` runtime types.

## Deployment boundary

The Platform MCP catalog bridge preserves package-qualified custom runtime
types, but Engine can expose these schemas only after this private package is
installed through the community-package path and the bridge slice is deployed.
Production installation uses a verified offline tarball, an immutable
root-owned release, an atomic current/previous switch and a read-only mount; it
does not run `npm install` inside a live container. This package deliberately
does not patch Engine or n8n core.

The Platform-side release builder is:

```sh
node ../../infra/deploy-runner/build-n8n-private-extension-artifact.mjs \
  n8n-nodes-ndc-release-YYYYMMDD-NNN
```

Its canonical deploy component only stages and seals a digest-bound release
under `/volume1/docker/nodedc-platform/n8n-private-extensions`; it does not
activate anything in Engine. Activation is intentionally blocked until an
Engine-owned deployment slice provides the exact read-only community-package
mount, atomic release selection, all-process restart and MCP schema acceptance.
The v2 rollback contract accepts a pre-activation, explicitly verified inactive
baseline for the first activation; subsequent upgrades prefer a previous
verified immutable release. The staged v1 `0.1.0` release is never overwritten:
this contract is published as a separate `0.1.1` release.
