392 lines
27 KiB
Markdown
392 lines
27 KiB
Markdown
# 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.
|
||
|
||
Первый production-shaped package — `providers/gelios/v1`. Он фиксирует реальный
|
||
Gelios REST `GET /api/v1/units` transport contract и точный output
|
||
`fleet.positions.current.v1@1.0.0` с revision
|
||
`ontology.map.moving_object.v1`. Все Data Product fields используют snake_case.
|
||
Его optional `tokenLifecycle` точно описывает два provider-issued artifacts —
|
||
`access` и `refresh`: request использует `access`, а refresh пока имеет режим
|
||
`operator_managed`. Это metadata без secret values и без заявления о
|
||
реализованном автоматическом refresh.
|
||
|
||
## Проверяемые 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, полями и
|
||
внутренней аудиторией.
|
||
- `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. Текущий credential type
|
||
`httpBearerAuth` использует access token для HTTP request. Автоматический обмен
|
||
refresh → access в текущем runtime не доказан и поэтому не заявлен: package
|
||
фиксирует `refreshMode: operator_managed`. Название 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 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.
|