@superbee/core 0.2.0-pre.2 → 0.2.0-pre.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1000 -0
- package/dist/artifact-contract.d.ts +90 -0
- package/dist/artifact-contract.d.ts.map +1 -0
- package/dist/artifact-contract.js +205 -0
- package/dist/artifact-contract.js.map +1 -0
- package/dist/backend.d.ts +19 -1
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js +83 -36
- package/dist/backend.js.map +1 -1
- package/dist/blob-contract.d.ts +80 -0
- package/dist/blob-contract.d.ts.map +1 -0
- package/dist/blob-contract.js +131 -0
- package/dist/blob-contract.js.map +1 -0
- package/dist/blob-journal.d.ts +302 -0
- package/dist/blob-journal.d.ts.map +1 -0
- package/dist/blob-journal.js +540 -0
- package/dist/blob-journal.js.map +1 -0
- package/dist/body-replace-guards.d.ts +50 -0
- package/dist/body-replace-guards.d.ts.map +1 -0
- package/dist/body-replace-guards.js +158 -0
- package/dist/body-replace-guards.js.map +1 -0
- package/dist/builtin-recipes/content.d.ts +93 -0
- package/dist/builtin-recipes/content.d.ts.map +1 -0
- package/dist/builtin-recipes/content.js +288 -0
- package/dist/builtin-recipes/content.js.map +1 -0
- package/dist/builtin-recipes.d.ts +10 -0
- package/dist/builtin-recipes.d.ts.map +1 -0
- package/dist/builtin-recipes.js +97 -0
- package/dist/builtin-recipes.js.map +1 -0
- package/dist/bundle-health/index.d.ts +14 -0
- package/dist/bundle-health/index.d.ts.map +1 -0
- package/dist/bundle-health/index.js +12 -0
- package/dist/bundle-health/index.js.map +1 -0
- package/dist/bundle-health/kind-findings.d.ts +44 -0
- package/dist/bundle-health/kind-findings.d.ts.map +1 -0
- package/dist/bundle-health/kind-findings.js +68 -0
- package/dist/bundle-health/kind-findings.js.map +1 -0
- package/dist/bundle-health/legacy-page.d.ts +39 -0
- package/dist/bundle-health/legacy-page.d.ts.map +1 -0
- package/dist/bundle-health/legacy-page.js +50 -0
- package/dist/bundle-health/legacy-page.js.map +1 -0
- package/dist/bundle-health/report.d.ts +202 -0
- package/dist/bundle-health/report.d.ts.map +1 -0
- package/dist/bundle-health/report.js +438 -0
- package/dist/bundle-health/report.js.map +1 -0
- package/dist/bundle-ops.d.ts +1 -1
- package/dist/bundle-ops.d.ts.map +1 -1
- package/dist/bundle-ops.js +2 -2
- package/dist/bundle-ops.js.map +1 -1
- package/dist/bundle.d.ts +2 -1
- package/dist/bundle.d.ts.map +1 -1
- package/dist/bundle.js +3 -3
- package/dist/bundle.js.map +1 -1
- package/dist/code-point-order.d.ts +8 -0
- package/dist/code-point-order.d.ts.map +1 -0
- package/dist/code-point-order.js +20 -0
- package/dist/code-point-order.js.map +1 -0
- package/dist/connectors.d.ts +230 -0
- package/dist/connectors.d.ts.map +1 -0
- package/dist/connectors.js +1150 -0
- package/dist/connectors.js.map +1 -0
- package/dist/cross-bundle.d.ts +135 -0
- package/dist/cross-bundle.d.ts.map +1 -0
- package/dist/cross-bundle.js +186 -0
- package/dist/cross-bundle.js.map +1 -0
- package/dist/dependencies.d.ts +94 -0
- package/dist/dependencies.d.ts.map +1 -0
- package/dist/dependencies.js +180 -0
- package/dist/dependencies.js.map +1 -0
- package/dist/document-codec.d.ts +6 -1
- package/dist/document-codec.d.ts.map +1 -1
- package/dist/document-codec.js +8 -4
- package/dist/document-codec.js.map +1 -1
- package/dist/document-field-actions.d.ts +31 -1
- package/dist/document-field-actions.d.ts.map +1 -1
- package/dist/document-field-actions.js +137 -34
- package/dist/document-field-actions.js.map +1 -1
- package/dist/document-mutation.d.ts +22 -10
- package/dist/document-mutation.d.ts.map +1 -1
- package/dist/document-mutation.js +58 -27
- package/dist/document-mutation.js.map +1 -1
- package/dist/document-write-policy.d.ts.map +1 -1
- package/dist/document-write-policy.js +4 -2
- package/dist/document-write-policy.js.map +1 -1
- package/dist/engine.d.ts +9 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +11 -4
- package/dist/engine.js.map +1 -1
- package/dist/file-journaled-backend-internal.d.ts +21 -0
- package/dist/file-journaled-backend-internal.d.ts.map +1 -0
- package/dist/file-journaled-backend-internal.js +12 -0
- package/dist/file-journaled-backend-internal.js.map +1 -0
- package/dist/file-journaled-backend.d.ts +200 -0
- package/dist/file-journaled-backend.d.ts.map +1 -0
- package/dist/file-journaled-backend.js +1334 -0
- package/dist/file-journaled-backend.js.map +1 -0
- package/dist/filesystem-host.d.ts +15 -0
- package/dist/filesystem-host.d.ts.map +1 -0
- package/dist/filesystem-host.js +20 -0
- package/dist/filesystem-host.js.map +1 -0
- package/dist/filesystem-identity.d.ts +86 -4
- package/dist/filesystem-identity.d.ts.map +1 -1
- package/dist/filesystem-identity.js +338 -102
- package/dist/filesystem-identity.js.map +1 -1
- package/dist/filesystem-lock.d.ts +16 -3
- package/dist/filesystem-lock.d.ts.map +1 -1
- package/dist/filesystem-lock.js +323 -77
- package/dist/filesystem-lock.js.map +1 -1
- package/dist/filesystem-push-role.d.ts +77 -0
- package/dist/filesystem-push-role.d.ts.map +1 -0
- package/dist/filesystem-push-role.js +165 -0
- package/dist/filesystem-push-role.js.map +1 -0
- package/dist/filesystem.d.ts +17 -0
- package/dist/filesystem.d.ts.map +1 -0
- package/dist/filesystem.js +18 -0
- package/dist/filesystem.js.map +1 -0
- package/dist/frontmatter.d.ts +8 -0
- package/dist/frontmatter.d.ts.map +1 -1
- package/dist/frontmatter.js +14 -4
- package/dist/frontmatter.js.map +1 -1
- package/dist/governed-body-write.d.ts +106 -0
- package/dist/governed-body-write.d.ts.map +1 -0
- package/dist/governed-body-write.js +251 -0
- package/dist/governed-body-write.js.map +1 -0
- package/dist/host-policy-snapshot.d.ts +9 -0
- package/dist/host-policy-snapshot.d.ts.map +1 -0
- package/dist/host-policy-snapshot.js +33 -0
- package/dist/host-policy-snapshot.js.map +1 -0
- package/dist/host-text.d.ts +31 -0
- package/dist/host-text.d.ts.map +1 -0
- package/dist/host-text.js +77 -0
- package/dist/host-text.js.map +1 -0
- package/dist/hosted-client/binding.d.ts +7 -0
- package/dist/hosted-client/binding.d.ts.map +1 -0
- package/dist/hosted-client/binding.js +10 -0
- package/dist/hosted-client/binding.js.map +1 -0
- package/dist/hosted-client/blob-delivery.d.ts +67 -0
- package/dist/hosted-client/blob-delivery.d.ts.map +1 -0
- package/dist/hosted-client/blob-delivery.js +169 -0
- package/dist/hosted-client/blob-delivery.js.map +1 -0
- package/dist/hosted-client/blob-write.d.ts +386 -0
- package/dist/hosted-client/blob-write.d.ts.map +1 -0
- package/dist/hosted-client/blob-write.js +770 -0
- package/dist/hosted-client/blob-write.js.map +1 -0
- package/dist/hosted-client/bundle-listing.d.ts +46 -0
- package/dist/hosted-client/bundle-listing.d.ts.map +1 -0
- package/dist/hosted-client/bundle-listing.js +36 -0
- package/dist/hosted-client/bundle-listing.js.map +1 -0
- package/dist/hosted-client/client.d.ts +219 -0
- package/dist/hosted-client/client.d.ts.map +1 -0
- package/dist/hosted-client/client.js +309 -0
- package/dist/hosted-client/client.js.map +1 -0
- package/dist/hosted-client/directory.d.ts +46 -0
- package/dist/hosted-client/directory.d.ts.map +1 -0
- package/dist/hosted-client/directory.js +74 -0
- package/dist/hosted-client/directory.js.map +1 -0
- package/dist/hosted-client/errors.d.ts +26 -0
- package/dist/hosted-client/errors.d.ts.map +1 -0
- package/dist/hosted-client/errors.js +35 -0
- package/dist/hosted-client/errors.js.map +1 -0
- package/dist/hosted-client/inbox.d.ts +320 -0
- package/dist/hosted-client/inbox.d.ts.map +1 -0
- package/dist/hosted-client/inbox.js +379 -0
- package/dist/hosted-client/inbox.js.map +1 -0
- package/dist/hosted-client/index.d.ts +30 -0
- package/dist/hosted-client/index.d.ts.map +1 -0
- package/dist/hosted-client/index.js +32 -0
- package/dist/hosted-client/index.js.map +1 -0
- package/dist/hosted-client/journal.d.ts +103 -0
- package/dist/hosted-client/journal.d.ts.map +1 -0
- package/dist/hosted-client/journal.js +464 -0
- package/dist/hosted-client/journal.js.map +1 -0
- package/dist/hosted-client/node.d.ts +74 -0
- package/dist/hosted-client/node.d.ts.map +1 -0
- package/dist/hosted-client/node.js +219 -0
- package/dist/hosted-client/node.js.map +1 -0
- package/dist/hosted-client/reference.d.ts +12 -0
- package/dist/hosted-client/reference.d.ts.map +1 -0
- package/dist/hosted-client/reference.js +29 -0
- package/dist/hosted-client/reference.js.map +1 -0
- package/dist/hosted-client/write.d.ts +217 -0
- package/dist/hosted-client/write.d.ts.map +1 -0
- package/dist/hosted-client/write.js +418 -0
- package/dist/hosted-client/write.js.map +1 -0
- package/dist/hosted-transport/answer-rows.d.ts +243 -0
- package/dist/hosted-transport/answer-rows.d.ts.map +1 -0
- package/dist/hosted-transport/answer-rows.js +454 -0
- package/dist/hosted-transport/answer-rows.js.map +1 -0
- package/dist/hosted-transport/blob-reads.d.ts +35 -0
- package/dist/hosted-transport/blob-reads.d.ts.map +1 -0
- package/dist/hosted-transport/blob-reads.js +179 -0
- package/dist/hosted-transport/blob-reads.js.map +1 -0
- package/dist/hosted-transport/blob-write.d.ts +34 -0
- package/dist/hosted-transport/blob-write.d.ts.map +1 -0
- package/dist/hosted-transport/blob-write.js +115 -0
- package/dist/hosted-transport/blob-write.js.map +1 -0
- package/dist/hosted-transport/carrier.d.ts +151 -0
- package/dist/hosted-transport/carrier.d.ts.map +1 -0
- package/dist/hosted-transport/carrier.js +260 -0
- package/dist/hosted-transport/carrier.js.map +1 -0
- package/dist/hosted-transport/history-pages.d.ts +55 -0
- package/dist/hosted-transport/history-pages.d.ts.map +1 -0
- package/dist/hosted-transport/history-pages.js +69 -0
- package/dist/hosted-transport/history-pages.js.map +1 -0
- package/dist/hosted-transport/index.d.ts +18 -0
- package/dist/hosted-transport/index.d.ts.map +1 -0
- package/dist/hosted-transport/index.js +18 -0
- package/dist/hosted-transport/index.js.map +1 -0
- package/dist/hosted-transport/operations.d.ts +85 -0
- package/dist/hosted-transport/operations.d.ts.map +1 -0
- package/dist/hosted-transport/operations.js +128 -0
- package/dist/hosted-transport/operations.js.map +1 -0
- package/dist/hosted-transport/paged-reads.d.ts +87 -0
- package/dist/hosted-transport/paged-reads.d.ts.map +1 -0
- package/dist/hosted-transport/paged-reads.js +238 -0
- package/dist/hosted-transport/paged-reads.js.map +1 -0
- package/dist/hosted-transport/read-adapter.d.ts +256 -0
- package/dist/hosted-transport/read-adapter.d.ts.map +1 -0
- package/dist/hosted-transport/read-adapter.js +568 -0
- package/dist/hosted-transport/read-adapter.js.map +1 -0
- package/dist/hosted-transport/root-write.d.ts +114 -0
- package/dist/hosted-transport/root-write.d.ts.map +1 -0
- package/dist/hosted-transport/root-write.js +171 -0
- package/dist/hosted-transport/root-write.js.map +1 -0
- package/dist/hosted-transport/whole-document-transport.d.ts +204 -0
- package/dist/hosted-transport/whole-document-transport.d.ts.map +1 -0
- package/dist/hosted-transport/whole-document-transport.js +447 -0
- package/dist/hosted-transport/whole-document-transport.js.map +1 -0
- package/dist/index.d.ts +34 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -8
- package/dist/index.js.map +1 -1
- package/dist/indexeddb-backend.d.ts +28 -6
- package/dist/indexeddb-backend.d.ts.map +1 -1
- package/dist/indexeddb-backend.js +471 -92
- package/dist/indexeddb-backend.js.map +1 -1
- package/dist/journaled-backend.d.ts +247 -10
- package/dist/journaled-backend.d.ts.map +1 -1
- package/dist/journaled-backend.js +302 -1
- package/dist/journaled-backend.js.map +1 -1
- package/dist/kind-authoring.d.ts +170 -0
- package/dist/kind-authoring.d.ts.map +1 -0
- package/dist/kind-authoring.js +451 -0
- package/dist/kind-authoring.js.map +1 -0
- package/dist/kind-conformance-error.d.ts +15 -0
- package/dist/kind-conformance-error.d.ts.map +1 -0
- package/dist/kind-conformance-error.js +20 -0
- package/dist/kind-conformance-error.js.map +1 -0
- package/dist/kind-coverage.d.ts +99 -0
- package/dist/kind-coverage.d.ts.map +1 -0
- package/dist/kind-coverage.js +186 -0
- package/dist/kind-coverage.js.map +1 -0
- package/dist/kind-field-mutation.d.ts +4 -0
- package/dist/kind-field-mutation.d.ts.map +1 -1
- package/dist/kind-field-mutation.js +28 -3
- package/dist/kind-field-mutation.js.map +1 -1
- package/dist/kinds-load.js +4 -4
- package/dist/kinds-load.js.map +1 -1
- package/dist/kinds.d.ts +65 -2
- package/dist/kinds.d.ts.map +1 -1
- package/dist/kinds.js +341 -21
- package/dist/kinds.js.map +1 -1
- package/dist/link-types.d.ts +26 -0
- package/dist/link-types.d.ts.map +1 -0
- package/dist/link-types.js +57 -0
- package/dist/link-types.js.map +1 -0
- package/dist/links.d.ts +21 -3
- package/dist/links.d.ts.map +1 -1
- package/dist/links.js +213 -14
- package/dist/links.js.map +1 -1
- package/dist/managed-document-metadata.d.ts +2 -0
- package/dist/managed-document-metadata.d.ts.map +1 -0
- package/dist/managed-document-metadata.js +9 -0
- package/dist/managed-document-metadata.js.map +1 -0
- package/dist/memory-backend.d.ts +2 -2
- package/dist/memory-backend.d.ts.map +1 -1
- package/dist/memory-backend.js +26 -11
- package/dist/memory-backend.js.map +1 -1
- package/dist/paths.d.ts +16 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +25 -0
- package/dist/paths.js.map +1 -1
- package/dist/platform.d.ts +13 -2
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +16 -0
- package/dist/platform.js.map +1 -1
- package/dist/portable-export.d.ts +119 -0
- package/dist/portable-export.d.ts.map +1 -0
- package/dist/portable-export.js +501 -0
- package/dist/portable-export.js.map +1 -0
- package/dist/publication-filesystem.d.ts +1 -1
- package/dist/publication-filesystem.d.ts.map +1 -1
- package/dist/publication-filesystem.js +1 -1
- package/dist/publication-filesystem.js.map +1 -1
- package/dist/qualified-links.d.ts +67 -0
- package/dist/qualified-links.d.ts.map +1 -0
- package/dist/qualified-links.js +117 -0
- package/dist/qualified-links.js.map +1 -0
- package/dist/query-filter.d.ts +16 -2
- package/dist/query-filter.d.ts.map +1 -1
- package/dist/query-filter.js +40 -1
- package/dist/query-filter.js.map +1 -1
- package/dist/query-order.d.ts +22 -0
- package/dist/query-order.d.ts.map +1 -0
- package/dist/query-order.js +42 -0
- package/dist/query-order.js.map +1 -0
- package/dist/recipe-lifecycle/errors.d.ts +94 -0
- package/dist/recipe-lifecycle/errors.d.ts.map +1 -0
- package/dist/recipe-lifecycle/errors.js +122 -0
- package/dist/recipe-lifecycle/errors.js.map +1 -0
- package/dist/recipe-lifecycle/evolution.d.ts +75 -0
- package/dist/recipe-lifecycle/evolution.d.ts.map +1 -0
- package/dist/recipe-lifecycle/evolution.js +789 -0
- package/dist/recipe-lifecycle/evolution.js.map +1 -0
- package/dist/recipe-lifecycle/index.d.ts +24 -0
- package/dist/recipe-lifecycle/index.d.ts.map +1 -0
- package/dist/recipe-lifecycle/index.js +20 -0
- package/dist/recipe-lifecycle/index.js.map +1 -0
- package/dist/recipe-lifecycle/install.d.ts +126 -0
- package/dist/recipe-lifecycle/install.d.ts.map +1 -0
- package/dist/recipe-lifecycle/install.js +511 -0
- package/dist/recipe-lifecycle/install.js.map +1 -0
- package/dist/recipe-lifecycle/legacy-aliases.d.ts +16 -0
- package/dist/recipe-lifecycle/legacy-aliases.d.ts.map +1 -0
- package/dist/recipe-lifecycle/legacy-aliases.js +51 -0
- package/dist/recipe-lifecycle/legacy-aliases.js.map +1 -0
- package/dist/recipe-lifecycle/stamp.d.ts +136 -0
- package/dist/recipe-lifecycle/stamp.d.ts.map +1 -0
- package/dist/recipe-lifecycle/stamp.js +326 -0
- package/dist/recipe-lifecycle/stamp.js.map +1 -0
- package/dist/recipes.d.ts +29 -1
- package/dist/recipes.d.ts.map +1 -1
- package/dist/recipes.js +137 -1
- package/dist/recipes.js.map +1 -1
- package/dist/record-overlay/apply.d.ts +43 -0
- package/dist/record-overlay/apply.d.ts.map +1 -0
- package/dist/record-overlay/apply.js +527 -0
- package/dist/record-overlay/apply.js.map +1 -0
- package/dist/record-overlay/canonical.d.ts +22 -0
- package/dist/record-overlay/canonical.d.ts.map +1 -0
- package/dist/record-overlay/canonical.js +47 -0
- package/dist/record-overlay/canonical.js.map +1 -0
- package/dist/record-overlay/digest.d.ts +4 -0
- package/dist/record-overlay/digest.d.ts.map +1 -0
- package/dist/record-overlay/digest.js +11 -0
- package/dist/record-overlay/digest.js.map +1 -0
- package/dist/record-overlay/errors.d.ts +44 -0
- package/dist/record-overlay/errors.d.ts.map +1 -0
- package/dist/record-overlay/errors.js +55 -0
- package/dist/record-overlay/errors.js.map +1 -0
- package/dist/record-overlay/findings.d.ts +5 -0
- package/dist/record-overlay/findings.d.ts.map +1 -0
- package/dist/record-overlay/findings.js +83 -0
- package/dist/record-overlay/findings.js.map +1 -0
- package/dist/record-overlay/index.d.ts +23 -0
- package/dist/record-overlay/index.d.ts.map +1 -0
- package/dist/record-overlay/index.js +21 -0
- package/dist/record-overlay/index.js.map +1 -0
- package/dist/record-overlay/plan.d.ts +12 -0
- package/dist/record-overlay/plan.d.ts.map +1 -0
- package/dist/record-overlay/plan.js +285 -0
- package/dist/record-overlay/plan.js.map +1 -0
- package/dist/record-overlay/policy.d.ts +69 -0
- package/dist/record-overlay/policy.d.ts.map +1 -0
- package/dist/record-overlay/policy.js +228 -0
- package/dist/record-overlay/policy.js.map +1 -0
- package/dist/record-overlay/redact.d.ts +3 -0
- package/dist/record-overlay/redact.d.ts.map +1 -0
- package/dist/record-overlay/redact.js +27 -0
- package/dist/record-overlay/redact.js.map +1 -0
- package/dist/record-overlay/signal-kind.d.ts +30 -0
- package/dist/record-overlay/signal-kind.d.ts.map +1 -0
- package/dist/record-overlay/signal-kind.js +38 -0
- package/dist/record-overlay/signal-kind.js.map +1 -0
- package/dist/record-overlay/signal-text.d.ts +45 -0
- package/dist/record-overlay/signal-text.d.ts.map +1 -0
- package/dist/record-overlay/signal-text.js +116 -0
- package/dist/record-overlay/signal-text.js.map +1 -0
- package/dist/record-overlay/source-index.d.ts +19 -0
- package/dist/record-overlay/source-index.d.ts.map +1 -0
- package/dist/record-overlay/source-index.js +102 -0
- package/dist/record-overlay/source-index.js.map +1 -0
- package/dist/record-overlay/types.d.ts +291 -0
- package/dist/record-overlay/types.d.ts.map +1 -0
- package/dist/record-overlay/types.js +2 -0
- package/dist/record-overlay/types.js.map +1 -0
- package/dist/remote-backend.d.ts +67 -67
- package/dist/remote-backend.d.ts.map +1 -1
- package/dist/remote-backend.js +169 -201
- package/dist/remote-backend.js.map +1 -1
- package/dist/remote-document-codec.d.ts +15 -0
- package/dist/remote-document-codec.d.ts.map +1 -0
- package/dist/remote-document-codec.js +103 -0
- package/dist/remote-document-codec.js.map +1 -0
- package/dist/remote-error.d.ts +38 -0
- package/dist/remote-error.d.ts.map +1 -0
- package/dist/remote-error.js +59 -0
- package/dist/remote-error.js.map +1 -0
- package/dist/remote-operations.d.ts +6 -3
- package/dist/remote-operations.d.ts.map +1 -1
- package/dist/remote-operations.js +23 -5
- package/dist/remote-operations.js.map +1 -1
- package/dist/remote-parsers.d.ts +86 -0
- package/dist/remote-parsers.d.ts.map +1 -0
- package/dist/remote-parsers.js +181 -0
- package/dist/remote-parsers.js.map +1 -0
- package/dist/remote.d.ts +5 -2
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +5 -1
- package/dist/remote.js.map +1 -1
- package/dist/search/backend-source.d.ts +6 -0
- package/dist/search/backend-source.d.ts.map +1 -0
- package/dist/search/backend-source.js +33 -0
- package/dist/search/backend-source.js.map +1 -0
- package/dist/search/contract.d.ts +151 -0
- package/dist/search/contract.d.ts.map +1 -0
- package/dist/search/contract.js +78 -0
- package/dist/search/contract.js.map +1 -0
- package/dist/search/errors.d.ts +30 -0
- package/dist/search/errors.d.ts.map +1 -0
- package/dist/search/errors.js +50 -0
- package/dist/search/errors.js.map +1 -0
- package/dist/search/identity.d.ts +12 -0
- package/dist/search/identity.d.ts.map +1 -0
- package/dist/search/identity.js +90 -0
- package/dist/search/identity.js.map +1 -0
- package/dist/search/index.d.ts +16 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +15 -0
- package/dist/search/index.js.map +1 -0
- package/dist/search/memory-source.d.ts +36 -0
- package/dist/search/memory-source.d.ts.map +1 -0
- package/dist/search/memory-source.js +235 -0
- package/dist/search/memory-source.js.map +1 -0
- package/dist/search/rank.d.ts +34 -0
- package/dist/search/rank.d.ts.map +1 -0
- package/dist/search/rank.js +129 -0
- package/dist/search/rank.js.map +1 -0
- package/dist/search/row-text.d.ts +38 -0
- package/dist/search/row-text.d.ts.map +1 -0
- package/dist/search/row-text.js +88 -0
- package/dist/search/row-text.js.map +1 -0
- package/dist/search/search.d.ts +28 -0
- package/dist/search/search.d.ts.map +1 -0
- package/dist/search/search.js +257 -0
- package/dist/search/search.js.map +1 -0
- package/dist/search/source.d.ts +67 -0
- package/dist/search/source.d.ts.map +1 -0
- package/dist/search/source.js +2 -0
- package/dist/search/source.js.map +1 -0
- package/dist/search/text.d.ts +47 -0
- package/dist/search/text.d.ts.map +1 -0
- package/dist/search/text.js +148 -0
- package/dist/search/text.js.map +1 -0
- package/dist/site-capabilities.d.ts +339 -0
- package/dist/site-capabilities.d.ts.map +1 -0
- package/dist/site-capabilities.js +717 -0
- package/dist/site-capabilities.js.map +1 -0
- package/dist/source-engine/guard.d.ts +82 -0
- package/dist/source-engine/guard.d.ts.map +1 -0
- package/dist/source-engine/guard.js +396 -0
- package/dist/source-engine/guard.js.map +1 -0
- package/dist/source-engine/index.d.ts +24 -0
- package/dist/source-engine/index.d.ts.map +1 -0
- package/dist/source-engine/index.js +20 -0
- package/dist/source-engine/index.js.map +1 -0
- package/dist/source-engine/machine-state.d.ts +74 -0
- package/dist/source-engine/machine-state.d.ts.map +1 -0
- package/dist/source-engine/machine-state.js +48 -0
- package/dist/source-engine/machine-state.js.map +1 -0
- package/dist/source-engine/observe.d.ts +18 -0
- package/dist/source-engine/observe.d.ts.map +1 -0
- package/dist/source-engine/observe.js +598 -0
- package/dist/source-engine/observe.js.map +1 -0
- package/dist/source-engine/robots.d.ts +53 -0
- package/dist/source-engine/robots.d.ts.map +1 -0
- package/dist/source-engine/robots.js +158 -0
- package/dist/source-engine/robots.js.map +1 -0
- package/dist/source-engine/types.d.ts +285 -0
- package/dist/source-engine/types.d.ts.map +1 -0
- package/dist/source-engine/types.js +2 -0
- package/dist/source-engine/types.js.map +1 -0
- package/dist/source-harness/conformance.d.ts +16 -0
- package/dist/source-harness/conformance.d.ts.map +1 -0
- package/dist/source-harness/conformance.js +265 -0
- package/dist/source-harness/conformance.js.map +1 -0
- package/dist/source-harness/fixture.d.ts +95 -0
- package/dist/source-harness/fixture.d.ts.map +1 -0
- package/dist/source-harness/fixture.js +261 -0
- package/dist/source-harness/fixture.js.map +1 -0
- package/dist/source-harness/index.d.ts +22 -0
- package/dist/source-harness/index.d.ts.map +1 -0
- package/dist/source-harness/index.js +19 -0
- package/dist/source-harness/index.js.map +1 -0
- package/dist/source-harness/peer.d.ts +74 -0
- package/dist/source-harness/peer.d.ts.map +1 -0
- package/dist/source-harness/peer.js +106 -0
- package/dist/source-harness/peer.js.map +1 -0
- package/dist/storage.d.ts +6 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +4 -1
- package/dist/storage.js.map +1 -1
- package/dist/types.d.ts +33 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/uncertain-write.d.ts +156 -19
- package/dist/uncertain-write.d.ts.map +1 -1
- package/dist/uncertain-write.js +90 -31
- package/dist/uncertain-write.js.map +1 -1
- package/dist/versioning.d.ts +9 -4
- package/dist/versioning.d.ts.map +1 -1
- package/dist/versioning.js +9 -4
- package/dist/versioning.js.map +1 -1
- package/dist/write-identity.d.ts +5 -0
- package/dist/write-identity.d.ts.map +1 -0
- package/dist/write-identity.js +9 -0
- package/dist/write-identity.js.map +1 -0
- package/package.json +94 -2
|
@@ -0,0 +1,1150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@superbee/core/connectors`: target-agnostic Connector declarations.
|
|
3
|
+
*
|
|
4
|
+
* A Connector is a `type: Connector` document under `connectors/` that says what an integration
|
|
5
|
+
* is: the credential kind (whose hosts come only from core's {@link AUTH_PROVIDERS} table, never
|
|
6
|
+
* from the document), keyless `domains`, `record_sources` that map normalized records onto one
|
|
7
|
+
* target Kind's fields, and actions and events as declared shapes. The document holds no authority:
|
|
8
|
+
* nothing here fetches, executes or receives. {@link parseConnectorDoc} is the one parser, every
|
|
9
|
+
* nested block is closed by {@link CONNECTOR_KEYS}, and {@link connectorPinDigest} reads the same
|
|
10
|
+
* table, so a key cannot be accepted without being pinned.
|
|
11
|
+
*
|
|
12
|
+
* The mapping contract is code: {@link mapRecord} never lets a mapping gap erase a stored value (an
|
|
13
|
+
* unobserved field is carried, an unmapped value is kept or refuses the record, and only an
|
|
14
|
+
* explicit empty value clears an optional field); {@link changeBetween} builds the record-overlay
|
|
15
|
+
* change; {@link findingsFor} turns diagnostics into findings for `planFindings`.
|
|
16
|
+
* Storage-neutral: no Node globals or builtins.
|
|
17
|
+
*/
|
|
18
|
+
import { compareCodePoints } from "./code-point-order.js";
|
|
19
|
+
import { isStandardDocumentSetField } from "./document-field-actions.js";
|
|
20
|
+
import { PROGRESS_STATUS_FIELD, isKindListField, isPresent } from "./kinds.js";
|
|
21
|
+
import { isManagedDocumentMetadataField } from "./managed-document-metadata.js";
|
|
22
|
+
import { overlayCanonicalJson, same } from "./record-overlay/canonical.js";
|
|
23
|
+
import { OverlayRefusal, REFUSAL_CODES } from "./record-overlay/errors.js";
|
|
24
|
+
import { NEVER_OWNED, readOverlayPolicy, workStateFields } from "./record-overlay/policy.js";
|
|
25
|
+
import { definitionCanonicalJson } from "./recipe-lifecycle/stamp.js";
|
|
26
|
+
import { sha256Hex } from "./versioning.js";
|
|
27
|
+
export const CONNECTOR_TYPE = "Connector";
|
|
28
|
+
export const CONNECTORS_PREFIX = "connectors/";
|
|
29
|
+
/** The normalization rules of {@link connectorPinDigest}; bumped whenever a default or sort rule changes. */
|
|
30
|
+
export const CONNECTOR_PIN_RULES = 1;
|
|
31
|
+
/**
|
|
32
|
+
* Credential providers: provider key to the exact hostnames a `token` or `oauth2` credential may be
|
|
33
|
+
* sent to. A document names a row; it never states a host. Each row is added in its own reviewed
|
|
34
|
+
* change, verified against the vendor's primary documentation. The pin records the hosts a row
|
|
35
|
+
* resolves to, so adding a host to an existing row changes the pin of every Connection on that
|
|
36
|
+
* provider, which then needs approving again.
|
|
37
|
+
*/
|
|
38
|
+
export const AUTH_PROVIDERS = Object.freeze({
|
|
39
|
+
github: Object.freeze(["api.github.com"]),
|
|
40
|
+
});
|
|
41
|
+
const GITHUB_APP_HOSTS = Object.freeze(["api.github.com"]);
|
|
42
|
+
/** The accepted keys of every closed block. The shape check and the pin digest read only this table. */
|
|
43
|
+
export const CONNECTOR_KEYS = Object.freeze({
|
|
44
|
+
auth: Object.freeze(["kind", "provider", "permissions", "scheme"]),
|
|
45
|
+
source: Object.freeze(["name", "profile", "target_kind", "mode", "identity", "fields", "completeness"]),
|
|
46
|
+
identity: Object.freeze(["resource", "id"]),
|
|
47
|
+
mapping: Object.freeze(["from", "values", "unmapped"]),
|
|
48
|
+
action: Object.freeze(["name", "method", "host", "input", "confirm"]),
|
|
49
|
+
input: Object.freeze(["type", "required", "values"]),
|
|
50
|
+
event: Object.freeze(["name", "signature", "references"]),
|
|
51
|
+
signature: Object.freeze(["scheme", "header"]),
|
|
52
|
+
});
|
|
53
|
+
/**
|
|
54
|
+
* The top-level keys a Connector declares beside OKF's `type`, `title` and `description`. None may
|
|
55
|
+
* be an OKF standard or managed field of either edition: OKF v0.2 reserves `sources` for
|
|
56
|
+
* provenance, which is why the declaration's sources live under `record_sources`.
|
|
57
|
+
*/
|
|
58
|
+
export const CONNECTOR_DOCUMENT_KEYS = Object.freeze(["version", "auth", "domains", "record_sources", "actions", "events"]);
|
|
59
|
+
export const CONNECTOR_ERROR_CODES = Object.freeze([
|
|
60
|
+
"CONNECTOR_MALFORMED",
|
|
61
|
+
"CONNECTOR_UNKNOWN_KEY",
|
|
62
|
+
"CONNECTOR_AUTH_INVALID",
|
|
63
|
+
"CONNECTOR_SECRET_PRESENT",
|
|
64
|
+
"CONNECTOR_DOMAIN_INVALID",
|
|
65
|
+
"CONNECTOR_DUPLICATE",
|
|
66
|
+
"CONNECTOR_PROFILE_INVALID",
|
|
67
|
+
"CONNECTOR_TARGET_KIND_INVALID",
|
|
68
|
+
"CONNECTOR_MODE_UNSUPPORTED",
|
|
69
|
+
"CONNECTOR_IDENTITY_UNSTABLE",
|
|
70
|
+
"CONNECTOR_FIELD_INVALID",
|
|
71
|
+
"CONNECTOR_FIELD_NEVER_OWNED",
|
|
72
|
+
"CONNECTOR_FIELD_UNDECLARED",
|
|
73
|
+
"CONNECTOR_VALUES_INVALID",
|
|
74
|
+
"CONNECTOR_LIST_FIELD_UNSUPPORTED",
|
|
75
|
+
"CONNECTOR_ACTION_INVALID",
|
|
76
|
+
"CONNECTOR_EVENT_INVALID",
|
|
77
|
+
]);
|
|
78
|
+
// ------------------------------------------------------------------------------------------ grammar
|
|
79
|
+
const SLUG = /^[a-z][a-z0-9-]*$/;
|
|
80
|
+
const DOC_SLUG = /^[a-z0-9][a-z0-9-]*$/;
|
|
81
|
+
const FIELD_NAME = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
82
|
+
const PATH_SEGMENT = /^[A-Za-z0-9_-]+$/;
|
|
83
|
+
const LOWER_NAME = /^[a-z][a-z0-9_]*$/;
|
|
84
|
+
const HEADER = /^[a-z0-9-]+$/;
|
|
85
|
+
const PARAMETER = "[A-Za-z0-9._~-]+";
|
|
86
|
+
const HOST_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
87
|
+
const URL_CHARS = /^[A-Za-z0-9\-._~:/?[\]@!$&'()*+,;=%]*$/;
|
|
88
|
+
const PROTOTYPE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
89
|
+
const RESERVED_TARGET_KINDS = new Set(["Convention", "View", "Connector", "Reference", "Source Feed", "Signal"]);
|
|
90
|
+
const AUTH_KINDS = ["none", "token", "github-app", "oauth2", "signed-webhook"];
|
|
91
|
+
const AUTH_KEYS_BY_KIND = {
|
|
92
|
+
none: [], token: ["provider"], "github-app": ["permissions"], oauth2: ["provider"], "signed-webhook": ["scheme"],
|
|
93
|
+
};
|
|
94
|
+
const METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
|
95
|
+
const INPUT_TYPES = new Set(["string", "text", "integer", "boolean"]);
|
|
96
|
+
const MAX_TEMPLATE = 2048;
|
|
97
|
+
function pointer(path) {
|
|
98
|
+
return path.map((s) => "/" + String(s).replaceAll("~", "~0").replaceAll("/", "~1")).join("");
|
|
99
|
+
}
|
|
100
|
+
function record(value) {
|
|
101
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
102
|
+
}
|
|
103
|
+
/** A hostname as the declaration grammar admits it, or the reason it is refused. */
|
|
104
|
+
function hostnameProblem(host) {
|
|
105
|
+
if (typeof host !== "string" || host.length === 0 || host.length > 253)
|
|
106
|
+
return "must be a hostname of 1 to 253 characters";
|
|
107
|
+
const labels = host.split(".");
|
|
108
|
+
if (labels.length < 2)
|
|
109
|
+
return "needs two or more labels";
|
|
110
|
+
if (!labels.every((label) => HOST_LABEL.test(label)))
|
|
111
|
+
return "takes lowercase labels of letters, digits and inner hyphens, with no trailing dot, scheme, port, path or wildcard";
|
|
112
|
+
const last = labels[labels.length - 1];
|
|
113
|
+
if (/^[0-9]+$/.test(last) || /^0x[0-9a-f]*$/.test(last))
|
|
114
|
+
return "may not end in a numeric label (an address literal)";
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
/** The hosts a credential of this kind may be sent to: a table row or a fixed set, never document text. */
|
|
118
|
+
export function credentialHosts(auth) {
|
|
119
|
+
if (auth.kind === "github-app")
|
|
120
|
+
return GITHUB_APP_HOSTS;
|
|
121
|
+
if ((auth.kind === "token" || auth.kind === "oauth2") && typeof auth.provider === "string" && Object.hasOwn(AUTH_PROVIDERS, auth.provider)) {
|
|
122
|
+
return AUTH_PROVIDERS[auth.provider];
|
|
123
|
+
}
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
// ------------------------------------------------------------------------------------ secret scan
|
|
127
|
+
const SECRET_KEYS = new Set(["token", "secret", "password", "passwd", "client_secret", "private_key", "api_key", "apikey", "authorization", "bearer", "cookie"]);
|
|
128
|
+
const SECRET_PREFIXES = ["ghp_", "gho_", "ghu_", "ghs_", "ghr_", "github_pat_"];
|
|
129
|
+
/**
|
|
130
|
+
* A URL authority carrying userinfo (`scheme://name@` or `scheme://name:password@`) anywhere in the
|
|
131
|
+
* text. A linear scan with no `URL` global, so the rule holds in every realm.
|
|
132
|
+
*/
|
|
133
|
+
function hasUserinfo(text) {
|
|
134
|
+
for (let at = text.indexOf("://"); at !== -1; at = text.indexOf("://", at + 3)) {
|
|
135
|
+
for (let i = at + 3; i < text.length; i++) {
|
|
136
|
+
const c = text[i];
|
|
137
|
+
if (c === "@")
|
|
138
|
+
return true;
|
|
139
|
+
if (c === "/" || c === "?" || c === "#" || c.trim() === "")
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
function secretShaped(text) {
|
|
146
|
+
const s = text.trimStart();
|
|
147
|
+
return SECRET_PREFIXES.some((prefix) => s.startsWith(prefix)) || /^xox[abprs]-/.test(s) || /^bearer /i.test(s)
|
|
148
|
+
|| (text.includes("-----BEGIN") && text.includes("PRIVATE KEY")) || hasUserinfo(text);
|
|
149
|
+
}
|
|
150
|
+
/** Token shapes in prose: a prefix followed by token characters, or `Bearer` followed by a token-like run. */
|
|
151
|
+
function bodyHoldsSecret(body) {
|
|
152
|
+
if (/(?:^|[^A-Za-z0-9_])(?:gh[pousr]_|github_pat_)[A-Za-z0-9_]{8}/.test(body))
|
|
153
|
+
return true;
|
|
154
|
+
if (/(?:^|[^A-Za-z0-9_])xox[abprs]-[A-Za-z0-9-]{8}/.test(body))
|
|
155
|
+
return true;
|
|
156
|
+
if (body.includes("-----BEGIN") && body.includes("PRIVATE KEY"))
|
|
157
|
+
return true;
|
|
158
|
+
if (hasUserinfo(body))
|
|
159
|
+
return true;
|
|
160
|
+
for (const match of body.matchAll(/\bbearer[ \t]+([A-Za-z0-9._~+/=-]{16,})/gi)) {
|
|
161
|
+
if (/[0-9]/.test(match[1]) && /[A-Za-z]/.test(match[1]))
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
const holdsSecret = (text) => secretShaped(text) || bodyHoldsSecret(text);
|
|
167
|
+
/**
|
|
168
|
+
* Keys hold no prose, so a token glued to a word still counts. The shapes use the providers' real
|
|
169
|
+
* token lengths, so data names that merely contain a prefix ("highs_", "weighs_") stay legal.
|
|
170
|
+
*/
|
|
171
|
+
const keyHoldsSecret = (key) => holdsSecret(key) || /gh[pousr]_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{22,}|xox[abprs]-[A-Za-z0-9-]{10,}/.test(key);
|
|
172
|
+
/**
|
|
173
|
+
* Maps whose keys are data names (target fields, provider natives, input and permission names), not
|
|
174
|
+
* credential slots: their keys get the token-shape rules only, never the key-name list.
|
|
175
|
+
*/
|
|
176
|
+
function keysAreDataNames(path) {
|
|
177
|
+
const n = path.length;
|
|
178
|
+
return (n === 3 && path[0] === "record_sources" && path[2] === "fields")
|
|
179
|
+
|| (n === 5 && path[0] === "record_sources" && path[2] === "fields" && path[4] === "values")
|
|
180
|
+
|| (n === 3 && path[0] === "actions" && path[2] === "input")
|
|
181
|
+
|| (n === 2 && path[0] === "auth" && path[1] === "permissions");
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Walk the frontmatter once, visiting each container at most once: a container reached twice is a
|
|
185
|
+
* YAML alias (shared or circular), refused as `CONNECTOR_MALFORMED` so the scan, and every reader
|
|
186
|
+
* after it, stays bounded by the document's size. Secret refusals name a path and never a value; a
|
|
187
|
+
* secret-looking key reports its parent.
|
|
188
|
+
*/
|
|
189
|
+
function scanDocument(value, path, out, seen) {
|
|
190
|
+
const add = (at, what) => {
|
|
191
|
+
const p = pointer(at);
|
|
192
|
+
if (!out.has(p))
|
|
193
|
+
out.set(p, { code: "CONNECTOR_SECRET_PRESENT", path: p, message: `${what} at '${p || "/"}'; a declaration never carries a credential` });
|
|
194
|
+
};
|
|
195
|
+
if (typeof value === "string") {
|
|
196
|
+
if (holdsSecret(value))
|
|
197
|
+
add(path, "a secret-shaped value");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (!Array.isArray(value) && !record(value))
|
|
201
|
+
return;
|
|
202
|
+
if (seen.has(value)) {
|
|
203
|
+
const p = pointer(path);
|
|
204
|
+
out.set(`alias:${p}`, { code: "CONNECTOR_MALFORMED", path: p, message: `a Connector may not use YAML aliases (at '${p || "/"}')` });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
seen.add(value);
|
|
208
|
+
if (Array.isArray(value)) {
|
|
209
|
+
value.forEach((item, index) => scanDocument(item, [...path, index], out, seen));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const data = keysAreDataNames(path);
|
|
213
|
+
for (const [key, item] of Object.entries(value)) {
|
|
214
|
+
if ((!data && SECRET_KEYS.has(key.toLowerCase())) || keyHoldsSecret(key))
|
|
215
|
+
add(path, "a secret-looking key");
|
|
216
|
+
else
|
|
217
|
+
scanDocument(item, [...path, key], out, seen);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function escapeRegExp(text) {
|
|
221
|
+
return text.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* An absolute https URL template. A placeholder lies inside one path segment (at most one per
|
|
225
|
+
* segment, with optional literal text around it) or is a whole query value; never in the host. No
|
|
226
|
+
* userinfo, port, fragment or dot segment.
|
|
227
|
+
*/
|
|
228
|
+
function parseResourceTemplate(template) {
|
|
229
|
+
const fail = (reason) => ({ ok: false, reason });
|
|
230
|
+
if (typeof template !== "string" || template.length === 0 || template.length > MAX_TEMPLATE)
|
|
231
|
+
return fail(`must be a URL template of at most ${MAX_TEMPLATE} characters`);
|
|
232
|
+
if (!template.startsWith("https://"))
|
|
233
|
+
return fail("must be an absolute https URL");
|
|
234
|
+
if (template.includes("#"))
|
|
235
|
+
return fail("may not carry a fragment");
|
|
236
|
+
if (!URL_CHARS.test(template.replace(/\{[^{}]*\}/g, "x")))
|
|
237
|
+
return fail("holds characters a URL does not");
|
|
238
|
+
const rest = template.slice("https://".length);
|
|
239
|
+
const cut = rest.search(/[/?]/);
|
|
240
|
+
const authority = cut === -1 ? rest : rest.slice(0, cut);
|
|
241
|
+
if (/[{}]/.test(authority))
|
|
242
|
+
return fail("may not place a placeholder in the host");
|
|
243
|
+
if (authority.includes("@"))
|
|
244
|
+
return fail("may not carry userinfo");
|
|
245
|
+
if (authority.includes(":"))
|
|
246
|
+
return fail("may not carry a port");
|
|
247
|
+
const hostProblem = hostnameProblem(authority);
|
|
248
|
+
if (hostProblem)
|
|
249
|
+
return fail(`host ${hostProblem}`);
|
|
250
|
+
const tail = cut === -1 ? "" : rest.slice(cut);
|
|
251
|
+
const queryAt = tail.indexOf("?");
|
|
252
|
+
const pathPart = queryAt === -1 ? tail : tail.slice(0, queryAt);
|
|
253
|
+
const query = queryAt === -1 ? undefined : tail.slice(queryAt + 1);
|
|
254
|
+
const parts = [{ literal: `https://${authority}` }];
|
|
255
|
+
const names = new Set();
|
|
256
|
+
const placeholder = (name) => {
|
|
257
|
+
if (!LOWER_NAME.test(name))
|
|
258
|
+
return `placeholder '{${name}}' must be named [a-z][a-z0-9_]*`;
|
|
259
|
+
if (names.has(name))
|
|
260
|
+
return `placeholder '{${name}}' appears twice`;
|
|
261
|
+
names.add(name);
|
|
262
|
+
parts.push({ name });
|
|
263
|
+
return undefined;
|
|
264
|
+
};
|
|
265
|
+
const decoded = (text) => {
|
|
266
|
+
try {
|
|
267
|
+
return decodeURIComponent(text);
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
if (pathPart !== "") {
|
|
274
|
+
for (const segment of pathPart.slice(1).split("/")) {
|
|
275
|
+
parts.push({ literal: "/" });
|
|
276
|
+
const match = /^([^{}]*)\{([^{}]*)\}([^{}]*)$/.exec(segment);
|
|
277
|
+
const literals = match ? [match[1], match[3]] : [segment];
|
|
278
|
+
const plain = literals.map(decoded);
|
|
279
|
+
if (plain.some((text) => text === undefined))
|
|
280
|
+
return fail("holds a malformed percent-encoding");
|
|
281
|
+
if (!match && (plain[0] === "." || plain[0] === ".."))
|
|
282
|
+
return fail("may not carry a dot segment, plain or percent-encoded");
|
|
283
|
+
if (plain.some((text) => /[/\\]/.test(text)))
|
|
284
|
+
return fail("may not encode a path separator");
|
|
285
|
+
if (match) {
|
|
286
|
+
parts.push({ literal: match[1] });
|
|
287
|
+
const problem = placeholder(match[2]);
|
|
288
|
+
if (problem)
|
|
289
|
+
return fail(problem);
|
|
290
|
+
parts.push({ literal: match[3] });
|
|
291
|
+
}
|
|
292
|
+
else if (/[{}]/.test(segment))
|
|
293
|
+
return fail("allows one placeholder per path segment");
|
|
294
|
+
else
|
|
295
|
+
parts.push({ literal: segment });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (query !== undefined) {
|
|
299
|
+
parts.push({ literal: "?" });
|
|
300
|
+
const pieces = query.split("&");
|
|
301
|
+
for (const [index, piece] of pieces.entries()) {
|
|
302
|
+
if (index > 0)
|
|
303
|
+
parts.push({ literal: "&" });
|
|
304
|
+
const eq = piece.indexOf("=");
|
|
305
|
+
const key = eq === -1 ? piece : piece.slice(0, eq);
|
|
306
|
+
const value = eq === -1 ? "" : piece.slice(eq + 1);
|
|
307
|
+
if (/[{}]/.test(key))
|
|
308
|
+
return fail("allows a placeholder only as a whole query value");
|
|
309
|
+
parts.push({ literal: eq === -1 ? key : `${key}=` });
|
|
310
|
+
const whole = /^\{([^{}]*)\}$/.exec(value);
|
|
311
|
+
if (whole) {
|
|
312
|
+
const problem = placeholder(whole[1]);
|
|
313
|
+
if (problem)
|
|
314
|
+
return fail(problem);
|
|
315
|
+
}
|
|
316
|
+
else if (/[{}]/.test(value))
|
|
317
|
+
return fail("allows a placeholder only as a whole query value");
|
|
318
|
+
else
|
|
319
|
+
parts.push({ literal: value });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const matcher = new RegExp(`^${parts.map((p) => ("name" in p ? `(${PARAMETER})` : escapeRegExp(p.literal))).join("")}$`);
|
|
323
|
+
return { ok: true, value: { parts, matcher } };
|
|
324
|
+
}
|
|
325
|
+
/** A record path: segments `[A-Za-z0-9_-]+` joined by dots, none a prototype name. */
|
|
326
|
+
function pathProblem(path) {
|
|
327
|
+
if (typeof path !== "string" || path === "" || path.length > 512)
|
|
328
|
+
return "must be a dotted record path";
|
|
329
|
+
for (const segment of path.split(".")) {
|
|
330
|
+
if (!PATH_SEGMENT.test(segment))
|
|
331
|
+
return "takes segments of letters, digits, '_' and '-' joined by '.'";
|
|
332
|
+
if (PROTOTYPE_KEYS.has(segment))
|
|
333
|
+
return `may not name '${segment}'`;
|
|
334
|
+
}
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
/** An id template over record paths with at least one placeholder. */
|
|
338
|
+
function parseIdTemplate(template) {
|
|
339
|
+
if (typeof template !== "string" || template === "" || template.length > MAX_TEMPLATE)
|
|
340
|
+
return { ok: false, reason: "must be a template string" };
|
|
341
|
+
const parts = [];
|
|
342
|
+
const paths = [];
|
|
343
|
+
const re = /\{([^{}]*)\}/g;
|
|
344
|
+
let last = 0;
|
|
345
|
+
for (const match of template.matchAll(re)) {
|
|
346
|
+
parts.push({ literal: template.slice(last, match.index) });
|
|
347
|
+
const problem = pathProblem(match[1]);
|
|
348
|
+
if (problem)
|
|
349
|
+
return { ok: false, reason: `placeholder '{${match[1]}}' ${problem}` };
|
|
350
|
+
parts.push({ path: match[1] });
|
|
351
|
+
paths.push(match[1]);
|
|
352
|
+
last = match.index + match[0].length;
|
|
353
|
+
}
|
|
354
|
+
parts.push({ literal: template.slice(last) });
|
|
355
|
+
if (parts.some((p) => "literal" in p && /[{}]/.test(p.literal)))
|
|
356
|
+
return { ok: false, reason: "has an unbalanced brace" };
|
|
357
|
+
if (paths.length === 0)
|
|
358
|
+
return { ok: false, reason: "needs at least one {record.path} placeholder" };
|
|
359
|
+
// Between two placeholders a literal must separate them, or two records could render one id.
|
|
360
|
+
if (parts.some((p, i) => "literal" in p && p.literal === "" && i > 0 && i < parts.length - 1)) {
|
|
361
|
+
return { ok: false, reason: "needs literal text between placeholders" };
|
|
362
|
+
}
|
|
363
|
+
return { ok: true, parts, paths };
|
|
364
|
+
}
|
|
365
|
+
// ----------------------------------------------------------------------------------------- parser
|
|
366
|
+
class Errors {
|
|
367
|
+
list = [];
|
|
368
|
+
add(code, path, message) {
|
|
369
|
+
this.list.push({ code, path: pointer(path), message });
|
|
370
|
+
}
|
|
371
|
+
closed(block, value, at) {
|
|
372
|
+
const accepted = CONNECTOR_KEYS[block];
|
|
373
|
+
for (const key of Object.keys(value)) {
|
|
374
|
+
if (!accepted.includes(key))
|
|
375
|
+
this.add("CONNECTOR_UNKNOWN_KEY", [...at, key], `'${key}' is not a ${block} key (accepted: ${accepted.join(", ")})`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function list(value, at, errors, what) {
|
|
380
|
+
if (value === undefined || value === null)
|
|
381
|
+
return [];
|
|
382
|
+
if (!Array.isArray(value)) {
|
|
383
|
+
errors.add("CONNECTOR_MALFORMED", at, `${what} must be a list`);
|
|
384
|
+
return undefined;
|
|
385
|
+
}
|
|
386
|
+
return value;
|
|
387
|
+
}
|
|
388
|
+
function parseAuth(raw, errors) {
|
|
389
|
+
if (raw === undefined) {
|
|
390
|
+
errors.add("CONNECTOR_MALFORMED", ["auth"], "auth is required");
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
|
393
|
+
if (!record(raw)) {
|
|
394
|
+
errors.add("CONNECTOR_MALFORMED", ["auth"], "auth must be a mapping");
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
errors.closed("auth", raw, ["auth"]);
|
|
398
|
+
const kind = raw.kind;
|
|
399
|
+
if (!AUTH_KINDS.includes(kind)) {
|
|
400
|
+
errors.add("CONNECTOR_AUTH_INVALID", ["auth", "kind"], `auth.kind must be one of ${AUTH_KINDS.join(", ")}`);
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
403
|
+
const before = errors.list.length;
|
|
404
|
+
for (const key of ["provider", "permissions", "scheme"]) {
|
|
405
|
+
if (key in raw && !AUTH_KEYS_BY_KIND[kind].includes(key))
|
|
406
|
+
errors.add("CONNECTOR_AUTH_INVALID", ["auth", key], `'${key}' belongs to another auth kind than ${kind}`);
|
|
407
|
+
}
|
|
408
|
+
const auth = { kind };
|
|
409
|
+
if (kind === "token" || kind === "oauth2") {
|
|
410
|
+
if (typeof raw.provider !== "string" || !Object.hasOwn(AUTH_PROVIDERS, raw.provider)) {
|
|
411
|
+
errors.add("CONNECTOR_AUTH_INVALID", ["auth", "provider"], `${kind} needs a provider core's table names (${Object.keys(AUTH_PROVIDERS).join(", ")})`);
|
|
412
|
+
}
|
|
413
|
+
else
|
|
414
|
+
auth.provider = raw.provider;
|
|
415
|
+
}
|
|
416
|
+
else if (kind === "github-app") {
|
|
417
|
+
const permissions = {};
|
|
418
|
+
if (raw.permissions !== undefined && !record(raw.permissions))
|
|
419
|
+
errors.add("CONNECTOR_AUTH_INVALID", ["auth", "permissions"], "permissions must be a mapping");
|
|
420
|
+
else
|
|
421
|
+
for (const [name, access] of Object.entries(raw.permissions ?? {})) {
|
|
422
|
+
if (!LOWER_NAME.test(name) || PROTOTYPE_KEYS.has(name))
|
|
423
|
+
errors.add("CONNECTOR_AUTH_INVALID", ["auth", "permissions", name], "a permission name is [a-z][a-z0-9_]*");
|
|
424
|
+
else if (access !== "read" && access !== "write")
|
|
425
|
+
errors.add("CONNECTOR_AUTH_INVALID", ["auth", "permissions", name], "a permission is read or write");
|
|
426
|
+
else
|
|
427
|
+
permissions[name] = access;
|
|
428
|
+
}
|
|
429
|
+
auth.permissions = permissions;
|
|
430
|
+
}
|
|
431
|
+
else if (kind === "signed-webhook") {
|
|
432
|
+
if (raw.scheme !== "hmac-sha256")
|
|
433
|
+
errors.add("CONNECTOR_AUTH_INVALID", ["auth", "scheme"], "signed-webhook takes scheme hmac-sha256");
|
|
434
|
+
else
|
|
435
|
+
auth.scheme = "hmac-sha256";
|
|
436
|
+
}
|
|
437
|
+
return errors.list.length === before ? auth : undefined;
|
|
438
|
+
}
|
|
439
|
+
function parseDomains(raw, auth, errors) {
|
|
440
|
+
const entries = list(raw, ["domains"], errors, "domains") ?? [];
|
|
441
|
+
const seen = new Set();
|
|
442
|
+
const credential = new Set(auth ? credentialHosts(auth) : []);
|
|
443
|
+
entries.forEach((entry, index) => {
|
|
444
|
+
const problem = hostnameProblem(entry);
|
|
445
|
+
if (problem)
|
|
446
|
+
errors.add("CONNECTOR_DOMAIN_INVALID", ["domains", index], `a domain ${problem}`);
|
|
447
|
+
else if (seen.has(entry))
|
|
448
|
+
errors.add("CONNECTOR_DOMAIN_INVALID", ["domains", index], `domain '${entry}' is listed twice`);
|
|
449
|
+
else if (credential.has(entry))
|
|
450
|
+
errors.add("CONNECTOR_DOMAIN_INVALID", ["domains", index], `domain '${entry}' is already a credential host of auth kind ${auth.kind}`);
|
|
451
|
+
seen.add(entry);
|
|
452
|
+
});
|
|
453
|
+
return [...seen];
|
|
454
|
+
}
|
|
455
|
+
/** Why a target field can never be a mapping key, whatever the Kind declares. */
|
|
456
|
+
function neverOwnedReason(field) {
|
|
457
|
+
if (NEVER_OWNED.has(field))
|
|
458
|
+
return "a field no source may own";
|
|
459
|
+
if (isManagedDocumentMetadataField(field))
|
|
460
|
+
return "managed document metadata";
|
|
461
|
+
if (field === PROGRESS_STATUS_FIELD || workStateFields(undefined, []).has(field))
|
|
462
|
+
return "work state";
|
|
463
|
+
if (field === "body")
|
|
464
|
+
return "the document body";
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
function parseMapping(field, raw, at, errors) {
|
|
468
|
+
const before = errors.list.length;
|
|
469
|
+
if (!FIELD_NAME.test(field) || PROTOTYPE_KEYS.has(field)) {
|
|
470
|
+
errors.add("CONNECTOR_FIELD_INVALID", at, `mapping key '${field}' must be a field name [A-Za-z][A-Za-z0-9_]*`);
|
|
471
|
+
return undefined;
|
|
472
|
+
}
|
|
473
|
+
const reason = neverOwnedReason(field);
|
|
474
|
+
if (reason)
|
|
475
|
+
errors.add("CONNECTOR_FIELD_NEVER_OWNED", at, `'${field}' is ${reason} and can never be mapped`);
|
|
476
|
+
const entry = typeof raw === "string" ? { from: raw } : raw;
|
|
477
|
+
if (!record(entry)) {
|
|
478
|
+
errors.add("CONNECTOR_MALFORMED", at, "a mapping is a record path or { from, values, unmapped }");
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
if (typeof raw !== "string")
|
|
482
|
+
errors.closed("mapping", entry, at);
|
|
483
|
+
const problem = pathProblem(entry.from);
|
|
484
|
+
if (problem)
|
|
485
|
+
errors.add("CONNECTOR_FIELD_INVALID", [...at, ...(typeof raw === "string" ? [] : ["from"])], `from ${problem}`);
|
|
486
|
+
const mapping = { from: entry.from };
|
|
487
|
+
if (entry.values !== undefined) {
|
|
488
|
+
if (!record(entry.values) || Object.keys(entry.values).length === 0)
|
|
489
|
+
errors.add("CONNECTOR_VALUES_INVALID", [...at, "values"], "values must be a non-empty mapping of native value to target value");
|
|
490
|
+
else {
|
|
491
|
+
const values = {};
|
|
492
|
+
for (const [native, target] of Object.entries(entry.values)) {
|
|
493
|
+
if (PROTOTYPE_KEYS.has(native) || typeof target !== "string" || target === "")
|
|
494
|
+
errors.add("CONNECTOR_VALUES_INVALID", [...at, "values", native], "each values row maps a native string to a non-empty target string");
|
|
495
|
+
else
|
|
496
|
+
values[native] = target;
|
|
497
|
+
}
|
|
498
|
+
mapping.values = values;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (entry.unmapped !== undefined && entry.values === undefined)
|
|
502
|
+
errors.add("CONNECTOR_VALUES_INVALID", [...at, "unmapped"], "unmapped needs a values table");
|
|
503
|
+
else if (entry.unmapped !== undefined && entry.unmapped !== "keep" && entry.unmapped !== "refuse")
|
|
504
|
+
errors.add("CONNECTOR_VALUES_INVALID", [...at, "unmapped"], "unmapped is keep or refuse");
|
|
505
|
+
else if (entry.values !== undefined)
|
|
506
|
+
mapping.unmapped = entry.unmapped ?? "keep";
|
|
507
|
+
return errors.list.length === before ? mapping : undefined;
|
|
508
|
+
}
|
|
509
|
+
function parseSource(raw, index, errors) {
|
|
510
|
+
const at = ["record_sources", index];
|
|
511
|
+
if (!record(raw)) {
|
|
512
|
+
errors.add("CONNECTOR_MALFORMED", at, "a source must be a mapping");
|
|
513
|
+
return undefined;
|
|
514
|
+
}
|
|
515
|
+
const before = errors.list.length;
|
|
516
|
+
errors.closed("source", raw, at);
|
|
517
|
+
if (typeof raw.name !== "string" || !SLUG.test(raw.name))
|
|
518
|
+
errors.add("CONNECTOR_MALFORMED", [...at, "name"], "a source name is a slug [a-z][a-z0-9-]*");
|
|
519
|
+
if (typeof raw.profile !== "string" || !SLUG.test(raw.profile))
|
|
520
|
+
errors.add("CONNECTOR_PROFILE_INVALID", [...at, "profile"], "profile is a slug [a-z][a-z0-9-]* naming a catalog profile");
|
|
521
|
+
if (typeof raw.target_kind !== "string" || raw.target_kind.trim() === "")
|
|
522
|
+
errors.add("CONNECTOR_TARGET_KIND_INVALID", [...at, "target_kind"], "target_kind must name a Kind");
|
|
523
|
+
else if (RESERVED_TARGET_KINDS.has(raw.target_kind))
|
|
524
|
+
errors.add("CONNECTOR_TARGET_KIND_INVALID", [...at, "target_kind"], `a source may not target ${raw.target_kind}`);
|
|
525
|
+
if (raw.mode !== undefined && typeof raw.mode !== "string")
|
|
526
|
+
errors.add("CONNECTOR_MALFORMED", [...at, "mode"], "mode must be a string");
|
|
527
|
+
else if (raw.mode !== undefined && raw.mode !== "overlay") {
|
|
528
|
+
errors.add("CONNECTOR_MODE_UNSUPPORTED", [...at, "mode"], raw.mode === "mirror" ? "mirror mode is not supported before I4; use overlay" : "mode is overlay");
|
|
529
|
+
}
|
|
530
|
+
if (raw.completeness !== "complete" && raw.completeness !== "partial")
|
|
531
|
+
errors.add("CONNECTOR_MALFORMED", [...at, "completeness"], "completeness is complete or partial");
|
|
532
|
+
const fields = {};
|
|
533
|
+
if (!record(raw.fields) || Object.keys(raw.fields).length === 0)
|
|
534
|
+
errors.add("CONNECTOR_MALFORMED", [...at, "fields"], "fields must map at least one target field");
|
|
535
|
+
else
|
|
536
|
+
for (const [field, entry] of Object.entries(raw.fields)) {
|
|
537
|
+
const mapping = parseMapping(field, entry, [...at, "fields", field], errors);
|
|
538
|
+
if (mapping)
|
|
539
|
+
fields[field] = mapping;
|
|
540
|
+
}
|
|
541
|
+
let identity;
|
|
542
|
+
if (!record(raw.identity))
|
|
543
|
+
errors.add("CONNECTOR_MALFORMED", [...at, "identity"], "identity must be a mapping of resource and id");
|
|
544
|
+
else {
|
|
545
|
+
errors.closed("identity", raw.identity, [...at, "identity"]);
|
|
546
|
+
const resource = parseResourceTemplate(raw.identity.resource);
|
|
547
|
+
if (!resource.ok)
|
|
548
|
+
errors.add("CONNECTOR_IDENTITY_UNSTABLE", [...at, "identity", "resource"], `identity.resource ${resource.reason}`);
|
|
549
|
+
const id = parseIdTemplate(raw.identity.id);
|
|
550
|
+
if (!id.ok)
|
|
551
|
+
errors.add("CONNECTOR_IDENTITY_UNSTABLE", [...at, "identity", "id"], `identity.id ${id.reason}`);
|
|
552
|
+
else {
|
|
553
|
+
const owned = Object.values(fields).map((m) => m.from);
|
|
554
|
+
const shared = id.paths.filter((p) => owned.includes(p));
|
|
555
|
+
if (shared.length)
|
|
556
|
+
errors.add("CONNECTOR_IDENTITY_UNSTABLE", [...at, "identity", "id"], `identity is never an owned value, but ${shared.join(", ")} is also mapped`);
|
|
557
|
+
}
|
|
558
|
+
if (resource.ok && id.ok)
|
|
559
|
+
identity = { resource: raw.identity.resource, id: raw.identity.id };
|
|
560
|
+
}
|
|
561
|
+
if (errors.list.length !== before || !identity)
|
|
562
|
+
return undefined;
|
|
563
|
+
return {
|
|
564
|
+
name: raw.name, profile: raw.profile, target_kind: raw.target_kind, mode: "overlay",
|
|
565
|
+
identity, fields, completeness: raw.completeness,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function parseAction(raw, index, reach, errors) {
|
|
569
|
+
const at = ["actions", index];
|
|
570
|
+
if (!record(raw)) {
|
|
571
|
+
errors.add("CONNECTOR_MALFORMED", at, "an action must be a mapping");
|
|
572
|
+
return undefined;
|
|
573
|
+
}
|
|
574
|
+
const before = errors.list.length;
|
|
575
|
+
errors.closed("action", raw, at);
|
|
576
|
+
if (typeof raw.name !== "string" || !SLUG.test(raw.name))
|
|
577
|
+
errors.add("CONNECTOR_MALFORMED", [...at, "name"], "an action name is a slug [a-z][a-z0-9-]*");
|
|
578
|
+
if (typeof raw.method !== "string" || !METHODS.has(raw.method))
|
|
579
|
+
errors.add("CONNECTOR_MALFORMED", [...at, "method"], `method is one of ${[...METHODS].join(", ")}`);
|
|
580
|
+
if (hostnameProblem(raw.host) || !reach.has(raw.host))
|
|
581
|
+
errors.add("CONNECTOR_ACTION_INVALID", [...at, "host"], "an action host must be a credential host of the auth kind or a declared domain");
|
|
582
|
+
if (raw.confirm !== undefined && raw.confirm !== "always" && raw.confirm !== "never")
|
|
583
|
+
errors.add("CONNECTOR_MALFORMED", [...at, "confirm"], "confirm is always or never");
|
|
584
|
+
const input = {};
|
|
585
|
+
if (raw.input !== undefined && !record(raw.input))
|
|
586
|
+
errors.add("CONNECTOR_MALFORMED", [...at, "input"], "input must be a mapping");
|
|
587
|
+
else
|
|
588
|
+
for (const [name, spec] of Object.entries(raw.input ?? {})) {
|
|
589
|
+
const where = [...at, "input", name];
|
|
590
|
+
if (!LOWER_NAME.test(name) || PROTOTYPE_KEYS.has(name)) {
|
|
591
|
+
errors.add("CONNECTOR_ACTION_INVALID", where, "an input name is [a-z][a-z0-9_]*");
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
if (!record(spec)) {
|
|
595
|
+
errors.add("CONNECTOR_MALFORMED", where, "an input is { type, required, values }");
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
errors.closed("input", spec, where);
|
|
599
|
+
if (typeof spec.type !== "string" || !INPUT_TYPES.has(spec.type))
|
|
600
|
+
errors.add("CONNECTOR_MALFORMED", [...where, "type"], `an input type is one of ${[...INPUT_TYPES].join(", ")}`);
|
|
601
|
+
if (spec.required !== undefined && typeof spec.required !== "boolean")
|
|
602
|
+
errors.add("CONNECTOR_MALFORMED", [...where, "required"], "required is true or false");
|
|
603
|
+
if (spec.values !== undefined && (!Array.isArray(spec.values) || !spec.values.every((v) => typeof v === "string" && v !== ""))) {
|
|
604
|
+
errors.add("CONNECTOR_MALFORMED", [...where, "values"], "values is a list of strings");
|
|
605
|
+
}
|
|
606
|
+
else if (spec.values !== undefined && spec.type !== "string")
|
|
607
|
+
errors.add("CONNECTOR_ACTION_INVALID", [...where, "values"], "values restrict a string input only");
|
|
608
|
+
input[name] = {
|
|
609
|
+
type: spec.type, required: spec.required === true,
|
|
610
|
+
...(Array.isArray(spec.values) ? { values: [...spec.values] } : {}),
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
if (errors.list.length !== before)
|
|
614
|
+
return undefined;
|
|
615
|
+
return { name: raw.name, method: raw.method, host: raw.host, input, confirm: raw.confirm ?? "always" };
|
|
616
|
+
}
|
|
617
|
+
function parseEvent(raw, index, sourceNames, errors) {
|
|
618
|
+
const at = ["events", index];
|
|
619
|
+
if (!record(raw)) {
|
|
620
|
+
errors.add("CONNECTOR_MALFORMED", at, "an event must be a mapping");
|
|
621
|
+
return undefined;
|
|
622
|
+
}
|
|
623
|
+
const before = errors.list.length;
|
|
624
|
+
errors.closed("event", raw, at);
|
|
625
|
+
if (typeof raw.name !== "string" || !SLUG.test(raw.name))
|
|
626
|
+
errors.add("CONNECTOR_MALFORMED", [...at, "name"], "an event name is a slug [a-z][a-z0-9-]*");
|
|
627
|
+
const signature = raw.signature;
|
|
628
|
+
if (!record(signature))
|
|
629
|
+
errors.add("CONNECTOR_MALFORMED", [...at, "signature"], "signature must be a mapping of scheme and header");
|
|
630
|
+
else {
|
|
631
|
+
errors.closed("signature", signature, [...at, "signature"]);
|
|
632
|
+
if (signature.scheme !== "hmac-sha256")
|
|
633
|
+
errors.add("CONNECTOR_EVENT_INVALID", [...at, "signature", "scheme"], "signature.scheme is hmac-sha256");
|
|
634
|
+
if (typeof signature.header !== "string" || !HEADER.test(signature.header))
|
|
635
|
+
errors.add("CONNECTOR_EVENT_INVALID", [...at, "signature", "header"], "signature.header is a lowercase token [a-z0-9-]+");
|
|
636
|
+
}
|
|
637
|
+
const references = list(raw.references, [...at, "references"], errors, "references") ?? [];
|
|
638
|
+
const seen = new Set();
|
|
639
|
+
references.forEach((ref, i) => {
|
|
640
|
+
if (typeof ref !== "string" || !sourceNames.has(ref))
|
|
641
|
+
errors.add("CONNECTOR_EVENT_INVALID", [...at, "references", i], "references name declared sources");
|
|
642
|
+
else if (seen.has(ref))
|
|
643
|
+
errors.add("CONNECTOR_DUPLICATE", [...at, "references", i], `reference '${ref}' is listed twice`);
|
|
644
|
+
else
|
|
645
|
+
seen.add(ref);
|
|
646
|
+
});
|
|
647
|
+
if (errors.list.length !== before || !record(signature))
|
|
648
|
+
return undefined;
|
|
649
|
+
return { name: raw.name, signature: { scheme: "hmac-sha256", header: signature.header }, references: [...seen] };
|
|
650
|
+
}
|
|
651
|
+
function duplicates(items, block, what, errors) {
|
|
652
|
+
const seen = new Set();
|
|
653
|
+
items.forEach((item, index) => {
|
|
654
|
+
const name = record(item) ? item.name : undefined;
|
|
655
|
+
if (typeof name !== "string")
|
|
656
|
+
return;
|
|
657
|
+
if (seen.has(name))
|
|
658
|
+
errors.add("CONNECTOR_DUPLICATE", [block, index, "name"], `${what} '${name}' is declared twice`);
|
|
659
|
+
seen.add(name);
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
/** Kind rules for one source: ownable, declared, scalar, and enum-safe mapping keys. */
|
|
663
|
+
function sourceKindErrors(source, index, kind) {
|
|
664
|
+
const errors = new Errors();
|
|
665
|
+
const work = workStateFields(kind, ["0.1", "0.2"]);
|
|
666
|
+
const declared = new Set([...kind.fields.required, ...kind.fields.optional, "title"]);
|
|
667
|
+
for (const [field, mapping] of Object.entries(source.fields)) {
|
|
668
|
+
const at = ["record_sources", index, "fields", field];
|
|
669
|
+
if (work.has(field)) {
|
|
670
|
+
errors.add("CONNECTOR_FIELD_NEVER_OWNED", at, `'${field}' holds the work state of ${kind.governs} and can never be mapped`);
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
if (!declared.has(field)) {
|
|
674
|
+
errors.add("CONNECTOR_FIELD_UNDECLARED", at, `${kind.governs} does not declare '${field}'`);
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
const allowed = Object.hasOwn(kind.fields.values, field) ? kind.fields.values[field] : undefined;
|
|
678
|
+
if (isKindListField(kind, field)) {
|
|
679
|
+
// Core's replace-all refuses an OKF standard or managed field whatever the Kind types it as.
|
|
680
|
+
if (isManagedDocumentMetadataField(field) || isStandardDocumentSetField(field, "0.1") || isStandardDocumentSetField(field, "0.2")) {
|
|
681
|
+
errors.add("CONNECTOR_LIST_FIELD_UNSUPPORTED", at, `'${field}' is an OKF standard or managed field; a connector cannot own it as a list`);
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
// A list maps whole: the profile flattens it, and there is no per-item value table yet.
|
|
685
|
+
if (mapping.values || Array.isArray(allowed))
|
|
686
|
+
errors.add("CONNECTOR_LIST_FIELD_UNSUPPORTED", at, `'${field}' is a ${kind.fields.types[field]}; a list field maps whole lists, without a values table or allowed values`);
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (!Array.isArray(allowed))
|
|
690
|
+
continue;
|
|
691
|
+
if (!mapping.values) {
|
|
692
|
+
errors.add("CONNECTOR_VALUES_INVALID", at, `'${field}' is an enum of ${kind.governs}; map it through a values table`);
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
for (const [native, target] of Object.entries(mapping.values)) {
|
|
696
|
+
if (!allowed.includes(target))
|
|
697
|
+
errors.add("CONNECTOR_VALUES_INVALID", [...at, "values", native], `${kind.governs}.${field} does not allow '${target}'`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return errors.list;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Parse one Connector document, returning every refusal found. With `ctx.kinds`, a source whose
|
|
704
|
+
* target Kind is in the registry is also checked against it; one whose Kind is absent is left to
|
|
705
|
+
* {@link checkConnectorAgainstKinds} with a warning. A secret-shaped key or value, or a YAML alias,
|
|
706
|
+
* refuses the document alone, so no other refusal can echo the one or expand the other.
|
|
707
|
+
*/
|
|
708
|
+
export function parseConnectorDoc(doc, ctx = {}) {
|
|
709
|
+
const fm = (doc.frontmatter ?? {});
|
|
710
|
+
const secrets = new Map();
|
|
711
|
+
scanDocument(fm, [], secrets, new WeakSet());
|
|
712
|
+
if (typeof doc.body === "string" && bodyHoldsSecret(doc.body)) {
|
|
713
|
+
secrets.set("(body)", { code: "CONNECTOR_SECRET_PRESENT", path: "", message: "a secret-shaped token in the body; a declaration never carries a credential" });
|
|
714
|
+
}
|
|
715
|
+
if (secrets.size)
|
|
716
|
+
return { ok: false, errors: [...secrets.values()] };
|
|
717
|
+
const errors = new Errors();
|
|
718
|
+
if (typeof doc.id !== "string" || !doc.id.startsWith(CONNECTORS_PREFIX) || !DOC_SLUG.test(doc.id.slice(CONNECTORS_PREFIX.length))) {
|
|
719
|
+
errors.add("CONNECTOR_MALFORMED", [], `a Connector lives at ${CONNECTORS_PREFIX}<slug>`);
|
|
720
|
+
}
|
|
721
|
+
if (fm.type !== CONNECTOR_TYPE)
|
|
722
|
+
errors.add("CONNECTOR_MALFORMED", ["type"], `type must be ${CONNECTOR_TYPE}`);
|
|
723
|
+
if (typeof fm.title !== "string" || fm.title.trim() === "")
|
|
724
|
+
errors.add("CONNECTOR_MALFORMED", ["title"], "title is required");
|
|
725
|
+
if (fm.description !== undefined && typeof fm.description !== "string")
|
|
726
|
+
errors.add("CONNECTOR_MALFORMED", ["description"], "description must be a string");
|
|
727
|
+
if (fm.version !== undefined && typeof fm.version !== "string" && !(typeof fm.version === "number" && Number.isFinite(fm.version))) {
|
|
728
|
+
errors.add("CONNECTOR_MALFORMED", ["version"], "version must be a string");
|
|
729
|
+
}
|
|
730
|
+
const auth = parseAuth(fm.auth, errors);
|
|
731
|
+
const domains = parseDomains(fm.domains, auth, errors);
|
|
732
|
+
const rawSources = list(fm.record_sources, ["record_sources"], errors, "record_sources");
|
|
733
|
+
const rawActions = list(fm.actions, ["actions"], errors, "actions");
|
|
734
|
+
const rawEvents = list(fm.events, ["events"], errors, "events");
|
|
735
|
+
if (rawSources && rawActions && rawEvents && rawSources.length + rawActions.length + rawEvents.length === 0) {
|
|
736
|
+
errors.add("CONNECTOR_MALFORMED", [], "a Connector declares at least one source, action or event");
|
|
737
|
+
}
|
|
738
|
+
duplicates(rawSources ?? [], "record_sources", "source", errors);
|
|
739
|
+
duplicates(rawActions ?? [], "actions", "action", errors);
|
|
740
|
+
duplicates(rawEvents ?? [], "events", "event", errors);
|
|
741
|
+
const sources = (rawSources ?? []).map((raw, index) => parseSource(raw, index, errors));
|
|
742
|
+
const reach = new Set([...(auth ? credentialHosts(auth) : []), ...domains]);
|
|
743
|
+
const actions = (rawActions ?? []).map((raw, index) => parseAction(raw, index, reach, errors));
|
|
744
|
+
const names = new Set((rawSources ?? []).flatMap((raw) => (record(raw) && typeof raw.name === "string" ? [raw.name] : [])));
|
|
745
|
+
const events = (rawEvents ?? []).map((raw, index) => parseEvent(raw, index, names, errors));
|
|
746
|
+
const warnings = [];
|
|
747
|
+
if (ctx.kinds) {
|
|
748
|
+
sources.forEach((source, index) => {
|
|
749
|
+
if (!source)
|
|
750
|
+
return;
|
|
751
|
+
const kind = ctx.kinds.kinds.get(source.target_kind);
|
|
752
|
+
if (kind)
|
|
753
|
+
errors.list.push(...sourceKindErrors(source, index, kind));
|
|
754
|
+
else
|
|
755
|
+
warnings.push({ code: "CONNECTOR_TARGET_KIND_DEFERRED", message: `source '${source.name}' targets ${source.target_kind}, which the given registry does not declare; its Kind rules run against the bundle`, field: pointer(["record_sources", index, "target_kind"]), severity: "warning" });
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
if (errors.list.length || !auth)
|
|
759
|
+
return { ok: false, errors: errors.list };
|
|
760
|
+
const connector = {
|
|
761
|
+
id: doc.id,
|
|
762
|
+
title: fm.title,
|
|
763
|
+
...(typeof fm.description === "string" ? { description: fm.description } : {}),
|
|
764
|
+
...(fm.version !== undefined ? { version: String(fm.version) } : {}),
|
|
765
|
+
auth,
|
|
766
|
+
domains,
|
|
767
|
+
sources: sources,
|
|
768
|
+
actions: actions,
|
|
769
|
+
events: events,
|
|
770
|
+
};
|
|
771
|
+
return { ok: true, connector, warnings };
|
|
772
|
+
}
|
|
773
|
+
/** The Kind rules over a bundle's registry; a source whose target Kind is not declared is refused. */
|
|
774
|
+
export function checkConnectorAgainstKinds(connector, kinds) {
|
|
775
|
+
return connector.sources.flatMap((source, index) => {
|
|
776
|
+
const kind = kinds.kinds.get(source.target_kind);
|
|
777
|
+
if (!kind)
|
|
778
|
+
return [{ code: "CONNECTOR_TARGET_KIND_INVALID", path: pointer(["record_sources", index, "target_kind"]), message: `${source.target_kind} is not declared in this bundle` }];
|
|
779
|
+
return sourceKindErrors(source, index, kind);
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
// --------------------------------------------------------------------------------------------- pin
|
|
783
|
+
function pinBlock(block, value, nested = {}) {
|
|
784
|
+
const source = value;
|
|
785
|
+
const out = {};
|
|
786
|
+
for (const key of CONNECTOR_KEYS[block]) {
|
|
787
|
+
const v = source[key];
|
|
788
|
+
out[key] = v !== undefined && nested[key] ? nested[key](v) : v;
|
|
789
|
+
}
|
|
790
|
+
return out;
|
|
791
|
+
}
|
|
792
|
+
const byName = (items) => [...items].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
793
|
+
const mapOf = (block, defaults = (item) => item) => (value) => Object.fromEntries(Object.entries(value).map(([key, item]) => [key, pinBlock(block, defaults(item))]));
|
|
794
|
+
/** The parser's defaults, filled again here so a Connector built without the parser pins the same. */
|
|
795
|
+
const PIN_DEFAULTS = {
|
|
796
|
+
auth: (a) => (a.kind === "github-app" && a.permissions === undefined ? { ...a, permissions: {} } : a),
|
|
797
|
+
source: (s) => ({ ...s, mode: s.mode ?? "overlay" }),
|
|
798
|
+
mapping: (m) => {
|
|
799
|
+
const mapping = m;
|
|
800
|
+
return mapping.values !== undefined && mapping.unmapped === undefined ? { ...mapping, unmapped: "keep" } : mapping;
|
|
801
|
+
},
|
|
802
|
+
action: (a) => ({ ...a, input: a.input ?? {}, confirm: a.confirm ?? "always" }),
|
|
803
|
+
input: (i) => ({ ...i, required: i.required ?? false }),
|
|
804
|
+
event: (e) => ({ ...e, references: e.references ?? [] }),
|
|
805
|
+
};
|
|
806
|
+
/**
|
|
807
|
+
* The digest a Connection records at enable time: every key {@link CONNECTOR_KEYS} accepts, with
|
|
808
|
+
* defaults filled and order-free lists sorted, plus the credential hosts core resolves the auth kind
|
|
809
|
+
* to, under the recipe stamp's canonical JSON. Blind to
|
|
810
|
+
* `title`, `description`, `version`, the body, `x_recipe` and unknown top-level keys.
|
|
811
|
+
*/
|
|
812
|
+
export function connectorPinDigest(connector) {
|
|
813
|
+
const pinned = {
|
|
814
|
+
rules: CONNECTOR_PIN_RULES,
|
|
815
|
+
auth: pinBlock("auth", PIN_DEFAULTS.auth(connector.auth)),
|
|
816
|
+
// Core's resolution of the provider, so a host joining a row changes every pin on that provider.
|
|
817
|
+
credential_hosts: [...credentialHosts(connector.auth)].sort(),
|
|
818
|
+
domains: [...connector.domains].sort(),
|
|
819
|
+
// The parsed sources, under the same label whichever document key they were read from.
|
|
820
|
+
sources: byName(connector.sources).map((source) => pinBlock("source", PIN_DEFAULTS.source(source), {
|
|
821
|
+
identity: (v) => pinBlock("identity", v),
|
|
822
|
+
fields: mapOf("mapping", PIN_DEFAULTS.mapping),
|
|
823
|
+
})),
|
|
824
|
+
actions: byName(connector.actions).map((action) => pinBlock("action", PIN_DEFAULTS.action(action), { input: mapOf("input", PIN_DEFAULTS.input) })),
|
|
825
|
+
events: byName(connector.events).map((event) => pinBlock("event", PIN_DEFAULTS.event(event), {
|
|
826
|
+
signature: (v) => pinBlock("signature", v),
|
|
827
|
+
references: (v) => [...v].sort(),
|
|
828
|
+
})),
|
|
829
|
+
};
|
|
830
|
+
return `sha256:${sha256Hex(definitionCanonicalJson(pinned))}`;
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Bind a Source Feed to one source of a Connector. The feed chooses only among the resources the
|
|
834
|
+
* identity template admits, within the mapping keys, under the source's own completeness and
|
|
835
|
+
* profile. Every refusal is `POLICY_INVALID` naming the feed and the key. `okfVersion` is the content
|
|
836
|
+
* bundle's edition; absent means v0.1, as an undeclared edition does, and an owned list field needs
|
|
837
|
+
* v0.2 because only field actions write lists.
|
|
838
|
+
*/
|
|
839
|
+
export function bindSourceFeed(connector, feed, kinds, options = {}) {
|
|
840
|
+
const fm = (feed.frontmatter ?? {});
|
|
841
|
+
const refuse = (key, message, details = {}) => {
|
|
842
|
+
throw new OverlayRefusal(REFUSAL_CODES.POLICY_INVALID, `${feed.id}: ${message}`, { feed: feed.id, key, ...details });
|
|
843
|
+
};
|
|
844
|
+
if (fm.connector !== connector.id)
|
|
845
|
+
refuse("connector", `connector must name ${connector.id}`);
|
|
846
|
+
const source = connector.sources.find((s) => s.name === fm.source);
|
|
847
|
+
if (!source)
|
|
848
|
+
refuse("source", `source must name a source of ${connector.id}`);
|
|
849
|
+
const bound = source;
|
|
850
|
+
for (const key of ["target_kind", "completeness", "profile"]) {
|
|
851
|
+
if (fm[key] !== undefined && fm[key] !== null && fm[key] !== bound[key])
|
|
852
|
+
refuse(key, `${key} must be the source's ${bound[key]} when present`);
|
|
853
|
+
}
|
|
854
|
+
if (typeof fm.url !== "string" || fm.url.length > MAX_TEMPLATE)
|
|
855
|
+
refuse("url", "url is required");
|
|
856
|
+
for (const key of ["owned_fields", "auto_apply", "needs_review"]) {
|
|
857
|
+
const v = fm[key];
|
|
858
|
+
if (v !== undefined && v !== null && typeof v !== "string" && !(Array.isArray(v) && v.every((item) => typeof item === "string")))
|
|
859
|
+
refuse(key, `${key} must be a list of field or change names`);
|
|
860
|
+
}
|
|
861
|
+
for (const key of ["missing_threshold", "exclude_pattern", "wall_time_zone"]) {
|
|
862
|
+
const v = fm[key];
|
|
863
|
+
if (v !== undefined && v !== null && typeof v !== "string" && typeof v !== "number")
|
|
864
|
+
refuse(key, `${key} must be a single value`);
|
|
865
|
+
}
|
|
866
|
+
const url = fm.url;
|
|
867
|
+
const template = parseResourceTemplate(bound.identity.resource);
|
|
868
|
+
if (!template.ok)
|
|
869
|
+
refuse("url", "the source's identity template is invalid");
|
|
870
|
+
const { parts, matcher } = template.value;
|
|
871
|
+
const match = matcher.exec(url);
|
|
872
|
+
if (!match)
|
|
873
|
+
refuse("url", "url is not a resource the source's identity template admits");
|
|
874
|
+
const parameters = {};
|
|
875
|
+
let group = 1;
|
|
876
|
+
for (const part of parts) {
|
|
877
|
+
if (!("name" in part))
|
|
878
|
+
continue;
|
|
879
|
+
const value = match[group++];
|
|
880
|
+
if (value === "." || value === "..")
|
|
881
|
+
refuse("url", `parameter ${part.name} may not be '.' or '..'`);
|
|
882
|
+
parameters[part.name] = value;
|
|
883
|
+
}
|
|
884
|
+
const resource = parts.map((part) => ("name" in part ? parameters[part.name] : part.literal)).join("");
|
|
885
|
+
if (resource !== url)
|
|
886
|
+
refuse("url", "url must equal the rendered identity template byte for byte");
|
|
887
|
+
// The policy reader owns the feed keys' grammar; the Kind-aware read follows the binding checks.
|
|
888
|
+
const owned = readOverlayPolicy(feed).owned_fields;
|
|
889
|
+
const outside = owned.filter((field) => !Object.hasOwn(bound.fields, field));
|
|
890
|
+
if (outside.length)
|
|
891
|
+
refuse("owned_fields", `owned_fields outside the source's mapping: ${outside.join(", ")}`, { fields: outside });
|
|
892
|
+
const kind = kinds.kinds.get(bound.target_kind);
|
|
893
|
+
if (!kind)
|
|
894
|
+
refuse("target_kind", `${bound.target_kind} is not declared in this bundle`);
|
|
895
|
+
const kindErrors = sourceKindErrors(bound, connector.sources.indexOf(bound), kind);
|
|
896
|
+
if (kindErrors.length)
|
|
897
|
+
refuse("connector", `the source does not fit ${bound.target_kind}: ${kindErrors.map((e) => e.code).join(", ")}`, { codes: kindErrors.map((e) => e.code) });
|
|
898
|
+
const policy = readOverlayPolicy(feed, { targetKind: kind });
|
|
899
|
+
const ownedLists = policy.owned_fields.filter((field) => policy.list_fields?.includes(field));
|
|
900
|
+
const edition = options.okfVersion ?? "0.1";
|
|
901
|
+
if (ownedLists.length && edition !== "0.2") {
|
|
902
|
+
refuse("owned_fields", `owned list fields need an OKF v0.2 bundle, and this bundle is v${edition}: ${ownedLists.join(", ")}`, { fields: ownedLists, okf_version: edition });
|
|
903
|
+
}
|
|
904
|
+
return { policy: { ...policy, target_kind: bound.target_kind, completeness: bound.completeness }, source: bound, kind: kind, parameters, resource };
|
|
905
|
+
}
|
|
906
|
+
/** Own properties only, through plain mappings, never a prototype name. */
|
|
907
|
+
function readPath(value, path) {
|
|
908
|
+
let current = value;
|
|
909
|
+
for (const segment of path.split(".")) {
|
|
910
|
+
if (PROTOTYPE_KEYS.has(segment) || !record(current) || !Object.hasOwn(current, segment))
|
|
911
|
+
return { found: false };
|
|
912
|
+
current = current[segment];
|
|
913
|
+
}
|
|
914
|
+
return current === undefined ? { found: false } : { found: true, value: current };
|
|
915
|
+
}
|
|
916
|
+
const MAX_ID = 2048;
|
|
917
|
+
/**
|
|
918
|
+
* Render the id template. A missing, blank or non-scalar value is `IDENTITY_MISSING`; a value holding
|
|
919
|
+
* any character of a literal that separates two placeholders (so two records could share one id, for
|
|
920
|
+
* multi-character separators too), or an id past {@link MAX_ID} characters, is `IDENTITY_INVALID`. A finite number renders as `String(n)`, so a
|
|
921
|
+
* profile emits one type per id.
|
|
922
|
+
*/
|
|
923
|
+
function renderId(template, value) {
|
|
924
|
+
const parsed = parseIdTemplate(template);
|
|
925
|
+
if (!parsed.ok)
|
|
926
|
+
return { ok: false, code: "IDENTITY_INVALID", path: template };
|
|
927
|
+
const separators = parsed.paths.length > 1
|
|
928
|
+
? parsed.parts.filter((p, i) => "literal" in p && i > 0 && i < parsed.parts.length - 1).map((p) => p.literal)
|
|
929
|
+
: [];
|
|
930
|
+
let id = "";
|
|
931
|
+
for (const part of parsed.parts) {
|
|
932
|
+
if ("literal" in part) {
|
|
933
|
+
id += part.literal;
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
const read = readPath(value, part.path);
|
|
937
|
+
const v = read.found ? read.value : undefined;
|
|
938
|
+
const text = typeof v === "string" ? v : typeof v === "number" && Number.isFinite(v) ? String(v) : undefined;
|
|
939
|
+
if (text === undefined || !isPresent(text))
|
|
940
|
+
return { ok: false, code: "IDENTITY_MISSING", path: part.path };
|
|
941
|
+
if (text.length > MAX_ID || separators.some((separator) => [...separator].some((ch) => text.includes(ch)))) {
|
|
942
|
+
return { ok: false, code: "IDENTITY_INVALID", path: part.path };
|
|
943
|
+
}
|
|
944
|
+
id += text;
|
|
945
|
+
}
|
|
946
|
+
if (id.length > MAX_ID)
|
|
947
|
+
return { ok: false, code: "IDENTITY_INVALID", path: template };
|
|
948
|
+
return { ok: true, id };
|
|
949
|
+
}
|
|
950
|
+
/** The record's identity: the bound resource and the id template rendered over the record. */
|
|
951
|
+
export function renderIdentity(source, bound, value) {
|
|
952
|
+
const id = renderId(source.identity.id, value);
|
|
953
|
+
if (!id.ok)
|
|
954
|
+
return { ok: false, diagnostic: { code: id.code, field: "identity", from: id.path, native: null, external_id: null } };
|
|
955
|
+
return { ok: true, resource: bound.resource, id: id.id };
|
|
956
|
+
}
|
|
957
|
+
const scalar = (v) => typeof v === "string" || typeof v === "boolean" || (typeof v === "number" && Number.isFinite(v));
|
|
958
|
+
/** A list of nonblank text, deduplicated and in code point order; undefined for anything else. */
|
|
959
|
+
function textSet(native) {
|
|
960
|
+
if (!Array.isArray(native) || !native.every((member) => typeof member === "string" && isPresent(member)))
|
|
961
|
+
return undefined;
|
|
962
|
+
return [...new Set(native)].sort(compareCodePoints);
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Map one normalized record onto the target Kind's fields. An absent path leaves the field
|
|
966
|
+
* unobserved. An empty value (`null` or blank text) clears an optional field and leaves a
|
|
967
|
+
* required one, or `title`, unobserved with `VALUE_MISSING`. A list or mapping is unobserved with
|
|
968
|
+
* `VALUE_NOT_SCALAR`. A native outside a values table is looked up as its string (a number as
|
|
969
|
+
* `String(n)`, a boolean as `true` or `false`) and then follows `unmapped`: `keep` leaves the field
|
|
970
|
+
* unobserved with `VALUE_UNMAPPED`; `refuse` refuses the whole record, which still counts as present.
|
|
971
|
+
* A record whose identity cannot be rendered is refused and not present.
|
|
972
|
+
*
|
|
973
|
+
* A field the Kind declares as `string-list` or `https-url-list` is always a list: a list of
|
|
974
|
+
* nonblank text is written deduplicated and in code point order, and `null` is `[]` (empty, so the
|
|
975
|
+
* owned list empties). An empty list on a required field is `VALUE_MISSING`; anything else, a
|
|
976
|
+
* member that is not nonblank text included, is unobserved with `VALUE_NOT_LIST`.
|
|
977
|
+
*/
|
|
978
|
+
export function mapRecord(source, value, kind) {
|
|
979
|
+
const id = renderId(source.identity.id, value);
|
|
980
|
+
if (!id.ok)
|
|
981
|
+
return { kind: "refused", present: false, external_id: null, diagnostics: [{ code: id.code, field: "identity", from: id.path, native: null, external_id: null }] };
|
|
982
|
+
const externalId = id.id;
|
|
983
|
+
// `title` counts as declared by every Kind, so an empty value never clears it either.
|
|
984
|
+
const required = new Set([...kind.fields.required, "title"]);
|
|
985
|
+
const fields = {};
|
|
986
|
+
const unobserved = [];
|
|
987
|
+
const cleared = [];
|
|
988
|
+
const refused = [];
|
|
989
|
+
const diagnostics = [];
|
|
990
|
+
const note = (code, field, from, native) => {
|
|
991
|
+
diagnostics.push({ code, field, from, native, external_id: externalId });
|
|
992
|
+
};
|
|
993
|
+
for (const [field, mapping] of Object.entries(source.fields)) {
|
|
994
|
+
const read = readPath(value, mapping.from);
|
|
995
|
+
if (!read.found) {
|
|
996
|
+
unobserved.push(field);
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
const native = read.value;
|
|
1000
|
+
if (isKindListField(kind, field)) {
|
|
1001
|
+
const members = native === null ? [] : textSet(native);
|
|
1002
|
+
if (members === undefined) {
|
|
1003
|
+
unobserved.push(field);
|
|
1004
|
+
note("VALUE_NOT_LIST", field, mapping.from, native);
|
|
1005
|
+
}
|
|
1006
|
+
else if (members.length === 0 && required.has(field)) {
|
|
1007
|
+
unobserved.push(field);
|
|
1008
|
+
note("VALUE_MISSING", field, mapping.from, native);
|
|
1009
|
+
}
|
|
1010
|
+
else
|
|
1011
|
+
fields[field] = members;
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
const empty = () => {
|
|
1015
|
+
if (required.has(field)) {
|
|
1016
|
+
unobserved.push(field);
|
|
1017
|
+
note("VALUE_MISSING", field, mapping.from, native);
|
|
1018
|
+
}
|
|
1019
|
+
else
|
|
1020
|
+
cleared.push(field);
|
|
1021
|
+
};
|
|
1022
|
+
if (native === null) {
|
|
1023
|
+
empty();
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
if (!scalar(native)) {
|
|
1027
|
+
unobserved.push(field);
|
|
1028
|
+
note("VALUE_NOT_SCALAR", field, mapping.from, native);
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
if (mapping.values) {
|
|
1032
|
+
const key = typeof native === "string" ? native : String(native);
|
|
1033
|
+
if (Object.hasOwn(mapping.values, key)) {
|
|
1034
|
+
fields[field] = mapping.values[key];
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
if (!isPresent(native)) {
|
|
1038
|
+
empty();
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
if (mapping.unmapped === "refuse") {
|
|
1042
|
+
refused.push(field);
|
|
1043
|
+
note("RECORD_REFUSED", field, mapping.from, native);
|
|
1044
|
+
}
|
|
1045
|
+
else {
|
|
1046
|
+
unobserved.push(field);
|
|
1047
|
+
note("VALUE_UNMAPPED", field, mapping.from, native);
|
|
1048
|
+
}
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
if (!isPresent(native)) {
|
|
1052
|
+
empty();
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
fields[field] = native;
|
|
1056
|
+
}
|
|
1057
|
+
if (refused.length)
|
|
1058
|
+
return { kind: "refused", present: true, external_id: externalId, diagnostics };
|
|
1059
|
+
return { kind: "mapped", external_id: externalId, fields, unobserved, cleared, diagnostics };
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* The record-overlay change between the last accepted `after` of this record (carried values
|
|
1063
|
+
* included; null for a first observation) and a new mapping. An unobserved field is carried from
|
|
1064
|
+
* `previous` and left out of `changed_fields`, so no write and no removal is planned for it; a
|
|
1065
|
+
* cleared field is absent from `after`. Only the source's mapping keys can change.
|
|
1066
|
+
*/
|
|
1067
|
+
export function changeBetween(previous, next, meta) {
|
|
1068
|
+
const before = previous ? { ...previous } : null;
|
|
1069
|
+
const after = { ...next.fields };
|
|
1070
|
+
for (const field of next.unobserved) {
|
|
1071
|
+
if (previous && Object.hasOwn(previous, field) && previous[field] !== undefined)
|
|
1072
|
+
after[field] = previous[field];
|
|
1073
|
+
}
|
|
1074
|
+
const unobserved = new Set(next.unobserved);
|
|
1075
|
+
const mapped = [...new Set([...Object.keys(next.fields), ...next.unobserved, ...next.cleared])].sort();
|
|
1076
|
+
return {
|
|
1077
|
+
external_id: meta.external_id,
|
|
1078
|
+
change_kind: meta.change_kind,
|
|
1079
|
+
changed_fields: mapped.filter((field) => !unobserved.has(field) && !same(before?.[field], after[field])),
|
|
1080
|
+
before,
|
|
1081
|
+
after,
|
|
1082
|
+
content_digest: `sha256:${sha256Hex(overlayCanonicalJson(after))}`,
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
const clip = (text, n) => (text.length > n ? text.slice(0, n - 1) + "…" : text);
|
|
1086
|
+
const shown = (native) => (native === null || native === undefined ? null : clip(typeof native === "string" ? native : JSON.stringify(native) ?? String(native), 80));
|
|
1087
|
+
const FINDING_TEXT = {
|
|
1088
|
+
VALUE_UNMAPPED: (field, native) => `${field}: value ${JSON.stringify(native)} has no row in the values table; the stored value was kept`,
|
|
1089
|
+
RECORD_REFUSED: (field, native) => `${field}: value ${JSON.stringify(native)} has no row in the values table; the record was refused and left unchanged`,
|
|
1090
|
+
VALUE_NOT_SCALAR: (field) => `${field}: the provider sent a list or mapping; the stored value was kept`,
|
|
1091
|
+
VALUE_NOT_LIST: (field) => `${field}: the provider sent something other than a list of text; the stored value was kept`,
|
|
1092
|
+
VALUE_MISSING: (field) => `${field}: required, and the provider sent no value; the stored value was kept`,
|
|
1093
|
+
IDENTITY_MISSING: () => "records without a value for the identity template were skipped",
|
|
1094
|
+
IDENTITY_INVALID: () => "records whose identity could collide with another's or is too long were skipped",
|
|
1095
|
+
};
|
|
1096
|
+
function* take(items, n) {
|
|
1097
|
+
if (n <= 0)
|
|
1098
|
+
return;
|
|
1099
|
+
for (const item of items) {
|
|
1100
|
+
yield item;
|
|
1101
|
+
if (--n === 0)
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
/** A native named by its shape: `list`, `mapping`, `empty` (absent, `null` or blank text) or `scalar`. */
|
|
1106
|
+
function nativeShape(native) {
|
|
1107
|
+
if (Array.isArray(native))
|
|
1108
|
+
return "list";
|
|
1109
|
+
if (record(native))
|
|
1110
|
+
return "mapping";
|
|
1111
|
+
return native === null || native === undefined || (typeof native === "string" && !isPresent(native)) ? "empty" : "scalar";
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Findings for `planFindings`, so one problem across many records raises one Signal: an unknown
|
|
1115
|
+
* value one per source, field, code and native value; a value of the wrong shape or an empty one
|
|
1116
|
+
* one per source, field, code and shape; identity gaps one per source. `facts` hold the code, field
|
|
1117
|
+
* and the clipped native value or the shape, never a record id, so a dismissed finding stays
|
|
1118
|
+
* dismissed until they change; the summary names the count and a sample of record ids.
|
|
1119
|
+
*/
|
|
1120
|
+
export function findingsFor(diagnostics, scope) {
|
|
1121
|
+
const groups = new Map();
|
|
1122
|
+
for (const d of diagnostics) {
|
|
1123
|
+
const identity = d.code === "IDENTITY_MISSING" || d.code === "IDENTITY_INVALID";
|
|
1124
|
+
const key = identity
|
|
1125
|
+
? `mapping:${scope.source}:${d.code}`
|
|
1126
|
+
: d.code === "VALUE_NOT_SCALAR" || d.code === "VALUE_NOT_LIST" || d.code === "VALUE_MISSING"
|
|
1127
|
+
? `mapping:${scope.source}:${d.field}:${d.code}:${nativeShape(d.native)}`
|
|
1128
|
+
// The string the values table was searched with, so 7 and "7" are one finding as one lookup.
|
|
1129
|
+
: `mapping:${scope.source}:${d.field}:${d.code}:${sha256Hex(typeof d.native === "string" ? d.native : String(d.native)).slice(0, 16)}`;
|
|
1130
|
+
let group = groups.get(key);
|
|
1131
|
+
if (!group)
|
|
1132
|
+
groups.set(key, group = { diagnostic: d, ids: new Set() });
|
|
1133
|
+
if (d.external_id !== null)
|
|
1134
|
+
group.ids.add(d.external_id);
|
|
1135
|
+
}
|
|
1136
|
+
return [...groups].map(([key, { diagnostic: d, ids }]) => {
|
|
1137
|
+
const byShape = d.code === "VALUE_NOT_SCALAR" || d.code === "VALUE_NOT_LIST" || d.code === "VALUE_MISSING";
|
|
1138
|
+
const native = d.code === "IDENTITY_MISSING" || d.code === "IDENTITY_INVALID" || byShape ? null : shown(d.native);
|
|
1139
|
+
const sample = [...take(ids, 3)].map((id) => clip(id, 60)).join(", ");
|
|
1140
|
+
const records = ids.size ? `${ids.size} record${ids.size === 1 ? "" : "s"} (${sample}${ids.size > 3 ? ", …" : ""})` : "records";
|
|
1141
|
+
return {
|
|
1142
|
+
key,
|
|
1143
|
+
title: clip(`Mapping ${d.code} on ${scope.source}.${d.field}`, 140),
|
|
1144
|
+
summary: clip(`${FINDING_TEXT[d.code](d.field, native)}: ${records}`, 600),
|
|
1145
|
+
priority: d.code === "RECORD_REFUSED" || d.code === "IDENTITY_MISSING" || d.code === "IDENTITY_INVALID" ? "normal" : "low",
|
|
1146
|
+
facts: byShape ? { code: d.code, field: d.field, shape: nativeShape(d.native) } : { code: d.code, field: d.field, native },
|
|
1147
|
+
};
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
//# sourceMappingURL=connectors.js.map
|