NODEDC_PLATFORM/packages/external-provider-contract/README.md

443 lines
32 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# External Provider Contract
Этот package — единственное общее место для формы внешних интеграций NODE.DC.
Он не содержит provider secrets, customer records, runtime payloads или
исполняемый connector code. `src/index.mjs` даёт dependency-free проверку
минимальных v1 contracts; она запускается через `npm run check`.
Каждый NDC L2 connector instance должен поставлять совместимые versioned
артефакты:
```text
provider-manifest
connection-profile
capability-catalog
field-catalog
collection-profile
retention-profile
semantic-mapping-contract
read-model-contract
command-catalog (metadata only until a red command gateway is approved)
```
`provider-manifest` — декларативный template-артефакт: provider ID, ontology
revision, NDC L2 template version, capability catalog и data-product IDs. Он не
является tenant connection, не содержит endpoint/URL, credential reference,
secret, ручной scope или executable provider code. Concrete non-secret
connection profile принадлежит и версионируется вместе с конкретным NDC L2
workflow; caller привязывает только opaque provider credential reference, а
внутренний workload grant разрешает control plane.
## Versioned provider packages
`providers/<provider>/<major>` — устанавливаемая contract/data единица, а не
runtime service и не custom node. Она объединяет manifest, auth-mode metadata,
capability catalog, field policies, collection profiles, Data Product
definitions, semantic mapping contracts, provider-neutral NDC L2 template
descriptor и synthetic fixtures. Package не содержит tenant/account state,
credential values или исполняемый provider daemon.
`instantiateL2Connection` создаёт произвольное число connection instances из
одного immutable package. Каждый instance получает собственные tenant,
connection и opaque provider credential reference, но не требует изменения
Platform source. Внутренние workload bindings объявляются отдельно как
`control_plane_managed` и не являются caller input. Канонический collection
scope — `all_visible_to_credential`: выбранная
read-capability передаёт все entities, которые provider возвращает для
привязанного credential. Unit allowlist и provider-group filter на collection
boundary запрещены; сортировка и видимость принадлежат Data Product consumer и
Foundry.
`compileL2ExecutionPlan` превращает package + connection instance в
детерминированный `nodedc.l2-execution-plan/v1`: разрешает generic шаги
collection/request/extract/mapping/publish, прикладывает полные декларативные
request/mapping contracts и фиксирует SHA-256 digests package, profile,
template, mapping, field policy, Data Product и telemetry registry. Plan
по умолчанию компилируется версией `1.2.0`; immutable plans `1.1.0` остаются
валидируемыми для воспроизводимости уже материализованных legacy runtime.
Версия компилятора выбирает только provider-neutral runtime semantics и не
добавляет ветвление по provider ID.
Plan
содержит exact `nodedc.l2-graph-blueprint/v1`: линейный набор generic runtime
node kinds, exact edges, config digests, credential slots, trigger/retry/batch/
cardinality policy и закрытую матрицу требуемых mapping operators. Blueprint
не содержит credential reference и фиксируется отдельным SHA-256 digest.
Runtime обязан fail-closed отклонить неизвестный operator, rule или geometry
strategy; молча подставлять provider-specific code запрещено. В компиляторе
нет веток по provider ID. Provider-specific URL, source paths и параметры
являются данными immutable package. После материализации Engine
`attestL2ExecutionPlanMaterialization` связывает exact plan/blueprint digest с
exact graph revision/digest и точным списком materialized steps.
Динамическая телеметрия проходит отдельный
`nodedc.telemetry-field-registry/v1`. Реестр хранит только имена source
parameters, типы, статус классификации, sensitivity и разрешённые surfaces —
никогда значения. Только `approved + operational` entries могут войти в
Data Product/analytics/Foundry projection. `observed`, PII/identifier,
command и restricted entries остаются audit-only; wildcard source key
запрещён. Поэтому появление нового параметра у provider не меняет ядро и не
открывает его автоматически потребителям.
Текущий production-shaped package — `providers/gelios/v5`. Он сохраняет два
официальных Gelios REST safe-read: `GET /api/v1/users/me/monitoring-config` и
`GET /api/v1/units?incltrip=true`, а также точный immutable output
`fleet.positions.current.v4@4.0.0` с revision
`ontology.map.moving_object.v3`. Единственные monitoring-state fields —
`signal_state` (`active|inactive`) и `movement_state` (`moving|stopped`),
полученные из Ontology Gelios `v1.1.0`. Все Data Product fields используют
snake_case. `fieldContracts` фиксирует тип и обязательность каждого поля, а
закрытые state values проверяются и в provider mapping, и на каждом publish в
External Data Plane. Native rotating credential использует `access`, а
`refresh` остаётся внутри NDC L2 Credentials. Предыдущие Data Product versions
не переписываются и остаются legacy-compatible.
## Проверяемые v1 contracts
- `Connection` — provider instance, tenant scope и *ссылка* на provider
credential в NDC L2 Credentials. Любые token/secret/password-like поля
запрещены; publisher представлен system-managed declaration/status без ref.
- `Collection Profile` — явная policy сбора. `manual` не может скрыто содержать
polling interval; `realtime` требует interval не чаще одного раза в секунду.
- `Data Product` — нормализованный versioned output с semantic types, полями,
optional `fieldContracts` и внутренней аудиторией. Если `fieldContracts`
задан, он обязан покрывать точный набор fields; каждый contract задаёт
`type`, `required`, optional closed `enum` и числовые bounds. Отсутствие
contracts допустимо только для опубликованных legacy versions.
- `Intake Batch` — canonical **scoped** record, который External Data Plane
валидирует и сохраняет: source, contract revision, idempotency, restricted
raw envelope и canonical facts. Его `source` содержит `providerId`,
`tenantId` и `connectionId`. Writer-bound NDC L2 request намеренно не является
готовым `Intake Batch`: Data Plane сначала materializes scope и лишь затем
применяет этот contract. В canonical record нет provider field mapping,
entity allowlist, token или renderer data. Inline `raw.payload` в v1
запрещён: если нужна provenance-ссылка, connector передаёт restricted
`raw.ref` вместе с hash. Отдельный raw-vault может быть добавлен только
отдельным ADR и не становится частью NDC L2 → Data Plane wire boundary.
- `NDC Foundry Binding` — адресует data product только в конкретную цепочку
`Foundry Application → Page → approved slot`; `templateId` можно сохранить
как дополнительную типизацию, но он не заменяет `applicationId` и `pageId`.
В binding запрещены provider transport, endpoint и credential reference.
- `Provider Manifest` — статическое описание NDC L2 connector template, capability
catalog и ontology/data-product contracts. Оно не может содержать tenant,
connection, credential, secret или provider transport.
## Data Product delivery contracts
Provider-neutral runtime использует отдельные wire schemas:
- `nodedc.data-product.publish/v1` — unscoped publish request от
`NDC Data Product Publish`; содержит только batch identity и canonical facts;
- `nodedc.data-product.snapshot/v1` — согласованный current snapshot с cursor;
- `nodedc.data-product.history/v1` — bounded Timescale history window с
`from/to`, provider-neutral `sourceIds`, resolution и opaque keyset cursor;
- `nodedc.data-product.patch/v1` — committed upsert operations из durable
outbox с `previousCursor`/`cursor`.
Publish request не может задавать provider, tenant, connection, endpoint,
receivedAt, ontology revision, version или persistence policy. Data Plane
materializes эти значения из opaque writer binding и зарегистрированного Data
Product definition. Snapshot/stream читаются только по отдельному opaque reader
binding; shared internal bearer и caller-provided scope headers являются legacy
и не используются новыми nodes/Foundry runtime.
`snapshot+patch` v1 — bounded contract: один scoped Data Product содержит не
более 5000 current entity keys `(sourceId, semanticType)`. Snapshot обязан быть
полным и привязанным к одному repeatable-read cursor. Параметр `limit` задаёт
защитный ceiling, не page size: превышение возвращает
`413 data_product_snapshot_limit_exceeded`, а reader не имеет права начинать
stream с усечённой базой. `nextPageCursor` зарезервирован для будущего отдельно
версионируемого query contract и текущим bounded runtime не выдаётся.
Для большей current cardinality definition заранее раскладывается по стабильным
partition Data Products с независимыми snapshot/patch cursors. History является
отдельным immutable-window query: cursor привязан digest-ом к exact
`from/to/resolution/sourceIds`, поэтому его нельзя переиспользовать с другим
запросом; offset pagination запрещена.
Offset/source-ID pagination поверх меняющегося current snapshot запрещена:
между страницами она способна потерять или задублировать изменения относительно
patch cursor.
Для каждого canonical fact действует одинаковый hard ceiling: сериализованный
`attributes` не больше 64 KiB как на publish/intake входе, так и в
snapshot/patch выходе. GeoJSON `Point` принимает longitude только в
`[-180, 180]`, latitude в `[-90, 90]`; batch sequence ограничен диапазоном
PostgreSQL `integer` `0..2147483647`. Эти ограничения нельзя ослабить опциями
конкретного caller-а.
Manifest, connection/collection profiles, data-product definition и Foundry
binding используют fail-closed allowed-key schemas. Неизвестные поля, а также
secret-like имена или значения (включая `ndc_edpwb_`, `ndc_edprb_` и
`ndc_edppr_`) отклоняются
на общей границе.
Все private custom nodes NODE.DC поставляются package
`platform/packages/n8n-nodes-ndc`. Их display name обязан начинаться с `NDC `,
а runtime type — с `n8n-nodes-ndc.`; package называется строго
`n8n-nodes-ndc`. Provider-specific adapters остаются NDC L2 workflow logic и не
становятся custom nodes или ветками Data Plane. Эти инварианты проверяются
package test.
Control-plane команда `NDC Foundry Binding` имеет отдельную replay-safe schema
`nodedc.foundry.binding-upsert/v1` (`validateFoundryBindingUpsert`). Это не
declarative provider artifact и не runtime transport: команда содержит только
application/page/binding/data-product projection и idempotency key, а право на
операцию извлекается Foundry из отдельного opaque workload grant.
## Native NDC L2 Credentials
Provider auth и внутренние workload capabilities используют один общий
секретный boundary — native Credentials NDC L2, — но являются разными
credential domains. После сохранения ядро владеет secret, а graph и MCP
используют только opaque reference.
Gelios выдаёт ровно access token и refresh token. Production credential type
`ndcProviderRotatingAccessApi` использует access token для HTTP request и выполняет
refresh → access в native n8n `preAuthentication`; provider package v3 фиксирует
`refreshMode: runtime_managed`. Оба secret artifact остаются в native Credentials и не
попадают в graph, package или trace. Название credential с текстом вроде
`read access` является лишь локальной меткой; read-классификацию задают
разрешённые endpoint/method в capability catalog и workflow policy, а не scope
самого access token. В deployed Engine exact method/path policy ещё не
подключена: текущая HTTP safe-ref граница проверяет host. Поэтому catalog
classification остаётся декларативной до capability-bound Engine/MCP proof.
Для общего Data Product transport используются `ndcDataProductWriterApi`,
`ndcDataProductReaderApi` и `ndcFoundryBindingApi`. Это внутренние NDC
capabilities, не Gelios tokens и не часть Gelios token lifecycle. Их значения
не являются частью provider package и не передаются между workflow nodes как
данные.
Connection caller передаёт только provider credential reference. Publisher
role остаётся credential binding для `NDC Data Product Publish`, но template
маркирует его `management: control_plane_managed`: ни writer secret, ни writer
reference не входят в connection parameters. Instantiation возвращает
декларативный `systemBindings.publisher` с desired state и unresolved status;
дальше его разрешает trusted control plane.
Canonical managed EDP writer capability генерируется и сохраняется внутри
Engine credential boundary, а EDP получает только digest. Старые manual
writer/reader и Foundry issuance paths могут возвращать capability один раз
trusted control-plane caller и остаются отдельно закрытыми compatibility
границами; MCP и пользователь получают только opaque reference/status.
Запрещены password input
как пользовательский acceptance-путь, graph parameters, connection/profile
files, env, Ops, logs и traces.
Broad credential sink/resolver/daemon и provider-specific provisioning
запрещены. Требуется узкий generic `ensure data-product publish grant` contract
с server-derived scope, idempotency, CAS/crash recovery и audit. Текущий
deployed Engine MCP этой операции ещё не имеет; root/UI transfer допустим
только как emergency self-hosted diagnostics, не как user journey.
## NDC L2 private-extension management
`src/engine-private-extension.mjs` задаёт строгую Platform-side границу для
NDC L2-owned активации проверенного `n8n-nodes-ndc` release. Контракт не
устанавливает package и не меняет NDC L2: он фиксирует async
`plan -> apply receipt -> operation/status` protocol, где `apply` обязан
быстро вернуть `queued` и `operationId`, а долгий recreate/acceptance
отслеживается отдельно.
Активация и rollback требуют отдельной глобальной capability
`engine.private-extension.manage`. Обычные NDC L1/NDC L2 grants её не дают. Чтение
состояния допускает `engine.private-extension.read` или manage-capability.
Запрос выбирает только allowlisted package, digest-bound `releaseId` и
`packageSha256`; caller не передаёт host path, package bytes, Compose service,
shell command или credential material. Plan живёт не более 15 минут, является
single-use и применяется только с тем же idempotency key и plan hash.
Runtime transition использует platform-managed community-package loader для
immutable `n8n-nodes-ndc` release; private runtime paths и environment settings
не являются частью публичного provider contract:
- Deploy/Run quiesced and execution queue drained before the version switch;
- read-only mount and atomic current/recovery state;
- force-recreate main, every worker and every webhook instance as one version
barrier; hot reload запрещён;
- acceptance требует единый generation и точный набор трёх package-qualified
node schemas и трёх credential schemas.
Любая ошибка после switch запускает automatic rollback и повторный
force-recreate/acceptance. Ошибка самого rollback переводит runtime в
`quarantined`. Immutable release и существующие NDC L2 Credentials
сохраняются. Для первой активации предыдущим проверенным состоянием является
`n8n-nodes-ndc.inactive/v1`: rollback в этот baseline удаляет package из
loader surface, но не удаляет credentials.
Public Ops gateway уже умеет прозрачно передавать эти операции через
`/engine/mcp`, если NDC L2 реализует соответствующие MCP tools. Так как
gateway имеет 30-second upstream timeout, side effect остаётся асинхронным;
отдельный public REST proxy для management boundary не требуется.
`providers/gelios/v1/fixtures` содержит synthetic provider response и ожидаемый
publish contract без customer, tenant identity или credential material.
## Ownership
- `platform/packages/external-provider-contract` — общий контракт и schemas.
- NDC L2 — provider API adapter: fetch, pagination, batching,
semantic mapping, collection profile и ссылка на credential в NDC L2
Credentials.
- Platform External Data Plane — provider-neutral intake, raw retention,
canonical facts, current/history projections и scoped read products. Он не
знает provider fields, customer/business filters или renderer rules.
- `platform/services/ontology-core/catalog/domain-packages/<provider>`
семантика, отношения и guardrails, но не runtime data.
- Foundry/interface bindings — consumers scoped data product; они не получают
provider transport, endpoint или credential reference.
`services/<provider>-gateway` — устаревший experimental path и не является
шаблоном новых integration services. Канон описан в
`docs/ADR_L2_OWNED_EXTERNAL_CONNECTORS.md`.
## Mandatory connection boundary
`connection` принадлежит одному tenant/client context и содержит только
ссылку на provider secret, утверждённый capability scope, collection profile,
field policy и retention policy. Во всех runtime records Data Plane сохраняет минимум
`tenant_id`, `connection_id`, `provider_id`, `observed_at`, `received_at` и
provenance/version там, где это применимо.
Provider credential reference текущего Gelios request адресует access token;
наличие provider-issued refresh token описывается `tokenLifecycle`, но его value
не входит в `Connection`. Writer capability — отдельный внутренний EDP runtime
credential, а не Gelios token и не caller-supplied поле `Connection`, profile,
provider manifest или NDC L2 graph. Connection artifact содержит только
system-managed publisher declaration/status без credential reference.
## Scoped writer binding
Writer binding — EDP-owned runtime security state, а не versioned artifact
provider manifest или connection profile. Он фиксирует `tenantId`,
`connectionId`, `providerId`, `allowedDataProductIds`, active/revoked state и
`expiresAt`. NDC L2 не может редактировать binding или задавать его scope;
изменение любого scope-поля либо TTL создаёт новый binding, а прежний binding
можно только rotate/revoke.
Binding создаёт, сохраняет и привязывает только trusted control plane. Caller
connection instance не принимает `writerCredentialRef`; publisher role в L2
template является обязательным `control_plane_managed` binding requirement.
Новый NDC L2 вызывает
`POST /internal/data-plane/v1/data-products/:dataProductId/publish` с opaque
writer capability и envelope `nodedc.data-product.publish/v1`. В body есть
только batch identity и canonical facts; `source`, provider, tenant,
connection, ontology revision, product version и persistence policy запрещены.
EDP проверяет binding и path `dataProductId`, materializes immutable scope и
contract из server-owned registries, затем сохраняет canonical batch.
Caller-provided scope отклоняется, а не доверяется и не объединяется с
binding.
Plaintext writer capability запрещена в provider package, connection/profile,
NDC L2 graph, files, environment, logs/traces, raw payload, Foundry и MCP.
В canonical пути она генерируется и сохраняется внутри native Engine credential
boundary; EDP получает только SHA-256 digest, а workflow — только opaque
reference/status. Старые ручные EDP create/rotate endpoints с one-time
plaintext response остаются отдельно выключенным compatibility-механизмом и не
являются пользовательским или acceptance-путём.
Legacy manual one-time issuance использует отдельный EDP-only runner-managed
bearer. Digest-only managed ensure этот bearer не принимает: Engine подписывает
каждый control-plane запрос отдельным Ed25519 service key, EDP хранит только
публичный ключ. Оба пути имеют независимые флаги и по умолчанию выключены.
Generic `ensure data-product publish grant` генерирует и сохраняет capability в
native Engine credential boundary, передаёт EDP её digest и возвращает только
opaque ref/status. Shared internal bearer и Gelios access/refresh token для
этого запрещены. Deployed Engine MCP умеет читать
opaque refs и bind только HTTP Request credentials; native
`ndcDataProductWriterApi` к custom Publish node этим policy не привязывается.
Поэтому ensure + exact private-node bind остаются реальным platform gap.
Root/UI перенос — только аварийная диагностика.
Control-plane EDP boundary — idempotent
`PUT /internal/data-plane/v1/writer-bindings/by-key/:bindingKey`. Engine создаёт
capability внутри native credential boundary и отправляет только её SHA-256
`capabilityDigest`, immutable scope, `generation` и expiry. Одинаковый
`bindingKey + generation + request hash` возвращает тот же binding без нового
secret; несовпадающий retry получает `409`. Rotation использует новую
generation/credential, поэтому старый binding можно оставить активным до
успешного node bind и затем явно revoke.
Managed revoke выполняется идемпотентно по точным `bindingKey + generation`
через managed-only route; для него не открывается legacy plaintext API.
Managed route включается только через
`EXTERNAL_DATA_PLANE_MANAGED_PROVISIONING_ENABLED`; legacy plaintext routes —
через отдельный `EXTERNAL_DATA_PLANE_PROVISIONING_ENABLED`.
Managed reader хранит два разных provider-neutral scope. `connectionId` в
reader grant идентифицирует consumer connection, а `sourceConnectionId`
фиксирует writer connection, из которого разрешено читать Data Product.
`sourceConnectionId` не приходит из L2 graph: при ensure EDP разрешает его по
активным writer bindings того же `tenantId`, `providerId` и полного набора
`allowedDataProductIds`. Точное совпадение connection выбирается напрямую;
иначе допускается только один кандидат. Ноль кандидатов возвращает
`managed_reader_source_scope_not_found`, несколько —
`managed_reader_source_scope_ambiguous`. Поэтому reader из отдельного L2 target
не попадает в пустой consumer scope, а несколько provider accounts никогда не
смешиваются эвристически. Legacy reader binding сохраняет прежнюю семантику:
его `connectionId` одновременно является source connection.
Managed auth wire contract —
`nodedc.external-data-plane.managed-provisioner-request/v1`. Engine отправляет
ровно по одному header:
`x-nodedc-engine-service-id`, `x-nodedc-engine-key-id`,
`x-nodedc-request-audience`, `x-nodedc-request-timestamp`,
`x-nodedc-request-nonce`, `x-nodedc-content-sha256` и
`x-nodedc-request-signature`. Timestamp — canonical UTC ISO с миллисекундами,
nonce и signature — unpadded canonical base64url, body hash — lowercase hex
SHA-256 от точных переданных bytes. Подписываются UTF-8 bytes результата
`JSON.stringify` объекта с полями строго в порядке `schemaVersion`, `audience`,
`serviceId`, `keyId`, `method`, `path`, `timestamp`, `nonce`, `bodySha256`.
`method` и request target `path` (включая query) должны совпадать побайтно.
Canonical identity: service `nodedc-engine`, key
`engine-edp-managed-provisioner-v1`, audience
`nodedc-external-data-plane.managed-provisioning.v1`. EDP проверяет bounded
clock skew и одноразовый nonce в bounded fail-closed replay cache; bearer
fallback для managed routes отсутствует, любой `Authorization` header на них
отклоняется.
EDP импортирует только `@nodedc/external-provider-contract/data-plane`.
Artifact/image allowlist для этого subpath содержит contract version, intake,
publish/snapshot/patch validators и scrub policy; provider packages, mappings,
fixtures и tests физически не входят в EDP runtime и не являются причиной его
restart.
## Legacy intake migration
`POST /internal/data-plane/v1/intake` — временный compatibility route для
existing writers. Он принимает только canonical scoped `Intake Batch` под
`NODEDC_INTERNAL_ACCESS_TOKEN` и требует совпадения body scope с
`x-nodedc-tenant-id` и `x-nodedc-connection-id`. Writer token этот route не
заменяет.
Для миграции: Platform обеспечивает scoped writer binding и native credential,
затем через Engine MCP новый workflow branch переключается на Data Product
publish route, удаляет tenant/connection из body и headers и проверяет
успешный publish. Существующий legacy workflow/credential остаётся неизменным,
пока владелец отдельно не решит его вывести; новый writer не должен
fallback-иться на legacy route после ошибки publish.
## Intake boundary
NDC L2 отправляет в общий Data Plane publish request, а не SQL-запрос в общие
таблицы. Platform проверяет boundary и сохраняет raw/history/current
projections; semantic mapping остаётся в NDC L2. Список конкретных source IDs не
является частью connection scope: scope выбирает read-capability API, а
visibility решает data-product consumer.
Inline `raw.payload` запрещён: NDC L2 не может записать в Data Plane полный ответ
provider-а или произвольную строку. Пока отдельный raw-vault не утверждён,
batch либо не содержит `raw`, либо содержит только restricted `raw.ref` и hash.
Secret-like keys и распознаваемые bearer/JWT/writer-token values в любом участке
canonical batch, включая `facts[].attributes`, отклоняются. Retention raw
reference вычисляется от server acceptance time, а не от `batch.receivedAt`;
service делает sweep expired envelopes при старте и по расписанию.
## Safety classification
Capabilities классифицируются как `read`, `metadata`, `write`, `destructive`
или `unknown`. Только `read` и согласованные `metadata` могут попасть в
collector. `write` и `destructive` остаются каталогизированными, но не имеют
transport route в read adapter. Эта классификация относится к разрешённому
method/endpoint workflow, а не утверждает, что provider access token имеет
read-only scope.