@wildo-ai/systems-of-record 1.1.6

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.
Files changed (34) hide show
  1. package/LICENSE +34 -0
  2. package/dist/esm/microsoft-dataverse.system-of-record.d.ts +3 -0
  3. package/dist/esm/microsoft-dataverse.system-of-record.d.ts.map +1 -0
  4. package/dist/esm/microsoft-dataverse.system-of-record.js +124 -0
  5. package/dist/esm/microsoft-dataverse.system-of-record.js.map +1 -0
  6. package/dist/esm/microsoft-dynamics-365-business-central.system-of-record.d.ts +3 -0
  7. package/dist/esm/microsoft-dynamics-365-business-central.system-of-record.d.ts.map +1 -0
  8. package/dist/esm/microsoft-dynamics-365-business-central.system-of-record.js +94 -0
  9. package/dist/esm/microsoft-dynamics-365-business-central.system-of-record.js.map +1 -0
  10. package/dist/esm/netsuite.system-of-record.d.ts +3 -0
  11. package/dist/esm/netsuite.system-of-record.d.ts.map +1 -0
  12. package/dist/esm/netsuite.system-of-record.js +77 -0
  13. package/dist/esm/netsuite.system-of-record.js.map +1 -0
  14. package/dist/esm/odata.system-of-record.d.ts +3 -0
  15. package/dist/esm/odata.system-of-record.d.ts.map +1 -0
  16. package/dist/esm/odata.system-of-record.js +102 -0
  17. package/dist/esm/odata.system-of-record.js.map +1 -0
  18. package/dist/esm/odoo.system-of-record.d.ts +3 -0
  19. package/dist/esm/odoo.system-of-record.d.ts.map +1 -0
  20. package/dist/esm/odoo.system-of-record.js +122 -0
  21. package/dist/esm/odoo.system-of-record.js.map +1 -0
  22. package/dist/esm/salesforce.system-of-record.d.ts +3 -0
  23. package/dist/esm/salesforce.system-of-record.d.ts.map +1 -0
  24. package/dist/esm/salesforce.system-of-record.js +80 -0
  25. package/dist/esm/salesforce.system-of-record.js.map +1 -0
  26. package/dist/esm/sap-s4hana.system-of-record.d.ts +3 -0
  27. package/dist/esm/sap-s4hana.system-of-record.d.ts.map +1 -0
  28. package/dist/esm/sap-s4hana.system-of-record.js +84 -0
  29. package/dist/esm/sap-s4hana.system-of-record.js.map +1 -0
  30. package/dist/esm/systems-of-record.exports.d.ts +78 -0
  31. package/dist/esm/systems-of-record.exports.d.ts.map +1 -0
  32. package/dist/esm/systems-of-record.exports.js +46 -0
  33. package/dist/esm/systems-of-record.exports.js.map +1 -0
  34. package/package.json +43 -0
package/LICENSE ADDED
@@ -0,0 +1,34 @@
1
+ Copyright (c) 2026 P2C Consulting, s.à r.l.-S
2
+ RCS Luxembourg B307591
3
+ 6C, Porte de France, L-4360 Esch-sur-Alzette, Luxembourg
4
+
5
+ All rights reserved.
6
+
7
+ This software and its accompanying materials (the "Software") are the
8
+ confidential and proprietary property of P2C Consulting, s.à r.l.-S (the
9
+ "Licensor"). The Software is made available for viewing only. No licence,
10
+ right, or permission of any kind is granted by its publication or by its
11
+ availability on any package registry or source repository.
12
+
13
+ Without the Licensor's prior written permission, you may not use, copy,
14
+ reproduce, modify, adapt, translate, merge, publish, distribute, sublicense,
15
+ sell, host, or create derivative works of the Software, in whole or in part,
16
+ nor use it to provide a service to any third party.
17
+
18
+ Publication of the Software in any public registry is for distribution to
19
+ authorised recipients and for technical evaluation of its packaging only. It
20
+ does not place the Software in the public domain, does not constitute an open
21
+ source release, and does not waive any right of the Licensor.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
27
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
28
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
29
+
30
+ Third-party components distributed with the Software remain governed by their
31
+ own licences.
32
+
33
+ Contact: P2C Consulting, s.à r.l.-S, 6C, Porte de France, L-4360
34
+ Esch-sur-Alzette, Luxembourg.
@@ -0,0 +1,3 @@
1
+ import type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';
2
+ export declare const MicrosoftDataverseSystemOfRecordFacet: SystemOfRecordVendorFacet;
3
+ //# sourceMappingURL=microsoft-dataverse.system-of-record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"microsoft-dataverse.system-of-record.d.ts","sourceRoot":"","sources":["../../../src/microsoft-dataverse.system-of-record.ts"],"names":[],"mappings":"AA8CA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AAUrF,eAAO,MAAM,qCAAqC,EAAE,yBA4EnD,CAAC"}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Microsoft Dataverse (Dynamics 365 Customer Engagement), as a SYSTEM OF RECORD.
3
+ *
4
+ * spec-cite: Microsoft Learn, "Use the Microsoft Dataverse Web API" —
5
+ * https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/overview
6
+ *
7
+ * ## It pages by CONTINUATION, and that is why this facet exists rather than `odata`
8
+ *
9
+ * The Dataverse Web API does not support `$skip`. The engine's read-through LIST compiles `$top` and
10
+ * `$skip` for every page after the first, so an instance declared against the generic `odata` facet
11
+ * would serve page one and be refused on page two — an application that looks correct in a demo and
12
+ * fails the first time somebody scrolls.
13
+ *
14
+ * spec-cite: Microsoft Learn, "Query data using the Web API" —
15
+ * https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/query-data-web-api
16
+ * (that page enumerates the OData query options the Web API does not support).
17
+ *
18
+ * The consequence an author should know before choosing this vendor: page N costs N requests, and
19
+ * the engine refuses past a bound rather than walking indefinitely. A deep collection wants a filter.
20
+ *
21
+ * ## Deterministic order is not decoration here
22
+ *
23
+ * Its paging cookie is built from the LAST ROW of the previous page under the query's own order, and
24
+ * its own documentation is explicit that without a deterministic order the same record can appear on
25
+ * more than one page. The engine appends the binding's key components to `$orderby` under
26
+ * continuation paging for exactly that reason — a duplicated row is harmless, a SKIPPED one is
27
+ * silently absent.
28
+ *
29
+ * spec-cite: Microsoft Learn, "Page results using OData from Microsoft Dataverse Web API" —
30
+ * "Deterministic ordering is important" / "Without proper ordering, the same record can appear in
31
+ * more than one page."
32
+ *
33
+ * ## Why three Microsoft refs exist and only this one is the facet
34
+ *
35
+ * The catalogue holds `microsoft-dataverse`, `microsoft-dynamics-365` and `microsoft-dynamics-crm` —
36
+ * three scraped refs over one Web API. `microsoft-dataverse` is the name the API itself uses, so it
37
+ * is the one an instance names. The other two are deliberately not given facets: a second facet for
38
+ * the same API would be a second answer to "how is this read", free to disagree.
39
+ *
40
+ * ## Partitioning is absent, and that is #1058's
41
+ *
42
+ * Dataverse partitions by business unit (`_owningbusinessunit_value`). The partition vocabulary does
43
+ * not exist yet, and declaring one here would claim a narrowing that does not happen — so until
44
+ * #1058 an application reading this vendor sees every business unit its service identity can see.
45
+ */
46
+ import { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';
47
+ import { ExternalDataIncrementalExtractionMode, ODATA_ENTITY_TAG_CONTROL_INFORMATION, SystemOfRecordAssertionHeaderPolicy, SystemOfRecordAssertionSigningAlgorithm, SystemOfRecordClientAuthenticationPlacement, SystemOfRecordODataListPaging, } from '@wildo-ai/saas-models/external-data';
48
+ export const MicrosoftDataverseSystemOfRecordFacet = {
49
+ vendor: 'microsoft-dataverse',
50
+ dialects: [HttpApiTransportDialect.ODATA],
51
+ odataListPaging: SystemOfRecordODataListPaging.NEXT_LINK_ONLY,
52
+ /*
53
+ * #1722 — every Dataverse table carries `modifiedon`, and the Web API tracks changes (delta links, deleted entities)
54
+ * for a table that has change tracking enabled. Enabling it is per table, so a pipeline on a table without it is
55
+ * refused at its first delta read, naming the cause.
56
+ * spec-cite: Microsoft Learn, "Use change tracking to synchronize data with external systems"
57
+ * (https://learn.microsoft.com/en-us/power-apps/developer/data-platform/use-change-tracking-synchronize-data-external-systems),
58
+ * "Retrieve changes for a table using the Web API".
59
+ */
60
+ incrementalExtractionModes: [ExternalDataIncrementalExtractionMode.ODATA_MODIFIED_AT, ExternalDataIncrementalExtractionMode.ODATA_DELTA_LINK],
61
+ /*
62
+ * #1722 — Dataverse keeps change tracking for `ExpireChangeTrackingInDays` (seven by default) and says only that "the
63
+ * system throws an exception" past it; its error-code reference names that exception, and it is not the protocol's
64
+ * `410`. Stating the code lets the pass start over with a full read instead of failing every tick. The HTTP status
65
+ * Dataverse pairs it with is not documented, so the code alone decides (UNPROVEN against a live table, #2009).
66
+ * spec-cite: Microsoft Learn, "Web service error codes" (https://learn.microsoft.com/en-us/power-apps/developer/data-platform/reference/web-service-error-codes)
67
+ * — row `0x80044352`, `ExpiredVersionStamp`: "Version stamp associated with the client has expired. Please perform a
68
+ * full sync."
69
+ */
70
+ odataExpiredChangePositionErrorCodes: ['0x80044352'],
71
+ /*
72
+ * #1726 — every Dataverse row carries an entity tag, and an update or delete sent with it as `If-Match` is refused
73
+ * with `412 Precondition Failed` when the row changed since. So a binding may declare `VERSION_CHECKED` on a field
74
+ * mapped to the tag and get a real optimistic-concurrency check.
75
+ *
76
+ * spec-cite: Microsoft Learn, "Perform conditional operations using the Web API (Microsoft Dataverse)" —
77
+ * https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/perform-conditional-operations-using-web-api
78
+ * ("Dataverse generates a weakly validating `@odata.etag` property for every entity instance", and the two 412
79
+ * examples under "Apply optimistic concurrency").
80
+ *
81
+ * UNCONFIRMED, and stated rather than hidden: Dataverse documents optimistic concurrency as supported "on all
82
+ * out-of-box tables enabled for offline sync and all custom tables" (Microsoft Learn, "Optimistic concurrency"), and
83
+ * the SDK refuses a version-checked update on another table (`OptimisticConcurrencyNotEnabled`). What the Web API
84
+ * answers to an `If-Match` on such a table was not measured; a refusal there reaches the caller as a remote refusal.
85
+ *
86
+ * Dataverse also UPSERTS: a `PATCH` to a key that no longer exists creates the row. The write dialect therefore sends
87
+ * `If-Match` on every update (the row's tag, or `*`), which the same page documents as the way to prevent it.
88
+ */
89
+ rowVersionField: ODATA_ENTITY_TAG_CONTROL_INFORMATION,
90
+ addressSlots: [
91
+ {
92
+ slug: 'baseUrl',
93
+ required: true,
94
+ description: "This environment's Web API service root, version included "
95
+ + '(https://<org>.crm<region>.dynamics.com/api/data/v9.2). The organization URL alone is not '
96
+ + 'enough: entity-set names are appended to the versioned data path.',
97
+ },
98
+ ],
99
+ credentialOffers: [
100
+ {
101
+ kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_SECRET,
102
+ // spec-cite: Microsoft Learn, "Microsoft identity platform and the OAuth 2.0 client credentials
103
+ // flow" — https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant
104
+ clientAuthentication: SystemOfRecordClientAuthenticationPlacement.REQUEST_BODY,
105
+ tokenLifetimeFallbackSeconds: 900,
106
+ },
107
+ {
108
+ kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_CERTIFICATE,
109
+ /*
110
+ * UNCONFIRMED, and left so deliberately: Entra's certificate credential is documented as
111
+ * identifying the signing key by an `x5t` thumbprint in the assertion header, and that is what
112
+ * this declares — but the page was not fetched at authoring time, so it is the plan's
113
+ * expectation rather than a quotation (`external-specification-citations.md` §4). An operator
114
+ * meeting `invalid_client` with a certificate credential should read the vendor's assertion
115
+ * format before assuming the engine is wrong.
116
+ */
117
+ assertionHeaderPolicy: SystemOfRecordAssertionHeaderPolicy.X509_THUMBPRINT_DERIVED,
118
+ assertionSigningAlgorithm: SystemOfRecordAssertionSigningAlgorithm.RS256,
119
+ assertionLifetimeSeconds: 300,
120
+ tokenLifetimeFallbackSeconds: 900,
121
+ },
122
+ ],
123
+ };
124
+ //# sourceMappingURL=microsoft-dataverse.system-of-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"microsoft-dataverse.system-of-record.js","sourceRoot":"","sources":["../../../src/microsoft-dataverse.system-of-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,OAAO,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAE9F,OAAO,EACL,qCAAqC,EACrC,oCAAoC,EACpC,mCAAmC,EACnC,uCAAuC,EACvC,2CAA2C,EAC3C,6BAA6B,GAC9B,MAAM,qCAAqC,CAAC;AAE7C,MAAM,CAAC,MAAM,qCAAqC,GAA8B;IAC9E,MAAM,EAAE,qBAAqB;IAC7B,QAAQ,EAAE,CAAC,uBAAuB,CAAC,KAAK,CAAC;IACzC,eAAe,EAAE,6BAA6B,CAAC,cAAc;IAC7D;;;;;;;OAOG;IACH,0BAA0B,EAAE,CAAC,qCAAqC,CAAC,iBAAiB,EAAE,qCAAqC,CAAC,gBAAgB,CAAC;IAC7I;;;;;;;;OAQG;IACH,oCAAoC,EAAE,CAAC,YAAY,CAAC;IACpD;;;;;;;;;;;;;;;;;OAiBG;IACH,eAAe,EAAE,oCAAoC;IACrD,YAAY,EAAE;QACZ;YACE,IAAI,EAAE,SAAS;YACf,QAAQ,EAAE,IAAI;YACd,WAAW,EACT,4DAA4D;kBAC1D,4FAA4F;kBAC5F,mEAAmE;SACxE;KACF;IACD,gBAAgB,EAAE;QAChB;YACE,IAAI,EAAE,4BAA4B,CAAC,gCAAgC;YACnE,gGAAgG;YAChG,iGAAiG;YACjG,oBAAoB,EAAE,2CAA2C,CAAC,YAAY;YAC9E,4BAA4B,EAAE,GAAG;SAClC;QACD;YACE,IAAI,EAAE,4BAA4B,CAAC,qCAAqC;YACxE;;;;;;;eAOG;YACH,qBAAqB,EAAE,mCAAmC,CAAC,uBAAuB;YAClF,yBAAyB,EAAE,uCAAuC,CAAC,KAAK;YACxE,wBAAwB,EAAE,GAAG;YAC7B,4BAA4B,EAAE,GAAG;SAClC;KACF;CACF,CAAC","sourcesContent":["/**\n * Microsoft Dataverse (Dynamics 365 Customer Engagement), as a SYSTEM OF RECORD.\n *\n * spec-cite: Microsoft Learn, \"Use the Microsoft Dataverse Web API\" —\n * https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/overview\n *\n * ## It pages by CONTINUATION, and that is why this facet exists rather than `odata`\n *\n * The Dataverse Web API does not support `$skip`. The engine's read-through LIST compiles `$top` and\n * `$skip` for every page after the first, so an instance declared against the generic `odata` facet\n * would serve page one and be refused on page two — an application that looks correct in a demo and\n * fails the first time somebody scrolls.\n *\n * spec-cite: Microsoft Learn, \"Query data using the Web API\" —\n * https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/query-data-web-api\n * (that page enumerates the OData query options the Web API does not support).\n *\n * The consequence an author should know before choosing this vendor: page N costs N requests, and\n * the engine refuses past a bound rather than walking indefinitely. A deep collection wants a filter.\n *\n * ## Deterministic order is not decoration here\n *\n * Its paging cookie is built from the LAST ROW of the previous page under the query's own order, and\n * its own documentation is explicit that without a deterministic order the same record can appear on\n * more than one page. The engine appends the binding's key components to `$orderby` under\n * continuation paging for exactly that reason — a duplicated row is harmless, a SKIPPED one is\n * silently absent.\n *\n * spec-cite: Microsoft Learn, \"Page results using OData from Microsoft Dataverse Web API\" —\n * \"Deterministic ordering is important\" / \"Without proper ordering, the same record can appear in\n * more than one page.\"\n *\n * ## Why three Microsoft refs exist and only this one is the facet\n *\n * The catalogue holds `microsoft-dataverse`, `microsoft-dynamics-365` and `microsoft-dynamics-crm` —\n * three scraped refs over one Web API. `microsoft-dataverse` is the name the API itself uses, so it\n * is the one an instance names. The other two are deliberately not given facets: a second facet for\n * the same API would be a second answer to \"how is this read\", free to disagree.\n *\n * ## Partitioning is absent, and that is #1058's\n *\n * Dataverse partitions by business unit (`_owningbusinessunit_value`). The partition vocabulary does\n * not exist yet, and declaring one here would claim a narrowing that does not happen — so until\n * #1058 an application reading this vendor sees every business unit its service identity can see.\n */\nimport { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';\nimport type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';\nimport {\n ExternalDataIncrementalExtractionMode,\n ODATA_ENTITY_TAG_CONTROL_INFORMATION,\n SystemOfRecordAssertionHeaderPolicy,\n SystemOfRecordAssertionSigningAlgorithm,\n SystemOfRecordClientAuthenticationPlacement,\n SystemOfRecordODataListPaging,\n} from '@wildo-ai/saas-models/external-data';\n\nexport const MicrosoftDataverseSystemOfRecordFacet: SystemOfRecordVendorFacet = {\n vendor: 'microsoft-dataverse',\n dialects: [HttpApiTransportDialect.ODATA],\n odataListPaging: SystemOfRecordODataListPaging.NEXT_LINK_ONLY,\n /*\n * #1722 — every Dataverse table carries `modifiedon`, and the Web API tracks changes (delta links, deleted entities)\n * for a table that has change tracking enabled. Enabling it is per table, so a pipeline on a table without it is\n * refused at its first delta read, naming the cause.\n * spec-cite: Microsoft Learn, \"Use change tracking to synchronize data with external systems\"\n * (https://learn.microsoft.com/en-us/power-apps/developer/data-platform/use-change-tracking-synchronize-data-external-systems),\n * \"Retrieve changes for a table using the Web API\".\n */\n incrementalExtractionModes: [ExternalDataIncrementalExtractionMode.ODATA_MODIFIED_AT, ExternalDataIncrementalExtractionMode.ODATA_DELTA_LINK],\n /*\n * #1722 — Dataverse keeps change tracking for `ExpireChangeTrackingInDays` (seven by default) and says only that \"the\n * system throws an exception\" past it; its error-code reference names that exception, and it is not the protocol's\n * `410`. Stating the code lets the pass start over with a full read instead of failing every tick. The HTTP status\n * Dataverse pairs it with is not documented, so the code alone decides (UNPROVEN against a live table, #2009).\n * spec-cite: Microsoft Learn, \"Web service error codes\" (https://learn.microsoft.com/en-us/power-apps/developer/data-platform/reference/web-service-error-codes)\n * — row `0x80044352`, `ExpiredVersionStamp`: \"Version stamp associated with the client has expired. Please perform a\n * full sync.\"\n */\n odataExpiredChangePositionErrorCodes: ['0x80044352'],\n /*\n * #1726 — every Dataverse row carries an entity tag, and an update or delete sent with it as `If-Match` is refused\n * with `412 Precondition Failed` when the row changed since. So a binding may declare `VERSION_CHECKED` on a field\n * mapped to the tag and get a real optimistic-concurrency check.\n *\n * spec-cite: Microsoft Learn, \"Perform conditional operations using the Web API (Microsoft Dataverse)\" —\n * https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/perform-conditional-operations-using-web-api\n * (\"Dataverse generates a weakly validating `@odata.etag` property for every entity instance\", and the two 412\n * examples under \"Apply optimistic concurrency\").\n *\n * UNCONFIRMED, and stated rather than hidden: Dataverse documents optimistic concurrency as supported \"on all\n * out-of-box tables enabled for offline sync and all custom tables\" (Microsoft Learn, \"Optimistic concurrency\"), and\n * the SDK refuses a version-checked update on another table (`OptimisticConcurrencyNotEnabled`). What the Web API\n * answers to an `If-Match` on such a table was not measured; a refusal there reaches the caller as a remote refusal.\n *\n * Dataverse also UPSERTS: a `PATCH` to a key that no longer exists creates the row. The write dialect therefore sends\n * `If-Match` on every update (the row's tag, or `*`), which the same page documents as the way to prevent it.\n */\n rowVersionField: ODATA_ENTITY_TAG_CONTROL_INFORMATION,\n addressSlots: [\n {\n slug: 'baseUrl',\n required: true,\n description:\n \"This environment's Web API service root, version included \"\n + '(https://<org>.crm<region>.dynamics.com/api/data/v9.2). The organization URL alone is not '\n + 'enough: entity-set names are appended to the versioned data path.',\n },\n ],\n credentialOffers: [\n {\n kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_SECRET,\n // spec-cite: Microsoft Learn, \"Microsoft identity platform and the OAuth 2.0 client credentials\n // flow\" — https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant\n clientAuthentication: SystemOfRecordClientAuthenticationPlacement.REQUEST_BODY,\n tokenLifetimeFallbackSeconds: 900,\n },\n {\n kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_CERTIFICATE,\n /*\n * UNCONFIRMED, and left so deliberately: Entra's certificate credential is documented as\n * identifying the signing key by an `x5t` thumbprint in the assertion header, and that is what\n * this declares — but the page was not fetched at authoring time, so it is the plan's\n * expectation rather than a quotation (`external-specification-citations.md` §4). An operator\n * meeting `invalid_client` with a certificate credential should read the vendor's assertion\n * format before assuming the engine is wrong.\n */\n assertionHeaderPolicy: SystemOfRecordAssertionHeaderPolicy.X509_THUMBPRINT_DERIVED,\n assertionSigningAlgorithm: SystemOfRecordAssertionSigningAlgorithm.RS256,\n assertionLifetimeSeconds: 300,\n tokenLifetimeFallbackSeconds: 900,\n },\n ],\n};\n"]}
@@ -0,0 +1,3 @@
1
+ import type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';
2
+ export declare const MicrosoftDynamics365BusinessCentralSystemOfRecordFacet: SystemOfRecordVendorFacet;
3
+ //# sourceMappingURL=microsoft-dynamics-365-business-central.system-of-record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"microsoft-dynamics-365-business-central.system-of-record.d.ts","sourceRoot":"","sources":["../../../src/microsoft-dynamics-365-business-central.system-of-record.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AASrF,eAAO,MAAM,sDAAsD,EAAE,yBA6DpE,CAAC"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Microsoft Dynamics 365 Business Central, as a SYSTEM OF RECORD.
3
+ *
4
+ * Business Central's own API is OData v4, and an application reads it as a registered Entra
5
+ * application rather than as a person — which is the whole of D12 for this vendor.
6
+ *
7
+ * spec-cite: Microsoft Learn, "Use OAuth to Authorize Business Central Web Services" —
8
+ * https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/webservices/authenticate-web-services-using-oauth
9
+ *
10
+ * ## Why `baseUrl` reaches down to the environment and not merely the host
11
+ *
12
+ * A Business Central address carries a TENANT and an ENVIRONMENT
13
+ * (`.../v2.0/<tenant>/<environment>/api/v2.0`), and both are deployment facts: production and
14
+ * sandbox differ only in that path segment. Putting the whole service root in one slot is what lets
15
+ * one declaration serve both by pointing at different environments, which is exactly the shape
16
+ * `systemsOfRecord` exists for.
17
+ *
18
+ * ## Its PARTITION is a path segment, and that is why this facet declares none
19
+ *
20
+ * Business Central partitions by COMPANY, and a company is addressed as a path segment
21
+ * (`/companies({id})/customers`) rather than as a filterable field. That is a real difference from
22
+ * every other vendor here and it has no expression yet: the partition vocabulary is #1058's, and
23
+ * that slice supports FIELD partitions first. Declaring something now would be declaring a
24
+ * narrowing that does not happen — which #1055 already refuses for the instance-side `partition`
25
+ * key, for the same reason.
26
+ *
27
+ * So until #1058: an application reading this vendor sees every company its service identity can
28
+ * see. That is stated here rather than discovered, and it is why the address slot below asks for a
29
+ * service root that MAY already name a company.
30
+ */
31
+ import { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';
32
+ import { ExternalDataIncrementalExtractionMode, ODATA_ENTITY_TAG_CONTROL_INFORMATION, SystemOfRecordAssertionHeaderPolicy, SystemOfRecordAssertionSigningAlgorithm, SystemOfRecordClientAuthenticationPlacement, } from '@wildo-ai/saas-models/external-data';
33
+ export const MicrosoftDynamics365BusinessCentralSystemOfRecordFacet = {
34
+ vendor: 'microsoft-dynamics-365-business-central',
35
+ dialects: [HttpApiTransportDialect.ODATA],
36
+ /*
37
+ * #1722 — MODIFIED_AT is a plain OData `$filter … ge` on a timestamp property the pipeline declaration names per entity;
38
+ * whether an entity carries one is the declaration's to say, not the vendor's.
39
+ */
40
+ incrementalExtractionModes: [ExternalDataIncrementalExtractionMode.ODATA_MODIFIED_AT],
41
+ /*
42
+ * #1726 — Business Central's API entities carry an entity tag, and it REQUIRES `If-Match` on an update: a request
43
+ * whose tag no longer matches is not applied. The write dialect sends the tag of the row it read first (or `*`) on
44
+ * every update and delete, so the requirement is always met.
45
+ *
46
+ * spec-cite: Microsoft Learn, "Update customers - Business Central" —
47
+ * https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/api-reference/v2.0/api/dynamics_customer_update
48
+ * (request header `If-Match`: "Required. When this request header is included and the eTag provided doesn't match
49
+ * the current tag on the **customers**, the **customers** won't be updated.").
50
+ *
51
+ * Writes and the company PARTITION: a Business Central company is a path segment, and this facet declares no
52
+ * partition (see above) — the vocabulary's PATH_SEGMENT kind is one startup refuses to confine on. A write therefore
53
+ * lands in whatever company its `baseUrl` or `entityRef` path names — the same reach its reads have, and nothing wider.
54
+ */
55
+ rowVersionField: ODATA_ENTITY_TAG_CONTROL_INFORMATION,
56
+ addressSlots: [
57
+ {
58
+ slug: 'baseUrl',
59
+ required: true,
60
+ description: 'The API service root for THIS environment, tenant and environment included '
61
+ + '(https://api.businesscentral.dynamics.com/v2.0/<tenant>/<environment>/api/v2.0). It may '
62
+ + "already name a company (…/companies(<id>)) — until per-company narrowing exists, that is "
63
+ + 'the only way to confine a deployment to one.',
64
+ },
65
+ ],
66
+ credentialOffers: [
67
+ {
68
+ kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_SECRET,
69
+ // Entra's token endpoint accepts both placements; the body form is what its own examples show.
70
+ // spec-cite: Microsoft Learn, "Microsoft identity platform and the OAuth 2.0 client credentials flow" —
71
+ // https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant
72
+ clientAuthentication: SystemOfRecordClientAuthenticationPlacement.REQUEST_BODY,
73
+ // Entra's client-credentials answer carries `expires_in`, so this fallback is a floor that is
74
+ // not expected to be reached rather than a guess at the real lifetime.
75
+ tokenLifetimeFallbackSeconds: 900,
76
+ },
77
+ {
78
+ kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_CERTIFICATE,
79
+ /*
80
+ * UNCONFIRMED: Entra's certificate credential is documented as identifying the signing key by
81
+ * an `x5t` thumbprint in the assertion header, and that is what this declares. The page could
82
+ * not be fetched at authoring time, so this is the plan's expectation rather than a quotation
83
+ * — `external-specification-citations.md` §4 rather than an invented citation. An operator who
84
+ * meets `invalid_client` with a certificate credential should read the vendor's assertion
85
+ * format before assuming the engine is wrong.
86
+ */
87
+ assertionHeaderPolicy: SystemOfRecordAssertionHeaderPolicy.X509_THUMBPRINT_DERIVED,
88
+ assertionSigningAlgorithm: SystemOfRecordAssertionSigningAlgorithm.RS256,
89
+ assertionLifetimeSeconds: 300,
90
+ tokenLifetimeFallbackSeconds: 900,
91
+ },
92
+ ],
93
+ };
94
+ //# sourceMappingURL=microsoft-dynamics-365-business-central.system-of-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"microsoft-dynamics-365-business-central.system-of-record.js","sourceRoot":"","sources":["../../../src/microsoft-dynamics-365-business-central.system-of-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAE9F,OAAO,EACL,qCAAqC,EACrC,oCAAoC,EACpC,mCAAmC,EACnC,uCAAuC,EACvC,2CAA2C,GAC5C,MAAM,qCAAqC,CAAC;AAE7C,MAAM,CAAC,MAAM,sDAAsD,GAA8B;IAC/F,MAAM,EAAE,yCAAyC;IACjD,QAAQ,EAAE,CAAC,uBAAuB,CAAC,KAAK,CAAC;IACzC;;;OAGG;IACH,0BAA0B,EAAE,CAAC,qCAAqC,CAAC,iBAAiB,CAAC;IACrF;;;;;;;;;;;;;OAaG;IACH,eAAe,EAAE,oCAAoC;IACrD,YAAY,EAAE;QACZ;YACE,IAAI,EAAE,SAAS;YACf,QAAQ,EAAE,IAAI;YACd,WAAW,EACT,6EAA6E;kBAC3E,0FAA0F;kBAC1F,2FAA2F;kBAC3F,8CAA8C;SACnD;KACF;IACD,gBAAgB,EAAE;QAChB;YACE,IAAI,EAAE,4BAA4B,CAAC,gCAAgC;YACnE,+FAA+F;YAC/F,wGAAwG;YACxG,yFAAyF;YACzF,oBAAoB,EAAE,2CAA2C,CAAC,YAAY;YAC9E,8FAA8F;YAC9F,uEAAuE;YACvE,4BAA4B,EAAE,GAAG;SAClC;QACD;YACE,IAAI,EAAE,4BAA4B,CAAC,qCAAqC;YACxE;;;;;;;eAOG;YACH,qBAAqB,EAAE,mCAAmC,CAAC,uBAAuB;YAClF,yBAAyB,EAAE,uCAAuC,CAAC,KAAK;YACxE,wBAAwB,EAAE,GAAG;YAC7B,4BAA4B,EAAE,GAAG;SAClC;KACF;CACF,CAAC","sourcesContent":["/**\n * Microsoft Dynamics 365 Business Central, as a SYSTEM OF RECORD.\n *\n * Business Central's own API is OData v4, and an application reads it as a registered Entra\n * application rather than as a person — which is the whole of D12 for this vendor.\n *\n * spec-cite: Microsoft Learn, \"Use OAuth to Authorize Business Central Web Services\" —\n * https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/webservices/authenticate-web-services-using-oauth\n *\n * ## Why `baseUrl` reaches down to the environment and not merely the host\n *\n * A Business Central address carries a TENANT and an ENVIRONMENT\n * (`.../v2.0/<tenant>/<environment>/api/v2.0`), and both are deployment facts: production and\n * sandbox differ only in that path segment. Putting the whole service root in one slot is what lets\n * one declaration serve both by pointing at different environments, which is exactly the shape\n * `systemsOfRecord` exists for.\n *\n * ## Its PARTITION is a path segment, and that is why this facet declares none\n *\n * Business Central partitions by COMPANY, and a company is addressed as a path segment\n * (`/companies({id})/customers`) rather than as a filterable field. That is a real difference from\n * every other vendor here and it has no expression yet: the partition vocabulary is #1058's, and\n * that slice supports FIELD partitions first. Declaring something now would be declaring a\n * narrowing that does not happen — which #1055 already refuses for the instance-side `partition`\n * key, for the same reason.\n *\n * So until #1058: an application reading this vendor sees every company its service identity can\n * see. That is stated here rather than discovered, and it is why the address slot below asks for a\n * service root that MAY already name a company.\n */\nimport { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';\nimport type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';\nimport {\n ExternalDataIncrementalExtractionMode,\n ODATA_ENTITY_TAG_CONTROL_INFORMATION,\n SystemOfRecordAssertionHeaderPolicy,\n SystemOfRecordAssertionSigningAlgorithm,\n SystemOfRecordClientAuthenticationPlacement,\n} from '@wildo-ai/saas-models/external-data';\n\nexport const MicrosoftDynamics365BusinessCentralSystemOfRecordFacet: SystemOfRecordVendorFacet = {\n vendor: 'microsoft-dynamics-365-business-central',\n dialects: [HttpApiTransportDialect.ODATA],\n /*\n * #1722 — MODIFIED_AT is a plain OData `$filter … ge` on a timestamp property the pipeline declaration names per entity;\n * whether an entity carries one is the declaration's to say, not the vendor's.\n */\n incrementalExtractionModes: [ExternalDataIncrementalExtractionMode.ODATA_MODIFIED_AT],\n /*\n * #1726 — Business Central's API entities carry an entity tag, and it REQUIRES `If-Match` on an update: a request\n * whose tag no longer matches is not applied. The write dialect sends the tag of the row it read first (or `*`) on\n * every update and delete, so the requirement is always met.\n *\n * spec-cite: Microsoft Learn, \"Update customers - Business Central\" —\n * https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/api-reference/v2.0/api/dynamics_customer_update\n * (request header `If-Match`: \"Required. When this request header is included and the eTag provided doesn't match\n * the current tag on the **customers**, the **customers** won't be updated.\").\n *\n * Writes and the company PARTITION: a Business Central company is a path segment, and this facet declares no\n * partition (see above) — the vocabulary's PATH_SEGMENT kind is one startup refuses to confine on. A write therefore\n * lands in whatever company its `baseUrl` or `entityRef` path names — the same reach its reads have, and nothing wider.\n */\n rowVersionField: ODATA_ENTITY_TAG_CONTROL_INFORMATION,\n addressSlots: [\n {\n slug: 'baseUrl',\n required: true,\n description:\n 'The API service root for THIS environment, tenant and environment included '\n + '(https://api.businesscentral.dynamics.com/v2.0/<tenant>/<environment>/api/v2.0). It may '\n + \"already name a company (…/companies(<id>)) — until per-company narrowing exists, that is \"\n + 'the only way to confine a deployment to one.',\n },\n ],\n credentialOffers: [\n {\n kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_SECRET,\n // Entra's token endpoint accepts both placements; the body form is what its own examples show.\n // spec-cite: Microsoft Learn, \"Microsoft identity platform and the OAuth 2.0 client credentials flow\" —\n // https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant\n clientAuthentication: SystemOfRecordClientAuthenticationPlacement.REQUEST_BODY,\n // Entra's client-credentials answer carries `expires_in`, so this fallback is a floor that is\n // not expected to be reached rather than a guess at the real lifetime.\n tokenLifetimeFallbackSeconds: 900,\n },\n {\n kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_CERTIFICATE,\n /*\n * UNCONFIRMED: Entra's certificate credential is documented as identifying the signing key by\n * an `x5t` thumbprint in the assertion header, and that is what this declares. The page could\n * not be fetched at authoring time, so this is the plan's expectation rather than a quotation\n * — `external-specification-citations.md` §4 rather than an invented citation. An operator who\n * meets `invalid_client` with a certificate credential should read the vendor's assertion\n * format before assuming the engine is wrong.\n */\n assertionHeaderPolicy: SystemOfRecordAssertionHeaderPolicy.X509_THUMBPRINT_DERIVED,\n assertionSigningAlgorithm: SystemOfRecordAssertionSigningAlgorithm.RS256,\n assertionLifetimeSeconds: 300,\n tokenLifetimeFallbackSeconds: 900,\n },\n ],\n};\n"]}
@@ -0,0 +1,3 @@
1
+ import type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';
2
+ export declare const NetsuiteSystemOfRecordFacet: SystemOfRecordVendorFacet;
3
+ //# sourceMappingURL=netsuite.system-of-record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"netsuite.system-of-record.d.ts","sourceRoot":"","sources":["../../../src/netsuite.system-of-record.ts"],"names":[],"mappings":"AAkDA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AAMrF,eAAO,MAAM,2BAA2B,EAAE,yBAyBzC,CAAC"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Oracle NetSuite, as a SYSTEM OF RECORD.
3
+ *
4
+ * spec-cite: Oracle NetSuite, "SuiteQL" —
5
+ * https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_156257770590.html
6
+ *
7
+ * ## Read through SuiteQL, posted rather than queried
8
+ *
9
+ * A collection read is a SuiteQL statement in the BODY of a POST to
10
+ * `/services/rest/query/v1/suiteql`, paged by `limit` and `offset` QUERY parameters, and the request
11
+ * requires a `Prefer: transient` header. All three are declared on the binding's operation — the
12
+ * statement, the paging parameters and the static header — which is what #1293 added.
13
+ *
14
+ * The statement-in-a-body shape is why its operation is a POST and Salesforce's is a GET: the
15
+ * request builder places the STATEMENT by method. Paging is the exception — NetSuite reads it from
16
+ * the URL even on a POST, so its placement is declared (below).
17
+ *
18
+ * ## Its app-only identity is CLIENT AUTHENTICATION, not the assertion grant
19
+ *
20
+ * NetSuite's machine-to-machine flow is `grant_type=client_credentials` with a `client_assertion`
21
+ * (RFC 7523 §2.2) — its documentation states the grant type is *always* `client_credentials`. That
22
+ * is client AUTHENTICATION by assertion rather than the assertion GRANT, which is Salesforce's, and
23
+ * the two are separate credential kinds because the token endpoint reads them differently.
24
+ *
25
+ * spec-cite: Oracle NetSuite, "OAuth 2.0 Client Credentials Flow" —
26
+ * https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_162755359851.html
27
+ *
28
+ * The assertion's header carries the key id NetSuite assigns when the certificate is uploaded, which
29
+ * is why this facet declares the key-id policy and the instance supplies `certificateKeyId`.
30
+ *
31
+ * ## What an author gives up, stated rather than discovered
32
+ *
33
+ * No declared `sortFields` — a LIST serves the statement's own order, and the ORDER BY belongs in
34
+ * the SuiteQL. Its paging parameters go in the URL while the statement is in the body, so the
35
+ * binding declares `pagingParams.placement: 'query-string'` (#1686); placed by method, a POST's
36
+ * `limit` would land in the body and bound nothing.
37
+ *
38
+ * A pipeline walks a declared keyset (#1686), never offsets: the binding's `list` statement declares
39
+ * `keyset: { keyRemoteField: 'id' }`, and each page is `… WHERE id > <last id> ORDER BY id ASC` with
40
+ * `limit=<n>&offset=0`, so the 100,000-result offset ceiling is never reached.
41
+ *
42
+ * ## The account id is in the ADDRESS, and so is the token endpoint
43
+ *
44
+ * Both the REST base URL and the token URL are account-specific
45
+ * (`https://<accountId>.suitetalk.api.netsuite.com/…`), so neither can be a constant in this file.
46
+ * They are separate slots because they are separate hosts' worth of a decision: a sandbox account
47
+ * changes both, and an operator who changed only one would get a working token against the wrong
48
+ * data.
49
+ */
50
+ import { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';
51
+ import { SystemOfRecordAssertionHeaderPolicy, SystemOfRecordAssertionSigningAlgorithm, } from '@wildo-ai/saas-models/external-data';
52
+ export const NetsuiteSystemOfRecordFacet = {
53
+ vendor: 'netsuite',
54
+ dialects: [HttpApiTransportDialect.REST_JSON],
55
+ addressSlots: [
56
+ {
57
+ slug: 'baseUrl',
58
+ required: true,
59
+ description: "This account's REST services host (https://<accountId>.suitetalk.api.netsuite.com). A "
60
+ + "binding's path templates are appended to it — the SuiteQL endpoint is one of them.",
61
+ },
62
+ ],
63
+ credentialOffers: [
64
+ {
65
+ kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_CERTIFICATE,
66
+ // NetSuite locates the signing key by the id it assigned at certificate upload, carried as
67
+ // `kid` in the assertion's protected header. A thumbprint would identify nothing to it.
68
+ assertionHeaderPolicy: SystemOfRecordAssertionHeaderPolicy.KEY_ID_SUPPLIED,
69
+ assertionSigningAlgorithm: SystemOfRecordAssertionSigningAlgorithm.RS256,
70
+ assertionLifetimeSeconds: 300,
71
+ // Its token answer states `expires_in` is always 3600, so this fallback is a floor that should
72
+ // never be reached rather than a guess at the real lifetime.
73
+ tokenLifetimeFallbackSeconds: 3600,
74
+ },
75
+ ],
76
+ };
77
+ //# sourceMappingURL=netsuite.system-of-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"netsuite.system-of-record.js","sourceRoot":"","sources":["../../../src/netsuite.system-of-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,OAAO,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAE9F,OAAO,EACL,mCAAmC,EACnC,uCAAuC,GACxC,MAAM,qCAAqC,CAAC;AAE7C,MAAM,CAAC,MAAM,2BAA2B,GAA8B;IACpE,MAAM,EAAE,UAAU;IAClB,QAAQ,EAAE,CAAC,uBAAuB,CAAC,SAAS,CAAC;IAC7C,YAAY,EAAE;QACZ;YACE,IAAI,EAAE,SAAS;YACf,QAAQ,EAAE,IAAI;YACd,WAAW,EACT,wFAAwF;kBACtF,oFAAoF;SACzF;KACF;IACD,gBAAgB,EAAE;QAChB;YACE,IAAI,EAAE,4BAA4B,CAAC,qCAAqC;YACxE,2FAA2F;YAC3F,wFAAwF;YACxF,qBAAqB,EAAE,mCAAmC,CAAC,eAAe;YAC1E,yBAAyB,EAAE,uCAAuC,CAAC,KAAK;YACxE,wBAAwB,EAAE,GAAG;YAC7B,+FAA+F;YAC/F,6DAA6D;YAC7D,4BAA4B,EAAE,IAAI;SACnC;KACF;CACF,CAAC","sourcesContent":["/**\n * Oracle NetSuite, as a SYSTEM OF RECORD.\n *\n * spec-cite: Oracle NetSuite, \"SuiteQL\" —\n * https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_156257770590.html\n *\n * ## Read through SuiteQL, posted rather than queried\n *\n * A collection read is a SuiteQL statement in the BODY of a POST to\n * `/services/rest/query/v1/suiteql`, paged by `limit` and `offset` QUERY parameters, and the request\n * requires a `Prefer: transient` header. All three are declared on the binding's operation — the\n * statement, the paging parameters and the static header — which is what #1293 added.\n *\n * The statement-in-a-body shape is why its operation is a POST and Salesforce's is a GET: the\n * request builder places the STATEMENT by method. Paging is the exception — NetSuite reads it from\n * the URL even on a POST, so its placement is declared (below).\n *\n * ## Its app-only identity is CLIENT AUTHENTICATION, not the assertion grant\n *\n * NetSuite's machine-to-machine flow is `grant_type=client_credentials` with a `client_assertion`\n * (RFC 7523 §2.2) — its documentation states the grant type is *always* `client_credentials`. That\n * is client AUTHENTICATION by assertion rather than the assertion GRANT, which is Salesforce's, and\n * the two are separate credential kinds because the token endpoint reads them differently.\n *\n * spec-cite: Oracle NetSuite, \"OAuth 2.0 Client Credentials Flow\" —\n * https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_162755359851.html\n *\n * The assertion's header carries the key id NetSuite assigns when the certificate is uploaded, which\n * is why this facet declares the key-id policy and the instance supplies `certificateKeyId`.\n *\n * ## What an author gives up, stated rather than discovered\n *\n * No declared `sortFields` — a LIST serves the statement's own order, and the ORDER BY belongs in\n * the SuiteQL. Its paging parameters go in the URL while the statement is in the body, so the\n * binding declares `pagingParams.placement: 'query-string'` (#1686); placed by method, a POST's\n * `limit` would land in the body and bound nothing.\n *\n * A pipeline walks a declared keyset (#1686), never offsets: the binding's `list` statement declares\n * `keyset: { keyRemoteField: 'id' }`, and each page is `… WHERE id > <last id> ORDER BY id ASC` with\n * `limit=<n>&offset=0`, so the 100,000-result offset ceiling is never reached.\n *\n * ## The account id is in the ADDRESS, and so is the token endpoint\n *\n * Both the REST base URL and the token URL are account-specific\n * (`https://<accountId>.suitetalk.api.netsuite.com/…`), so neither can be a constant in this file.\n * They are separate slots because they are separate hosts' worth of a decision: a sandbox account\n * changes both, and an operator who changed only one would get a working token against the wrong\n * data.\n */\nimport { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';\nimport type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';\nimport {\n SystemOfRecordAssertionHeaderPolicy,\n SystemOfRecordAssertionSigningAlgorithm,\n} from '@wildo-ai/saas-models/external-data';\n\nexport const NetsuiteSystemOfRecordFacet: SystemOfRecordVendorFacet = {\n vendor: 'netsuite',\n dialects: [HttpApiTransportDialect.REST_JSON],\n addressSlots: [\n {\n slug: 'baseUrl',\n required: true,\n description:\n \"This account's REST services host (https://<accountId>.suitetalk.api.netsuite.com). A \"\n + \"binding's path templates are appended to it — the SuiteQL endpoint is one of them.\",\n },\n ],\n credentialOffers: [\n {\n kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_CERTIFICATE,\n // NetSuite locates the signing key by the id it assigned at certificate upload, carried as\n // `kid` in the assertion's protected header. A thumbprint would identify nothing to it.\n assertionHeaderPolicy: SystemOfRecordAssertionHeaderPolicy.KEY_ID_SUPPLIED,\n assertionSigningAlgorithm: SystemOfRecordAssertionSigningAlgorithm.RS256,\n assertionLifetimeSeconds: 300,\n // Its token answer states `expires_in` is always 3600, so this fallback is a floor that should\n // never be reached rather than a guess at the real lifetime.\n tokenLifetimeFallbackSeconds: 3600,\n },\n ],\n};\n"]}
@@ -0,0 +1,3 @@
1
+ import type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';
2
+ export declare const OdataSystemOfRecordFacet: SystemOfRecordVendorFacet;
3
+ //# sourceMappingURL=odata.system-of-record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"odata.system-of-record.d.ts","sourceRoot":"","sources":["../../../src/odata.system-of-record.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AAQrF,eAAO,MAAM,wBAAwB,EAAE,yBAsEtC,CAAC"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The GENERIC OData v4 facet — for a system of record this catalogue has never heard of.
3
+ *
4
+ * Every other facet beside this one names a vendor and encodes what that vendor is known to do.
5
+ * This one names none, and that is its purpose: an enterprise's own OData v4 service — a bespoke
6
+ * middleware, an internal ERP surface, a vendor nobody catalogued — is the ordinary case for an
7
+ * internal tool, and without this facet such a deployment cannot be declared a system of record at
8
+ * all.
9
+ *
10
+ * ## What it can and cannot know
11
+ *
12
+ * It declares the widest credential set, because it has no vendor to narrow it to; the INSTANCE
13
+ * chooses one kind, and boot refuses a kind this list does not offer. It declares OData v4 only,
14
+ * because that is the one dialect its name asserts. It declares no partition field, because a
15
+ * generic service has none to declare.
16
+ *
17
+ * The consequence an author should know: naming `odata` gives up every vendor-specific correction —
18
+ * the paging mode, the header a key travels in, the query options a Web API refuses. A vendor with
19
+ * its own facet is always the better answer when one exists.
20
+ *
21
+ * ## `$skip` paging is assumed, and that is a real limitation
22
+ *
23
+ * The read-through LIST compiles `$top`/`$skip` (`odata-query-compiler.backend.ts`). OData v4
24
+ * defines both, so a conformant service serves them — but at least one large implementation
25
+ * (Dataverse) refuses `$skip` outright, which is why that vendor needs its own facet rather than
26
+ * this one.
27
+ * spec-cite: OData Version 4.01 Part 1: Protocol §11.2.6 ($top and $skip system query options).
28
+ */
29
+ import { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';
30
+ import { ExternalDataIncrementalExtractionMode, ODATA_ENTITY_TAG_CONTROL_INFORMATION, SystemOfRecordActionCallShape } from '@wildo-ai/saas-models/external-data';
31
+ import { SystemOfRecordAssertionHeaderPolicy, SystemOfRecordAssertionSigningAlgorithm, SystemOfRecordClientAuthenticationPlacement, } from '@wildo-ai/saas-models/external-data';
32
+ export const OdataSystemOfRecordFacet = {
33
+ vendor: 'odata',
34
+ dialects: [HttpApiTransportDialect.ODATA],
35
+ /*
36
+ * #1722 — both protocol mechanisms, because the generic facet names no vendor and can narrow nothing: a modified-at
37
+ * filter is plain OData, and change tracking is an optional protocol feature (`Prefer: track-changes`) a service
38
+ * may or may not offer. A service that does not track changes answers the first delta read without a delta link,
39
+ * and that pass is refused by name.
40
+ */
41
+ incrementalExtractionModes: [ExternalDataIncrementalExtractionMode.ODATA_MODIFIED_AT, ExternalDataIncrementalExtractionMode.ODATA_DELTA_LINK],
42
+ /*
43
+ * #1678 — actions are part of the OData v4 PROTOCOL rather than of any one vendor, so the generic facet lists both
44
+ * invocation forms. Whether a given service exposes a given action is the declaring binding's to know; the engine
45
+ * only guarantees it calls it the way the protocol defines.
46
+ *
47
+ * spec-cite: OData Version 4.01 Part 1: Protocol §11.5.5.1 (Invoking an Action).
48
+ */
49
+ actionCallShapes: [SystemOfRecordActionCallShape.ODATA_BOUND_ACTION, SystemOfRecordActionCallShape.ODATA_UNBOUND_ACTION],
50
+ /*
51
+ * #1726 — entity tags are part of the OData v4 PROTOCOL: each entity has one that "MUST change when structural
52
+ * properties or links from that entity have changed", and a write carrying it as `If-Match` is refused with 412 when it
53
+ * no longer matches. So the generic facet names the tag as its row version, which is what lets a binding declare
54
+ * `writeConcurrency: VERSION_CHECKED` and have the SERVICE refuse a stale update.
55
+ *
56
+ * Whether a given service serves a tag for a given entity set is the declaring binding's to know, exactly as for an
57
+ * action. A row served without one is refused by name (`WRITE_ROW_VERSION_NOT_SERVED`) rather than compared with
58
+ * nothing, and a write to such a row still carries `If-Match: *`, which forbids an upsert re-creating it.
59
+ *
60
+ * spec-cite: OData Version 4.01 Part 1: Protocol §11.4.1.1 (Use of ETags for Avoiding Update Conflicts) and §8.2.4
61
+ * (Header If-Match).
62
+ */
63
+ rowVersionField: ODATA_ENTITY_TAG_CONTROL_INFORMATION,
64
+ addressSlots: [
65
+ {
66
+ slug: 'baseUrl',
67
+ required: true,
68
+ description: "The OData SERVICE ROOT, not the host — the URL an entity-set name is appended to "
69
+ + '(e.g. https://erp.example/odata/v4). Everything this facet compiles is relative to it.',
70
+ },
71
+ ],
72
+ credentialOffers: [
73
+ // Bearer is the common shape for a generic service, so it leads. `valuePrefix` carries its own
74
+ // trailing space: the prefix is concatenated verbatim, which is what lets a vendor that wants a
75
+ // bare key declare none at all.
76
+ { kind: SystemOfRecordCredentialKind.API_KEY_HEADER, headerName: 'Authorization', valuePrefix: 'Bearer ' },
77
+ { kind: SystemOfRecordCredentialKind.BASIC },
78
+ {
79
+ kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_SECRET,
80
+ // RFC 6749 §2.3.1 says a server MUST support the Basic header and MAY support the body form,
81
+ // so the header is the one a generic facet can rely on.
82
+ // spec-cite: RFC 6749 §2.3.1 (Client Password).
83
+ clientAuthentication: SystemOfRecordClientAuthenticationPlacement.BASIC_HEADER,
84
+ // RFC 6749 §4.2.2 makes `expires_in` RECOMMENDED rather than required, so a generic facet
85
+ // needs a fallback. Fifteen minutes: short enough that a server issuing something shorter is
86
+ // re-minted before it dies, long enough not to mint on every read.
87
+ // spec-cite: RFC 6749 §4.2.2 (Access Token Response).
88
+ tokenLifetimeFallbackSeconds: 900,
89
+ },
90
+ {
91
+ kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_CERTIFICATE,
92
+ // A generic service cannot be assumed to issue key ids, so the thumbprint policy — derivable
93
+ // from the certificate the operator already supplies — is the one that needs nothing from the
94
+ // vendor.
95
+ assertionHeaderPolicy: SystemOfRecordAssertionHeaderPolicy.X509_THUMBPRINT_DERIVED,
96
+ assertionSigningAlgorithm: SystemOfRecordAssertionSigningAlgorithm.RS256,
97
+ assertionLifetimeSeconds: 300,
98
+ tokenLifetimeFallbackSeconds: 900,
99
+ },
100
+ ],
101
+ };
102
+ //# sourceMappingURL=odata.system-of-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"odata.system-of-record.js","sourceRoot":"","sources":["../../../src/odata.system-of-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAE9F,OAAO,EAAE,qCAAqC,EAAE,oCAAoC,EAAE,6BAA6B,EAAE,MAAM,qCAAqC,CAAC;AACjK,OAAO,EACL,mCAAmC,EACnC,uCAAuC,EACvC,2CAA2C,GAC5C,MAAM,qCAAqC,CAAC;AAE7C,MAAM,CAAC,MAAM,wBAAwB,GAA8B;IACjE,MAAM,EAAE,OAAO;IACf,QAAQ,EAAE,CAAC,uBAAuB,CAAC,KAAK,CAAC;IACzC;;;;;OAKG;IACH,0BAA0B,EAAE,CAAC,qCAAqC,CAAC,iBAAiB,EAAE,qCAAqC,CAAC,gBAAgB,CAAC;IAC7I;;;;;;OAMG;IACH,gBAAgB,EAAE,CAAC,6BAA6B,CAAC,kBAAkB,EAAE,6BAA6B,CAAC,oBAAoB,CAAC;IACxH;;;;;;;;;;;;OAYG;IACH,eAAe,EAAE,oCAAoC;IACrD,YAAY,EAAE;QACZ;YACE,IAAI,EAAE,SAAS;YACf,QAAQ,EAAE,IAAI;YACd,WAAW,EACT,mFAAmF;kBACjF,wFAAwF;SAC7F;KACF;IACD,gBAAgB,EAAE;QAChB,+FAA+F;QAC/F,gGAAgG;QAChG,gCAAgC;QAChC,EAAE,IAAI,EAAE,4BAA4B,CAAC,cAAc,EAAE,UAAU,EAAE,eAAe,EAAE,WAAW,EAAE,SAAS,EAAE;QAC1G,EAAE,IAAI,EAAE,4BAA4B,CAAC,KAAK,EAAE;QAC5C;YACE,IAAI,EAAE,4BAA4B,CAAC,gCAAgC;YACnE,6FAA6F;YAC7F,wDAAwD;YACxD,gDAAgD;YAChD,oBAAoB,EAAE,2CAA2C,CAAC,YAAY;YAC9E,0FAA0F;YAC1F,6FAA6F;YAC7F,mEAAmE;YACnE,sDAAsD;YACtD,4BAA4B,EAAE,GAAG;SAClC;QACD;YACE,IAAI,EAAE,4BAA4B,CAAC,qCAAqC;YACxE,6FAA6F;YAC7F,8FAA8F;YAC9F,UAAU;YACV,qBAAqB,EAAE,mCAAmC,CAAC,uBAAuB;YAClF,yBAAyB,EAAE,uCAAuC,CAAC,KAAK;YACxE,wBAAwB,EAAE,GAAG;YAC7B,4BAA4B,EAAE,GAAG;SAClC;KACF;CACF,CAAC","sourcesContent":["/**\n * The GENERIC OData v4 facet — for a system of record this catalogue has never heard of.\n *\n * Every other facet beside this one names a vendor and encodes what that vendor is known to do.\n * This one names none, and that is its purpose: an enterprise's own OData v4 service — a bespoke\n * middleware, an internal ERP surface, a vendor nobody catalogued — is the ordinary case for an\n * internal tool, and without this facet such a deployment cannot be declared a system of record at\n * all.\n *\n * ## What it can and cannot know\n *\n * It declares the widest credential set, because it has no vendor to narrow it to; the INSTANCE\n * chooses one kind, and boot refuses a kind this list does not offer. It declares OData v4 only,\n * because that is the one dialect its name asserts. It declares no partition field, because a\n * generic service has none to declare.\n *\n * The consequence an author should know: naming `odata` gives up every vendor-specific correction —\n * the paging mode, the header a key travels in, the query options a Web API refuses. A vendor with\n * its own facet is always the better answer when one exists.\n *\n * ## `$skip` paging is assumed, and that is a real limitation\n *\n * The read-through LIST compiles `$top`/`$skip` (`odata-query-compiler.backend.ts`). OData v4\n * defines both, so a conformant service serves them — but at least one large implementation\n * (Dataverse) refuses `$skip` outright, which is why that vendor needs its own facet rather than\n * this one.\n * spec-cite: OData Version 4.01 Part 1: Protocol §11.2.6 ($top and $skip system query options).\n */\nimport { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';\nimport type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';\nimport { ExternalDataIncrementalExtractionMode, ODATA_ENTITY_TAG_CONTROL_INFORMATION, SystemOfRecordActionCallShape } from '@wildo-ai/saas-models/external-data';\nimport {\n SystemOfRecordAssertionHeaderPolicy,\n SystemOfRecordAssertionSigningAlgorithm,\n SystemOfRecordClientAuthenticationPlacement,\n} from '@wildo-ai/saas-models/external-data';\n\nexport const OdataSystemOfRecordFacet: SystemOfRecordVendorFacet = {\n vendor: 'odata',\n dialects: [HttpApiTransportDialect.ODATA],\n /*\n * #1722 — both protocol mechanisms, because the generic facet names no vendor and can narrow nothing: a modified-at\n * filter is plain OData, and change tracking is an optional protocol feature (`Prefer: track-changes`) a service\n * may or may not offer. A service that does not track changes answers the first delta read without a delta link,\n * and that pass is refused by name.\n */\n incrementalExtractionModes: [ExternalDataIncrementalExtractionMode.ODATA_MODIFIED_AT, ExternalDataIncrementalExtractionMode.ODATA_DELTA_LINK],\n /*\n * #1678 — actions are part of the OData v4 PROTOCOL rather than of any one vendor, so the generic facet lists both\n * invocation forms. Whether a given service exposes a given action is the declaring binding's to know; the engine\n * only guarantees it calls it the way the protocol defines.\n *\n * spec-cite: OData Version 4.01 Part 1: Protocol §11.5.5.1 (Invoking an Action).\n */\n actionCallShapes: [SystemOfRecordActionCallShape.ODATA_BOUND_ACTION, SystemOfRecordActionCallShape.ODATA_UNBOUND_ACTION],\n /*\n * #1726 — entity tags are part of the OData v4 PROTOCOL: each entity has one that \"MUST change when structural\n * properties or links from that entity have changed\", and a write carrying it as `If-Match` is refused with 412 when it\n * no longer matches. So the generic facet names the tag as its row version, which is what lets a binding declare\n * `writeConcurrency: VERSION_CHECKED` and have the SERVICE refuse a stale update.\n *\n * Whether a given service serves a tag for a given entity set is the declaring binding's to know, exactly as for an\n * action. A row served without one is refused by name (`WRITE_ROW_VERSION_NOT_SERVED`) rather than compared with\n * nothing, and a write to such a row still carries `If-Match: *`, which forbids an upsert re-creating it.\n *\n * spec-cite: OData Version 4.01 Part 1: Protocol §11.4.1.1 (Use of ETags for Avoiding Update Conflicts) and §8.2.4\n * (Header If-Match).\n */\n rowVersionField: ODATA_ENTITY_TAG_CONTROL_INFORMATION,\n addressSlots: [\n {\n slug: 'baseUrl',\n required: true,\n description:\n \"The OData SERVICE ROOT, not the host — the URL an entity-set name is appended to \"\n + '(e.g. https://erp.example/odata/v4). Everything this facet compiles is relative to it.',\n },\n ],\n credentialOffers: [\n // Bearer is the common shape for a generic service, so it leads. `valuePrefix` carries its own\n // trailing space: the prefix is concatenated verbatim, which is what lets a vendor that wants a\n // bare key declare none at all.\n { kind: SystemOfRecordCredentialKind.API_KEY_HEADER, headerName: 'Authorization', valuePrefix: 'Bearer ' },\n { kind: SystemOfRecordCredentialKind.BASIC },\n {\n kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_SECRET,\n // RFC 6749 §2.3.1 says a server MUST support the Basic header and MAY support the body form,\n // so the header is the one a generic facet can rely on.\n // spec-cite: RFC 6749 §2.3.1 (Client Password).\n clientAuthentication: SystemOfRecordClientAuthenticationPlacement.BASIC_HEADER,\n // RFC 6749 §4.2.2 makes `expires_in` RECOMMENDED rather than required, so a generic facet\n // needs a fallback. Fifteen minutes: short enough that a server issuing something shorter is\n // re-minted before it dies, long enough not to mint on every read.\n // spec-cite: RFC 6749 §4.2.2 (Access Token Response).\n tokenLifetimeFallbackSeconds: 900,\n },\n {\n kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_CERTIFICATE,\n // A generic service cannot be assumed to issue key ids, so the thumbprint policy — derivable\n // from the certificate the operator already supplies — is the one that needs nothing from the\n // vendor.\n assertionHeaderPolicy: SystemOfRecordAssertionHeaderPolicy.X509_THUMBPRINT_DERIVED,\n assertionSigningAlgorithm: SystemOfRecordAssertionSigningAlgorithm.RS256,\n assertionLifetimeSeconds: 300,\n tokenLifetimeFallbackSeconds: 900,\n },\n ],\n};\n"]}
@@ -0,0 +1,3 @@
1
+ import type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';
2
+ export declare const OdooSystemOfRecordFacet: SystemOfRecordVendorFacet;
3
+ //# sourceMappingURL=odoo.system-of-record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"odoo.system-of-record.d.ts","sourceRoot":"","sources":["../../../src/odoo.system-of-record.ts"],"names":[],"mappings":"AA0DA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AASrF,eAAO,MAAM,uBAAuB,EAAE,yBA+DrC,CAAC"}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Odoo, as a SYSTEM OF RECORD — moved here from the engine (#1057 E9).
3
+ *
4
+ * ## Why it moved
5
+ *
6
+ * #1055 shipped this facet inside `engine/external-connectors-private` by decision, with its own
7
+ * docstring calling the arrangement temporary and the engine's facet map one that "should shrink to
8
+ * empty". A vendor facet is a PACKAGE's to own: the engine has no business knowing what an Odoo
9
+ * needs, any more than it knows what a Salesforce needs, and an engine-resident facet is a vendor
10
+ * every application carries whether or not it declared one.
11
+ *
12
+ * ## Why it is AUTHORED rather than generated
13
+ *
14
+ * Odoo has no scraped source — the catalogue record has an `odoo` entry that produced nothing, and
15
+ * the transposer excludes engine-owned refs by construction. More fundamentally, nothing a scrape
16
+ * yields could produce this file: a scraped source gives a base URL and an auth kind, and never
17
+ * "JSON-RPC over `/jsonrpc`, credentials inside the RPC arguments". Dialect and credential kind are
18
+ * always authored, for every vendor.
19
+ *
20
+ * ## Why the slots are what they are
21
+ *
22
+ * `baseUrl` is registration-scoped and never credential material: each enterprise runs its own
23
+ * Odoo, so the URL comes from the deployment environment.
24
+ *
25
+ * `database` and `login` are VENDOR address slots even though `ODOO_API_KEY_IN_ARGS` consumes them:
26
+ * they identify WHICH Odoo database and WHICH user, which stays true under any other credential
27
+ * kind Odoo might later offer. Only `apiKey` belongs to the kind, and it is declared there.
28
+ *
29
+ * ## Its PARTITION is `company_id`, and every part of the declaration was measured
30
+ *
31
+ * Odoo holds several COMPANIES inside one database, and `company_id` is on the row. This is the
32
+ * FIRST vendor facet to declare a partition: #1058 built the whole engine-side
33
+ * vocabulary and no facet had ever used it, so `partition: true` on an instance was refused by
34
+ * `PARTITION_NOT_SUPPORTED_BY_VENDOR` for every vendor we ship. The two neighbouring facets that
35
+ * mention partitioning (Dataverse, Business Central) say only that the vocabulary did not exist
36
+ * when they were written; Business Central's remains true for a different reason, its partition
37
+ * being a PATH SEGMENT this engine deliberately cannot confine.
38
+ *
39
+ * Measured 2026-09-22 against a REAL Odoo (a 20-company instance), which is what settles the two
40
+ * fields a reader would otherwise have to guess at:
41
+ *
42
+ * - `valueShape` is `MANY2ONE_PAIR` because a many2one reads back as `[1, "Odoo S.A."]`, never as
43
+ * a bare id. A reader taking the whole value matches nothing; one coercing it to a scalar gets
44
+ * `"1,Odoo S.A."`. The id is the first element.
45
+ * - `codec` is `INTEGER` because the domain is compared by id and Odoo is STRICT about the type:
46
+ * `["company_id", "in", [1]]` matches, and `["company_id", "in", ["1"]]` matches NOTHING and
47
+ * raises no error. A string key would therefore confine a member to an empty result set while
48
+ * looking like a correct narrowing — silence being the dangerous half.
49
+ *
50
+ * spec-cite: N/A — both bullets are this repository's own measurement against a live instance, not
51
+ * a quotation. Odoo's External API documentation states the many2one read shape; it does not state
52
+ * the domain's type strictness, which is why that half was probed rather than cited.
53
+ *
54
+ * An unset `company_id` reads back as `false` (Odoo's spelling of "no value"), which the engine
55
+ * already reads as the SHARED row that belongs to every partition
56
+ * (`http-api-partition-confinement.backend.ts:266-273`).
57
+ */
58
+ import { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';
59
+ import { SystemOfRecordActionCallShape } from '@wildo-ai/saas-models/external-data';
60
+ import { ExternalDataIncrementalExtractionMode, HttpApiKeyComponentCodec, SystemOfRecordPartitionKind, SystemOfRecordPartitionValueShape, } from '@wildo-ai/saas-models/external-data';
61
+ export const OdooSystemOfRecordFacet = {
62
+ vendor: 'odoo',
63
+ dialects: [HttpApiTransportDialect.ODOO_JSONRPC],
64
+ addressSlots: [
65
+ {
66
+ slug: 'baseUrl',
67
+ required: true,
68
+ description: "This instance's Odoo URL (e.g. https://mycompany.odoo.com). Deployment-targeted: each "
69
+ + 'environment names ITS Odoo, and there is no fixed SaaS host to fall back on.',
70
+ },
71
+ {
72
+ slug: 'database',
73
+ required: true,
74
+ description: 'The Odoo database name on that instance — the first positional argument of every '
75
+ + '`execute_kw` call, and part of the `common.authenticate` handshake.',
76
+ },
77
+ {
78
+ slug: 'login',
79
+ required: true,
80
+ description: 'The Odoo user login the API key belongs to. `common.authenticate` resolves it to the uid '
81
+ + 'that every `execute_kw` call then acts as, so it decides what the application can read.',
82
+ },
83
+ ],
84
+ credentialOffers: [{ kind: SystemOfRecordCredentialKind.ODOO_API_KEY_IN_ARGS }],
85
+ /*
86
+ * #1678 — Odoo's external API calls ANY public model method through `execute_kw`, so a declared action
87
+ * (`account.move` `action_post`, `sale.order` `action_confirm`, a model-level `create`) is one call shape.
88
+ * Odoo offers no idempotency mechanism for such a call, so no Odoo action is ever retry-safe.
89
+ *
90
+ * spec-cite: Odoo 18.0 developer documentation, External API — "Calling methods".
91
+ */
92
+ actionCallShapes: [SystemOfRecordActionCallShape.ODOO_EXECUTE_KW],
93
+ partition: {
94
+ kind: SystemOfRecordPartitionKind.FIELD,
95
+ remoteField: 'company_id',
96
+ codec: HttpApiKeyComponentCodec.INTEGER,
97
+ valueShape: SystemOfRecordPartitionValueShape.MANY2ONE_PAIR,
98
+ },
99
+ /*
100
+ * #1682 — every Odoo model that logs access carries `write_date` ("Last Updated on"), maintained by
101
+ * the ORM on every write, so a pipeline can read what changed since a watermark. A model declared
102
+ * `_log_access = False` has no such column; a pipeline over one is refused at its first incremental
103
+ * page, which names the missing field, rather than here — this facet describes the vendor, not
104
+ * each of its models.
105
+ */
106
+ incrementalExtractionModes: [ExternalDataIncrementalExtractionMode.ODOO_WRITE_DATE],
107
+ /*
108
+ * #1724 — the same ORM-maintained `write_date` is the row's VERSION: every `write` stamps it, and no caller can
109
+ * forget to. It is what a `writeConcurrency: VERSION_CHECKED` binding compares before sending an update.
110
+ *
111
+ * Best-effort, and stated where the vendor is described: Odoo 17's `BaseModel.write` takes no precondition, so the
112
+ * engine can only read the version and then write — two calls, with a race between them. Odoo also SERVES the value
113
+ * cut to the second (`Datetime.to_string`) while storing microseconds, so two changes inside one second read as one
114
+ * version. The check narrows the lost-update window; it never closes it.
115
+ *
116
+ * spec-cite: Odoo 17.0 `odoo/models.py` — `BaseModel.write(self, vals)` ("Updates all records in ``self`` with the
117
+ * provided values", no version argument) and `_write` (`vals.setdefault('write_date', self.env.cr.now())`);
118
+ * `odoo/fields.py` `Datetime.to_string` for the served precision (https://github.com/odoo/odoo/tree/17.0/odoo).
119
+ */
120
+ rowVersionField: 'write_date',
121
+ };
122
+ //# sourceMappingURL=odoo.system-of-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"odoo.system-of-record.js","sourceRoot":"","sources":["../../../src/odoo.system-of-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,OAAO,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAE9F,OAAO,EAAE,6BAA6B,EAAE,MAAM,qCAAqC,CAAC;AACpF,OAAO,EACL,qCAAqC,EACrC,wBAAwB,EACxB,2BAA2B,EAC3B,iCAAiC,GAClC,MAAM,qCAAqC,CAAC;AAE7C,MAAM,CAAC,MAAM,uBAAuB,GAA8B;IAChE,MAAM,EAAE,MAAM;IACd,QAAQ,EAAE,CAAC,uBAAuB,CAAC,YAAY,CAAC;IAChD,YAAY,EAAE;QACZ;YACE,IAAI,EAAE,SAAS;YACf,QAAQ,EAAE,IAAI;YACd,WAAW,EACT,wFAAwF;kBACtF,8EAA8E;SACnF;QACD;YACE,IAAI,EAAE,UAAU;YAChB,QAAQ,EAAE,IAAI;YACd,WAAW,EACT,mFAAmF;kBACjF,qEAAqE;SAC1E;QACD;YACE,IAAI,EAAE,OAAO;YACb,QAAQ,EAAE,IAAI;YACd,WAAW,EACT,2FAA2F;kBACzF,yFAAyF;SAC9F;KACF;IACD,gBAAgB,EAAE,CAAC,EAAE,IAAI,EAAE,4BAA4B,CAAC,oBAAoB,EAAE,CAAC;IAC/E;;;;;;OAMG;IACH,gBAAgB,EAAE,CAAC,6BAA6B,CAAC,eAAe,CAAC;IACjE,SAAS,EAAE;QACT,IAAI,EAAE,2BAA2B,CAAC,KAAK;QACvC,WAAW,EAAE,YAAY;QACzB,KAAK,EAAE,wBAAwB,CAAC,OAAO;QACvC,UAAU,EAAE,iCAAiC,CAAC,aAAa;KAC5D;IACD;;;;;;OAMG;IACH,0BAA0B,EAAE,CAAC,qCAAqC,CAAC,eAAe,CAAC;IACnF;;;;;;;;;;;;OAYG;IACH,eAAe,EAAE,YAAY;CAC9B,CAAC","sourcesContent":["/**\n * Odoo, as a SYSTEM OF RECORD — moved here from the engine (#1057 E9).\n *\n * ## Why it moved\n *\n * #1055 shipped this facet inside `engine/external-connectors-private` by decision, with its own\n * docstring calling the arrangement temporary and the engine's facet map one that \"should shrink to\n * empty\". A vendor facet is a PACKAGE's to own: the engine has no business knowing what an Odoo\n * needs, any more than it knows what a Salesforce needs, and an engine-resident facet is a vendor\n * every application carries whether or not it declared one.\n *\n * ## Why it is AUTHORED rather than generated\n *\n * Odoo has no scraped source — the catalogue record has an `odoo` entry that produced nothing, and\n * the transposer excludes engine-owned refs by construction. More fundamentally, nothing a scrape\n * yields could produce this file: a scraped source gives a base URL and an auth kind, and never\n * \"JSON-RPC over `/jsonrpc`, credentials inside the RPC arguments\". Dialect and credential kind are\n * always authored, for every vendor.\n *\n * ## Why the slots are what they are\n *\n * `baseUrl` is registration-scoped and never credential material: each enterprise runs its own\n * Odoo, so the URL comes from the deployment environment.\n *\n * `database` and `login` are VENDOR address slots even though `ODOO_API_KEY_IN_ARGS` consumes them:\n * they identify WHICH Odoo database and WHICH user, which stays true under any other credential\n * kind Odoo might later offer. Only `apiKey` belongs to the kind, and it is declared there.\n *\n * ## Its PARTITION is `company_id`, and every part of the declaration was measured\n *\n * Odoo holds several COMPANIES inside one database, and `company_id` is on the row. This is the\n * FIRST vendor facet to declare a partition: #1058 built the whole engine-side\n * vocabulary and no facet had ever used it, so `partition: true` on an instance was refused by\n * `PARTITION_NOT_SUPPORTED_BY_VENDOR` for every vendor we ship. The two neighbouring facets that\n * mention partitioning (Dataverse, Business Central) say only that the vocabulary did not exist\n * when they were written; Business Central's remains true for a different reason, its partition\n * being a PATH SEGMENT this engine deliberately cannot confine.\n *\n * Measured 2026-09-22 against a REAL Odoo (a 20-company instance), which is what settles the two\n * fields a reader would otherwise have to guess at:\n *\n * - `valueShape` is `MANY2ONE_PAIR` because a many2one reads back as `[1, \"Odoo S.A.\"]`, never as\n * a bare id. A reader taking the whole value matches nothing; one coercing it to a scalar gets\n * `\"1,Odoo S.A.\"`. The id is the first element.\n * - `codec` is `INTEGER` because the domain is compared by id and Odoo is STRICT about the type:\n * `[\"company_id\", \"in\", [1]]` matches, and `[\"company_id\", \"in\", [\"1\"]]` matches NOTHING and\n * raises no error. A string key would therefore confine a member to an empty result set while\n * looking like a correct narrowing — silence being the dangerous half.\n *\n * spec-cite: N/A — both bullets are this repository's own measurement against a live instance, not\n * a quotation. Odoo's External API documentation states the many2one read shape; it does not state\n * the domain's type strictness, which is why that half was probed rather than cited.\n *\n * An unset `company_id` reads back as `false` (Odoo's spelling of \"no value\"), which the engine\n * already reads as the SHARED row that belongs to every partition\n * (`http-api-partition-confinement.backend.ts:266-273`).\n */\nimport { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';\nimport type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';\nimport { SystemOfRecordActionCallShape } from '@wildo-ai/saas-models/external-data';\nimport {\n ExternalDataIncrementalExtractionMode,\n HttpApiKeyComponentCodec,\n SystemOfRecordPartitionKind,\n SystemOfRecordPartitionValueShape,\n} from '@wildo-ai/saas-models/external-data';\n\nexport const OdooSystemOfRecordFacet: SystemOfRecordVendorFacet = {\n vendor: 'odoo',\n dialects: [HttpApiTransportDialect.ODOO_JSONRPC],\n addressSlots: [\n {\n slug: 'baseUrl',\n required: true,\n description:\n \"This instance's Odoo URL (e.g. https://mycompany.odoo.com). Deployment-targeted: each \"\n + 'environment names ITS Odoo, and there is no fixed SaaS host to fall back on.',\n },\n {\n slug: 'database',\n required: true,\n description:\n 'The Odoo database name on that instance — the first positional argument of every '\n + '`execute_kw` call, and part of the `common.authenticate` handshake.',\n },\n {\n slug: 'login',\n required: true,\n description:\n 'The Odoo user login the API key belongs to. `common.authenticate` resolves it to the uid '\n + 'that every `execute_kw` call then acts as, so it decides what the application can read.',\n },\n ],\n credentialOffers: [{ kind: SystemOfRecordCredentialKind.ODOO_API_KEY_IN_ARGS }],\n /*\n * #1678 — Odoo's external API calls ANY public model method through `execute_kw`, so a declared action\n * (`account.move` `action_post`, `sale.order` `action_confirm`, a model-level `create`) is one call shape.\n * Odoo offers no idempotency mechanism for such a call, so no Odoo action is ever retry-safe.\n *\n * spec-cite: Odoo 18.0 developer documentation, External API — \"Calling methods\".\n */\n actionCallShapes: [SystemOfRecordActionCallShape.ODOO_EXECUTE_KW],\n partition: {\n kind: SystemOfRecordPartitionKind.FIELD,\n remoteField: 'company_id',\n codec: HttpApiKeyComponentCodec.INTEGER,\n valueShape: SystemOfRecordPartitionValueShape.MANY2ONE_PAIR,\n },\n /*\n * #1682 — every Odoo model that logs access carries `write_date` (\"Last Updated on\"), maintained by\n * the ORM on every write, so a pipeline can read what changed since a watermark. A model declared\n * `_log_access = False` has no such column; a pipeline over one is refused at its first incremental\n * page, which names the missing field, rather than here — this facet describes the vendor, not\n * each of its models.\n */\n incrementalExtractionModes: [ExternalDataIncrementalExtractionMode.ODOO_WRITE_DATE],\n /*\n * #1724 — the same ORM-maintained `write_date` is the row's VERSION: every `write` stamps it, and no caller can\n * forget to. It is what a `writeConcurrency: VERSION_CHECKED` binding compares before sending an update.\n *\n * Best-effort, and stated where the vendor is described: Odoo 17's `BaseModel.write` takes no precondition, so the\n * engine can only read the version and then write — two calls, with a race between them. Odoo also SERVES the value\n * cut to the second (`Datetime.to_string`) while storing microseconds, so two changes inside one second read as one\n * version. The check narrows the lost-update window; it never closes it.\n *\n * spec-cite: Odoo 17.0 `odoo/models.py` — `BaseModel.write(self, vals)` (\"Updates all records in ``self`` with the\n * provided values\", no version argument) and `_write` (`vals.setdefault('write_date', self.env.cr.now())`);\n * `odoo/fields.py` `Datetime.to_string` for the served precision (https://github.com/odoo/odoo/tree/17.0/odoo).\n */\n rowVersionField: 'write_date',\n};\n"]}
@@ -0,0 +1,3 @@
1
+ import type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';
2
+ export declare const SalesforceSystemOfRecordFacet: SystemOfRecordVendorFacet;
3
+ //# sourceMappingURL=salesforce.system-of-record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"salesforce.system-of-record.d.ts","sourceRoot":"","sources":["../../../src/salesforce.system-of-record.ts"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AAMrF,eAAO,MAAM,6BAA6B,EAAE,yBA0C3C,CAAC"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Salesforce, as a SYSTEM OF RECORD.
3
+ *
4
+ * spec-cite: Salesforce Developers, "REST API Developer Guide" —
5
+ * https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/
6
+ *
7
+ * ## Read through SOQL, which is why this is REST_JSON and not a dialect
8
+ *
9
+ * A collection read is a SOQL statement at `/services/data/vXX.X/query?q=…`, and a page is followed
10
+ * by the answer's own `nextRecordsUrl`. Neither is expressible as a set of filter parameters, which
11
+ * is what #1293 added the statement declaration for. A BINDING supplies the object and its fields;
12
+ * this facet says the vendor speaks REST_JSON and how its service identity authenticates.
13
+ *
14
+ * spec-cite: Salesforce Developers, "SOQL and SOSL Reference" —
15
+ * https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/
16
+ *
17
+ * ## What an author gives up by choosing it, stated rather than discovered
18
+ *
19
+ * **Pipelines walk a declared keyset (#1686).** A Salesforce object feeds a pipeline when the
20
+ * binding's `list` statement declares `keyset: { keyRemoteField: 'Id' }` and its `cursorPath`
21
+ * (`nextRecordsUrl`): the engine reads `… WHERE Id > <last Id> ORDER BY Id ASC LIMIT <n>`, follows
22
+ * the answer's batches to the end of each page, and persists the last Id — never the query locator.
23
+ * Without a keyset the object is read-through only: offset paging is refused for pipelines.
24
+ *
25
+ * **No declared sort.** Resource registration refuses `sortFields` on a REST_JSON binding, so a LIST
26
+ * serves the statement's own order. A binding may still order inside its SOQL, which is the honest
27
+ * place for it — the ordering belongs to the query, not to an advertised capability.
28
+ *
29
+ * ## The instance URL is an ADDRESS, not an account fact
30
+ *
31
+ * An OAuth answer carries `instance_url`, and it is deliberately not read: a system of record is a
32
+ * deployment the operator names in this environment, and taking its address from a token response
33
+ * would make the address a property of the credential. Sandbox and production differ by that URL
34
+ * alone, which is exactly what an address slot is for.
35
+ */
36
+ import { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';
37
+ import { SystemOfRecordAssertionSigningAlgorithm, SystemOfRecordClientAuthenticationPlacement, } from '@wildo-ai/saas-models/external-data';
38
+ export const SalesforceSystemOfRecordFacet = {
39
+ vendor: 'salesforce',
40
+ dialects: [HttpApiTransportDialect.REST_JSON],
41
+ addressSlots: [
42
+ {
43
+ slug: 'baseUrl',
44
+ required: true,
45
+ description: "This org's instance URL (https://<myDomain>.my.salesforce.com). A binding's path templates "
46
+ + 'are appended to it, so it carries no API version — the version belongs to the operation.',
47
+ },
48
+ ],
49
+ credentialOffers: [
50
+ {
51
+ kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_SECRET,
52
+ clientAuthentication: SystemOfRecordClientAuthenticationPlacement.REQUEST_BODY,
53
+ /*
54
+ * R3's case, and the reason the fallback lives on the OFFER. Salesforce's client-credentials
55
+ * answer is reported to omit `expires_in` while NetSuite's always carries it — two vendors of
56
+ * ONE kind, so a per-kind engine constant would have to be wrong for one of them.
57
+ *
58
+ * Two hours: a Salesforce session lives far longer, and this is the interval after which the
59
+ * engine re-mints rather than a claim about the token's real life. Erring short costs a token
60
+ * request; erring long costs every read until the token dies.
61
+ */
62
+ tokenLifetimeFallbackSeconds: 7200,
63
+ },
64
+ {
65
+ /*
66
+ * The assertion GRANT (RFC 7523 §2.1), which among the vendors surveyed is Salesforce alone —
67
+ * NetSuite uses the same assertion for client AUTHENTICATION under `client_credentials`. The
68
+ * two differ in one form field and are two credential kinds for exactly that reason.
69
+ *
70
+ * Its `subject` is the integration user the assertion is made FOR, which is why that slot
71
+ * becomes required under this kind and under no other.
72
+ */
73
+ kind: SystemOfRecordCredentialKind.OAUTH2_JWT_BEARER,
74
+ assertionSigningAlgorithm: SystemOfRecordAssertionSigningAlgorithm.RS256,
75
+ assertionLifetimeSeconds: 180,
76
+ tokenLifetimeFallbackSeconds: 7200,
77
+ },
78
+ ],
79
+ };
80
+ //# sourceMappingURL=salesforce.system-of-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"salesforce.system-of-record.js","sourceRoot":"","sources":["../../../src/salesforce.system-of-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAE9F,OAAO,EACL,uCAAuC,EACvC,2CAA2C,GAC5C,MAAM,qCAAqC,CAAC;AAE7C,MAAM,CAAC,MAAM,6BAA6B,GAA8B;IACtE,MAAM,EAAE,YAAY;IACpB,QAAQ,EAAE,CAAC,uBAAuB,CAAC,SAAS,CAAC;IAC7C,YAAY,EAAE;QACZ;YACE,IAAI,EAAE,SAAS;YACf,QAAQ,EAAE,IAAI;YACd,WAAW,EACT,6FAA6F;kBAC3F,0FAA0F;SAC/F;KACF;IACD,gBAAgB,EAAE;QAChB;YACE,IAAI,EAAE,4BAA4B,CAAC,gCAAgC;YACnE,oBAAoB,EAAE,2CAA2C,CAAC,YAAY;YAC9E;;;;;;;;eAQG;YACH,4BAA4B,EAAE,IAAI;SACnC;QACD;YACE;;;;;;;eAOG;YACH,IAAI,EAAE,4BAA4B,CAAC,iBAAiB;YACpD,yBAAyB,EAAE,uCAAuC,CAAC,KAAK;YACxE,wBAAwB,EAAE,GAAG;YAC7B,4BAA4B,EAAE,IAAI;SACnC;KACF;CACF,CAAC","sourcesContent":["/**\n * Salesforce, as a SYSTEM OF RECORD.\n *\n * spec-cite: Salesforce Developers, \"REST API Developer Guide\" —\n * https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/\n *\n * ## Read through SOQL, which is why this is REST_JSON and not a dialect\n *\n * A collection read is a SOQL statement at `/services/data/vXX.X/query?q=…`, and a page is followed\n * by the answer's own `nextRecordsUrl`. Neither is expressible as a set of filter parameters, which\n * is what #1293 added the statement declaration for. A BINDING supplies the object and its fields;\n * this facet says the vendor speaks REST_JSON and how its service identity authenticates.\n *\n * spec-cite: Salesforce Developers, \"SOQL and SOSL Reference\" —\n * https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/\n *\n * ## What an author gives up by choosing it, stated rather than discovered\n *\n * **Pipelines walk a declared keyset (#1686).** A Salesforce object feeds a pipeline when the\n * binding's `list` statement declares `keyset: { keyRemoteField: 'Id' }` and its `cursorPath`\n * (`nextRecordsUrl`): the engine reads `… WHERE Id > <last Id> ORDER BY Id ASC LIMIT <n>`, follows\n * the answer's batches to the end of each page, and persists the last Id — never the query locator.\n * Without a keyset the object is read-through only: offset paging is refused for pipelines.\n *\n * **No declared sort.** Resource registration refuses `sortFields` on a REST_JSON binding, so a LIST\n * serves the statement's own order. A binding may still order inside its SOQL, which is the honest\n * place for it — the ordering belongs to the query, not to an advertised capability.\n *\n * ## The instance URL is an ADDRESS, not an account fact\n *\n * An OAuth answer carries `instance_url`, and it is deliberately not read: a system of record is a\n * deployment the operator names in this environment, and taking its address from a token response\n * would make the address a property of the credential. Sandbox and production differ by that URL\n * alone, which is exactly what an address slot is for.\n */\nimport { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';\nimport type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';\nimport {\n SystemOfRecordAssertionSigningAlgorithm,\n SystemOfRecordClientAuthenticationPlacement,\n} from '@wildo-ai/saas-models/external-data';\n\nexport const SalesforceSystemOfRecordFacet: SystemOfRecordVendorFacet = {\n vendor: 'salesforce',\n dialects: [HttpApiTransportDialect.REST_JSON],\n addressSlots: [\n {\n slug: 'baseUrl',\n required: true,\n description:\n \"This org's instance URL (https://<myDomain>.my.salesforce.com). A binding's path templates \"\n + 'are appended to it, so it carries no API version — the version belongs to the operation.',\n },\n ],\n credentialOffers: [\n {\n kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_SECRET,\n clientAuthentication: SystemOfRecordClientAuthenticationPlacement.REQUEST_BODY,\n /*\n * R3's case, and the reason the fallback lives on the OFFER. Salesforce's client-credentials\n * answer is reported to omit `expires_in` while NetSuite's always carries it — two vendors of\n * ONE kind, so a per-kind engine constant would have to be wrong for one of them.\n *\n * Two hours: a Salesforce session lives far longer, and this is the interval after which the\n * engine re-mints rather than a claim about the token's real life. Erring short costs a token\n * request; erring long costs every read until the token dies.\n */\n tokenLifetimeFallbackSeconds: 7200,\n },\n {\n /*\n * The assertion GRANT (RFC 7523 §2.1), which among the vendors surveyed is Salesforce alone —\n * NetSuite uses the same assertion for client AUTHENTICATION under `client_credentials`. The\n * two differ in one form field and are two credential kinds for exactly that reason.\n *\n * Its `subject` is the integration user the assertion is made FOR, which is why that slot\n * becomes required under this kind and under no other.\n */\n kind: SystemOfRecordCredentialKind.OAUTH2_JWT_BEARER,\n assertionSigningAlgorithm: SystemOfRecordAssertionSigningAlgorithm.RS256,\n assertionLifetimeSeconds: 180,\n tokenLifetimeFallbackSeconds: 7200,\n },\n ],\n};\n"]}
@@ -0,0 +1,3 @@
1
+ import type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';
2
+ export declare const SapS4hanaSystemOfRecordFacet: SystemOfRecordVendorFacet;
3
+ //# sourceMappingURL=sap-s4hana.system-of-record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sap-s4hana.system-of-record.d.ts","sourceRoot":"","sources":["../../../src/sap-s4hana.system-of-record.ts"],"names":[],"mappings":"AAsDA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AAGrF,eAAO,MAAM,4BAA4B,EAAE,yBA4B1C,CAAC"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * SAP S/4HANA, as a SYSTEM OF RECORD.
3
+ *
4
+ * Named `sap-s4hana` rather than `sap-s4hana-cloud`, and that is a measured choice rather than a
5
+ * preference: `sap-s4hana` already exists as a catalogue-record ref and is the `providerRef` three
6
+ * OData fixtures in `saas-models` already use. A second spelling would make the same vendor two
7
+ * vendors, which is what `mergedCatalogueEntries` exists to prevent.
8
+ *
9
+ * ## Only BASIC, and the honest reason
10
+ *
11
+ * S/4HANA's app-only identity is a COMMUNICATION USER, and SAP's own guidance prefers an X.509
12
+ * client certificate over a password for one. That preference cannot be honoured here: presenting a
13
+ * client certificate is a TRANSPORT credential, and the outbound seam carries no channel for one
14
+ * (#1263). So this facet offers the password half only, and an operator whose communication
15
+ * arrangement is certificate-based cannot use it yet.
16
+ *
17
+ * Stated rather than omitted, because the two failure modes read identically from outside: a vendor
18
+ * this facet cannot serve and a vendor nobody has authored look the same to an author holding a
19
+ * refusal.
20
+ *
21
+ * spec-cite: N/A — SAP's certificate preference for communication users is recorded as UNCONFIRMED
22
+ * in epic #1049 §4.6 and is deliberately not quoted here; what is asserted above is only what this
23
+ * facet does, which is a fact about this file.
24
+ *
25
+ * ## OData v2 and v4, one instance per service root (#1687)
26
+ *
27
+ * S/4HANA's most-used API surface (`API_BUSINESS_PARTNER`) is OData **v2**, published beside newer v4
28
+ * services, so this facet speaks both: an instance declares `dialect: ODATA_V2` for a classic Gateway root
29
+ * (`/sap/opu/odata/sap/<SERVICE>`) and `ODATA` for a v4 root (`/sap/opu/odata4/…`). The binding's dialect
30
+ * must match its instance's. A system reached over both is two instances, one per root, each with its own
31
+ * `maxRequestsPerMinute` — the reasoning is at the declaration's `dialect` field.
32
+ *
33
+ * A v2 root declared as `ODATA` is still refused BY NAME at the first read, because #1055 forbids a
34
+ * boot-time probe of the remote and a path heuristic could only ever warn.
35
+ *
36
+ * A v2 instance READS only: the engine takes each property's type from the service's `$metadata`, decodes
37
+ * `/Date(…)/`, decimals and 64-bit integers from their string forms, and a pipeline walks the entity's key
38
+ * rather than `__next` (v2 promises no stability across a partial listing).
39
+ *
40
+ * ## Writes are REFUSED at startup: SAP requires a CSRF-token handshake the engine does not perform (#1726)
41
+ *
42
+ * SAP Gateway protects every modifying request with a token: the client first sends a non-modifying request carrying
43
+ * `X-CSRF-Token: Fetch`, then sends the returned token and the session cookie with the write. The engine's read client
44
+ * keeps no cookie jar and fetches no token, so every S/4HANA write would be answered `403` — hence
45
+ * `writePrerequisites`, and a binding opening writes against this vendor is refused at startup
46
+ * (`WRITE_PREREQUISITE_NOT_SUPPORTED`) instead of failing at its first request.
47
+ * spec-cite: SAP Help Portal, SAP Gateway Foundation — "Cross-Site Request Forgery Protection"
48
+ * (https://help.sap.com/saphelp_gateway20sp12/helpdata/en/e6/cae27d5e8d4996add4067280c8714e/frameset.htm).
49
+ *
50
+ * No row version is declared either: SAP's OData v4 services may serve entity tags per entity type, and nobody here
51
+ * has read that for a service this facet would reach. Both are for whoever performs the handshake; proof against a real
52
+ * service is #1474's.
53
+ */
54
+ import { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';
55
+ import { ExternalDataIncrementalExtractionMode, SystemOfRecordWritePrerequisite } from '@wildo-ai/saas-models/external-data';
56
+ export const SapS4hanaSystemOfRecordFacet = {
57
+ vendor: 'sap-s4hana',
58
+ dialects: [HttpApiTransportDialect.ODATA, HttpApiTransportDialect.ODATA_V2],
59
+ /*
60
+ * #1722 — v4 roots only (the mode is spoken over ODATA). MODIFIED_AT is a plain OData `$filter … ge` on a timestamp property the pipeline declaration names per entity;
61
+ * whether an entity carries one is the declaration's to say, not the vendor's.
62
+ */
63
+ incrementalExtractionModes: [ExternalDataIncrementalExtractionMode.ODATA_MODIFIED_AT],
64
+ // #1726 — see "Writes are REFUSED at startup" above.
65
+ writePrerequisites: [SystemOfRecordWritePrerequisite.CSRF_TOKEN_HANDSHAKE],
66
+ addressSlots: [
67
+ {
68
+ slug: 'baseUrl',
69
+ required: true,
70
+ description: 'The OData service root for this instance: https://<host>/sap/opu/odata4/sap/<service>/… for a v4 service '
71
+ + '(declare dialect ODATA), or https://<host>/sap/opu/odata/sap/<SERVICE> for a classic v2 Gateway service '
72
+ + '(declare dialect ODATA_V2). One root per instance; a system serving both is two instances.',
73
+ },
74
+ ],
75
+ credentialOffers: [
76
+ /*
77
+ * The communication user's password. Its USERNAME is an address slot rather than a secret — it
78
+ * names WHICH user rather than proving anything — and it may not contain `:`, which RFC 7617
79
+ * makes the delimiter; boot refuses one that does.
80
+ */
81
+ { kind: SystemOfRecordCredentialKind.BASIC },
82
+ ],
83
+ };
84
+ //# sourceMappingURL=sap-s4hana.system-of-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sap-s4hana.system-of-record.js","sourceRoot":"","sources":["../../../src/sap-s4hana.system-of-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,OAAO,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAE9F,OAAO,EAAE,qCAAqC,EAAE,+BAA+B,EAAE,MAAM,qCAAqC,CAAC;AAE7H,MAAM,CAAC,MAAM,4BAA4B,GAA8B;IACrE,MAAM,EAAE,YAAY;IACpB,QAAQ,EAAE,CAAC,uBAAuB,CAAC,KAAK,EAAE,uBAAuB,CAAC,QAAQ,CAAC;IAC3E;;;OAGG;IACH,0BAA0B,EAAE,CAAC,qCAAqC,CAAC,iBAAiB,CAAC;IACrF,qDAAqD;IACrD,kBAAkB,EAAE,CAAC,+BAA+B,CAAC,oBAAoB,CAAC;IAC1E,YAAY,EAAE;QACZ;YACE,IAAI,EAAE,SAAS;YACf,QAAQ,EAAE,IAAI;YACd,WAAW,EACT,2GAA2G;kBACzG,0GAA0G;kBAC1G,4FAA4F;SACjG;KACF;IACD,gBAAgB,EAAE;QAChB;;;;WAIG;QACH,EAAE,IAAI,EAAE,4BAA4B,CAAC,KAAK,EAAE;KAC7C;CACF,CAAC","sourcesContent":["/**\n * SAP S/4HANA, as a SYSTEM OF RECORD.\n *\n * Named `sap-s4hana` rather than `sap-s4hana-cloud`, and that is a measured choice rather than a\n * preference: `sap-s4hana` already exists as a catalogue-record ref and is the `providerRef` three\n * OData fixtures in `saas-models` already use. A second spelling would make the same vendor two\n * vendors, which is what `mergedCatalogueEntries` exists to prevent.\n *\n * ## Only BASIC, and the honest reason\n *\n * S/4HANA's app-only identity is a COMMUNICATION USER, and SAP's own guidance prefers an X.509\n * client certificate over a password for one. That preference cannot be honoured here: presenting a\n * client certificate is a TRANSPORT credential, and the outbound seam carries no channel for one\n * (#1263). So this facet offers the password half only, and an operator whose communication\n * arrangement is certificate-based cannot use it yet.\n *\n * Stated rather than omitted, because the two failure modes read identically from outside: a vendor\n * this facet cannot serve and a vendor nobody has authored look the same to an author holding a\n * refusal.\n *\n * spec-cite: N/A — SAP's certificate preference for communication users is recorded as UNCONFIRMED\n * in epic #1049 §4.6 and is deliberately not quoted here; what is asserted above is only what this\n * facet does, which is a fact about this file.\n *\n * ## OData v2 and v4, one instance per service root (#1687)\n *\n * S/4HANA's most-used API surface (`API_BUSINESS_PARTNER`) is OData **v2**, published beside newer v4\n * services, so this facet speaks both: an instance declares `dialect: ODATA_V2` for a classic Gateway root\n * (`/sap/opu/odata/sap/<SERVICE>`) and `ODATA` for a v4 root (`/sap/opu/odata4/…`). The binding's dialect\n * must match its instance's. A system reached over both is two instances, one per root, each with its own\n * `maxRequestsPerMinute` — the reasoning is at the declaration's `dialect` field.\n *\n * A v2 root declared as `ODATA` is still refused BY NAME at the first read, because #1055 forbids a\n * boot-time probe of the remote and a path heuristic could only ever warn.\n *\n * A v2 instance READS only: the engine takes each property's type from the service's `$metadata`, decodes\n * `/Date(…)/`, decimals and 64-bit integers from their string forms, and a pipeline walks the entity's key\n * rather than `__next` (v2 promises no stability across a partial listing).\n *\n * ## Writes are REFUSED at startup: SAP requires a CSRF-token handshake the engine does not perform (#1726)\n *\n * SAP Gateway protects every modifying request with a token: the client first sends a non-modifying request carrying\n * `X-CSRF-Token: Fetch`, then sends the returned token and the session cookie with the write. The engine's read client\n * keeps no cookie jar and fetches no token, so every S/4HANA write would be answered `403` — hence\n * `writePrerequisites`, and a binding opening writes against this vendor is refused at startup\n * (`WRITE_PREREQUISITE_NOT_SUPPORTED`) instead of failing at its first request.\n * spec-cite: SAP Help Portal, SAP Gateway Foundation — \"Cross-Site Request Forgery Protection\"\n * (https://help.sap.com/saphelp_gateway20sp12/helpdata/en/e6/cae27d5e8d4996add4067280c8714e/frameset.htm).\n *\n * No row version is declared either: SAP's OData v4 services may serve entity tags per entity type, and nobody here\n * has read that for a service this facet would reach. Both are for whoever performs the handshake; proof against a real\n * service is #1474's.\n */\nimport { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';\nimport type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';\nimport { ExternalDataIncrementalExtractionMode, SystemOfRecordWritePrerequisite } from '@wildo-ai/saas-models/external-data';\n\nexport const SapS4hanaSystemOfRecordFacet: SystemOfRecordVendorFacet = {\n vendor: 'sap-s4hana',\n dialects: [HttpApiTransportDialect.ODATA, HttpApiTransportDialect.ODATA_V2],\n /*\n * #1722 — v4 roots only (the mode is spoken over ODATA). MODIFIED_AT is a plain OData `$filter … ge` on a timestamp property the pipeline declaration names per entity;\n * whether an entity carries one is the declaration's to say, not the vendor's.\n */\n incrementalExtractionModes: [ExternalDataIncrementalExtractionMode.ODATA_MODIFIED_AT],\n // #1726 — see \"Writes are REFUSED at startup\" above.\n writePrerequisites: [SystemOfRecordWritePrerequisite.CSRF_TOKEN_HANDSHAKE],\n addressSlots: [\n {\n slug: 'baseUrl',\n required: true,\n description:\n 'The OData service root for this instance: https://<host>/sap/opu/odata4/sap/<service>/… for a v4 service '\n + '(declare dialect ODATA), or https://<host>/sap/opu/odata/sap/<SERVICE> for a classic v2 Gateway service '\n + '(declare dialect ODATA_V2). One root per instance; a system serving both is two instances.',\n },\n ],\n credentialOffers: [\n /*\n * The communication user's password. Its USERNAME is an address slot rather than a secret — it\n * names WHICH user rather than proving anything — and it may not contain `:`, which RFC 7617\n * makes the delimiter; boot refuses one that does.\n */\n { kind: SystemOfRecordCredentialKind.BASIC },\n ],\n};\n"]}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The framework's SYSTEM-OF-RECORD FACETS — the public entrypoint behind
3
+ * `@wildo-ai/systems-of-record/systems-of-record` (#1057, moved into its own library by #2013).
4
+ *
5
+ * ## Why a library of its own
6
+ *
7
+ * These facets were born inside the transposed provider catalogue (now archived under `_code_archive/providers/`),
8
+ * under an underscore folder that its regeneration skipped. That kept them alive and nothing more: a system of
9
+ * record is not a provider an application CALLS, it is a remote system that is the authoritative store
10
+ * of some of the application's own resources — declared under `systemsOfRecord`, reached through its
11
+ * instance's service identity only, and never copied into an application (#1049). Paul decided on
12
+ * 2026-09-29 that the lane is delivered as a classical library, and the catalogue was archived
13
+ * (#2012, #2019), so the facets moved here.
14
+ *
15
+ * It is an OPT-IN framework package (`optInFrameworkPackages`, #1836): an application links it only by
16
+ * declaring it, and `wildo config sync` declares it on the backend the moment `systemsOfRecord` is
17
+ * non-empty. An application that declares no system of record never receives it.
18
+ *
19
+ * Discovery admits this package's `./systems-of-record` by its exact NAME; any other facet module is
20
+ * admitted only when an instance opts into it through `facetAugmentations`. The engine code that reads
21
+ * a facet (dialects, pipelines, actions) stays in `@wildo-ai/saas-backend-lib`.
22
+ *
23
+ * ## Why ONE subpath rather than one per vendor
24
+ *
25
+ * The first plan called for a subpath per vendor, `./<ref>/facet`. It does not survive contact with
26
+ * what a facet IS: every field that makes a facet a facet — the dialect, the credential kinds, the
27
+ * partition — is AUTHORED vendor knowledge, and a facet is inert data with no transitive graph. One
28
+ * module holding a record of seven object literals is the whole of it.
29
+ *
30
+ * ## Why a flat record rather than a loader map
31
+ *
32
+ * The engine's map — the one this replaces — used thunks (`vendor -> () => import(...)`) so nothing
33
+ * was evaluated until a vendor was asked for. That cost is real when the map sits in an engine
34
+ * everybody loads. Here it does not apply: this module is reached only when an application declared a
35
+ * system of record at all.
36
+ *
37
+ * ## The export NAMES are the facet-module contract
38
+ *
39
+ * `PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_FACETS` and its siblings keep the names they had in the
40
+ * catalogue because they are what `SYSTEM_OF_RECORD_FACET_RECORD_EXPORT` (saas-models) names, and every
41
+ * augmentation package exports its record under the matching `…_FACET_AUGMENTATIONS`. Renaming them is a
42
+ * contract change for every facet module, not a tidy-up of this file.
43
+ *
44
+ * ## Adding one
45
+ *
46
+ * Author `<vendor>.system-of-record.ts` beside this file and add its row below. The key and the
47
+ * facet's own `vendor` must agree; `systems-of-record.exports.test.ts` asserts it, because a
48
+ * hand-written key beside a declared id is exactly the shape that drifts.
49
+ */
50
+ import type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';
51
+ /**
52
+ * Every vendor the framework can serve as a system of record, keyed by the id an instance names.
53
+ *
54
+ * Far fewer vendors than there are providers, because being reachable over HTTP is not the same as
55
+ * being a deployment an application reads its own rows out of. A vendor absent from here is not
56
+ * refused by this record — it is a vendor the framework does not describe; the generic `odata` facet
57
+ * serves any OData v4 service.
58
+ */
59
+ export declare const PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_FACETS: Readonly<Record<string, SystemOfRecordVendorFacet>>;
60
+ /**
61
+ * Vendor ids that are ANOTHER vendor's, and which one — so a refusal can point rather than just say no.
62
+ *
63
+ * Not misspellings. Each of these is a real provider id, and an author naming one has made a
64
+ * reasonable mistake: the provider catalogue held `microsoft-dataverse`,
65
+ * `microsoft-dynamics-365` and `microsoft-dynamics-crm` as three separate connector refs, and they
66
+ * are three names for ONE Web API. Only one of them can carry the facet, and a refusal that says
67
+ * "no facet" without saying which sibling has it leaves the author to guess between the other two.
68
+ *
69
+ * Lives HERE rather than in the engine for the reason the facets do: which vendor ids are the same
70
+ * product is vendor knowledge, and adding a vendor tomorrow adds its aliases in the same edit.
71
+ *
72
+ * Every value MUST be a key of the facet record above — asserted by `systems-of-record.exports.test.ts`,
73
+ * because an alias pointing at a vendor that has no facet sends the author from one refusal to another.
74
+ */
75
+ export declare const PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_VENDOR_ALIASES: Readonly<Record<string, string>>;
76
+ /** The vendor ids above, sorted — what a sync-time discovery records without loading a facet's shape. */
77
+ export declare const PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_VENDORS: readonly string[];
78
+ //# sourceMappingURL=systems-of-record.exports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"systems-of-record.exports.d.ts","sourceRoot":"","sources":["../../../src/systems-of-record.exports.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AAUrF;;;;;;;GAOG;AACH,eAAO,MAAM,0CAA0C,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAQ1G,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,kDAAkD,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAG/F,CAAC;AAEF,yGAAyG;AACzG,eAAO,MAAM,2CAA2C,EAAE,SAAS,MAAM,EACT,CAAC"}
@@ -0,0 +1,46 @@
1
+ import { MicrosoftDataverseSystemOfRecordFacet } from './microsoft-dataverse.system-of-record.js';
2
+ import { MicrosoftDynamics365BusinessCentralSystemOfRecordFacet } from './microsoft-dynamics-365-business-central.system-of-record.js';
3
+ import { NetsuiteSystemOfRecordFacet } from './netsuite.system-of-record.js';
4
+ import { OdataSystemOfRecordFacet } from './odata.system-of-record.js';
5
+ import { OdooSystemOfRecordFacet } from './odoo.system-of-record.js';
6
+ import { SalesforceSystemOfRecordFacet } from './salesforce.system-of-record.js';
7
+ import { SapS4hanaSystemOfRecordFacet } from './sap-s4hana.system-of-record.js';
8
+ /**
9
+ * Every vendor the framework can serve as a system of record, keyed by the id an instance names.
10
+ *
11
+ * Far fewer vendors than there are providers, because being reachable over HTTP is not the same as
12
+ * being a deployment an application reads its own rows out of. A vendor absent from here is not
13
+ * refused by this record — it is a vendor the framework does not describe; the generic `odata` facet
14
+ * serves any OData v4 service.
15
+ */
16
+ export const PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_FACETS = {
17
+ 'microsoft-dataverse': MicrosoftDataverseSystemOfRecordFacet,
18
+ 'microsoft-dynamics-365-business-central': MicrosoftDynamics365BusinessCentralSystemOfRecordFacet,
19
+ netsuite: NetsuiteSystemOfRecordFacet,
20
+ odata: OdataSystemOfRecordFacet,
21
+ odoo: OdooSystemOfRecordFacet,
22
+ salesforce: SalesforceSystemOfRecordFacet,
23
+ 'sap-s4hana': SapS4hanaSystemOfRecordFacet,
24
+ };
25
+ /**
26
+ * Vendor ids that are ANOTHER vendor's, and which one — so a refusal can point rather than just say no.
27
+ *
28
+ * Not misspellings. Each of these is a real provider id, and an author naming one has made a
29
+ * reasonable mistake: the provider catalogue held `microsoft-dataverse`,
30
+ * `microsoft-dynamics-365` and `microsoft-dynamics-crm` as three separate connector refs, and they
31
+ * are three names for ONE Web API. Only one of them can carry the facet, and a refusal that says
32
+ * "no facet" without saying which sibling has it leaves the author to guess between the other two.
33
+ *
34
+ * Lives HERE rather than in the engine for the reason the facets do: which vendor ids are the same
35
+ * product is vendor knowledge, and adding a vendor tomorrow adds its aliases in the same edit.
36
+ *
37
+ * Every value MUST be a key of the facet record above — asserted by `systems-of-record.exports.test.ts`,
38
+ * because an alias pointing at a vendor that has no facet sends the author from one refusal to another.
39
+ */
40
+ export const PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_VENDOR_ALIASES = {
41
+ 'microsoft-dynamics-365': 'microsoft-dataverse',
42
+ 'microsoft-dynamics-crm': 'microsoft-dataverse',
43
+ };
44
+ /** The vendor ids above, sorted — what a sync-time discovery records without loading a facet's shape. */
45
+ export const PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_VENDORS = Object.keys(PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_FACETS).sort();
46
+ //# sourceMappingURL=systems-of-record.exports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"systems-of-record.exports.js","sourceRoot":"","sources":["../../../src/systems-of-record.exports.ts"],"names":[],"mappings":"AAmDA,OAAO,EAAE,qCAAqC,EAAE,MAAM,wCAAwC,CAAC;AAC/F,OAAO,EAAE,sDAAsD,EAAE,MAAM,4DAA4D,CAAC;AACpI,OAAO,EAAE,2BAA2B,EAAE,MAAM,6BAA6B,CAAC;AAC1E,OAAO,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AAC9E,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAE7E;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,0CAA0C,GAAwD;IAC7G,qBAAqB,EAAE,qCAAqC;IAC5D,yCAAyC,EAAE,sDAAsD;IACjG,QAAQ,EAAE,2BAA2B;IACrC,KAAK,EAAE,wBAAwB;IAC/B,IAAI,EAAE,uBAAuB;IAC7B,UAAU,EAAE,6BAA6B;IACzC,YAAY,EAAE,4BAA4B;CAC3C,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,kDAAkD,GAAqC;IAClG,wBAAwB,EAAE,qBAAqB;IAC/C,wBAAwB,EAAE,qBAAqB;CAChD,CAAC;AAEF,yGAAyG;AACzG,MAAM,CAAC,MAAM,2CAA2C,GACtD,MAAM,CAAC,IAAI,CAAC,0CAA0C,CAAC,CAAC,IAAI,EAAE,CAAC","sourcesContent":["/**\n * The framework's SYSTEM-OF-RECORD FACETS — the public entrypoint behind\n * `@wildo-ai/systems-of-record/systems-of-record` (#1057, moved into its own library by #2013).\n *\n * ## Why a library of its own\n *\n * These facets were born inside the transposed provider catalogue (now archived under `_code_archive/providers/`),\n * under an underscore folder that its regeneration skipped. That kept them alive and nothing more: a system of\n * record is not a provider an application CALLS, it is a remote system that is the authoritative store\n * of some of the application's own resources — declared under `systemsOfRecord`, reached through its\n * instance's service identity only, and never copied into an application (#1049). Paul decided on\n * 2026-09-29 that the lane is delivered as a classical library, and the catalogue was archived\n * (#2012, #2019), so the facets moved here.\n *\n * It is an OPT-IN framework package (`optInFrameworkPackages`, #1836): an application links it only by\n * declaring it, and `wildo config sync` declares it on the backend the moment `systemsOfRecord` is\n * non-empty. An application that declares no system of record never receives it.\n *\n * Discovery admits this package's `./systems-of-record` by its exact NAME; any other facet module is\n * admitted only when an instance opts into it through `facetAugmentations`. The engine code that reads\n * a facet (dialects, pipelines, actions) stays in `@wildo-ai/saas-backend-lib`.\n *\n * ## Why ONE subpath rather than one per vendor\n *\n * The first plan called for a subpath per vendor, `./<ref>/facet`. It does not survive contact with\n * what a facet IS: every field that makes a facet a facet — the dialect, the credential kinds, the\n * partition — is AUTHORED vendor knowledge, and a facet is inert data with no transitive graph. One\n * module holding a record of seven object literals is the whole of it.\n *\n * ## Why a flat record rather than a loader map\n *\n * The engine's map — the one this replaces — used thunks (`vendor -> () => import(...)`) so nothing\n * was evaluated until a vendor was asked for. That cost is real when the map sits in an engine\n * everybody loads. Here it does not apply: this module is reached only when an application declared a\n * system of record at all.\n *\n * ## The export NAMES are the facet-module contract\n *\n * `PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_FACETS` and its siblings keep the names they had in the\n * catalogue because they are what `SYSTEM_OF_RECORD_FACET_RECORD_EXPORT` (saas-models) names, and every\n * augmentation package exports its record under the matching `…_FACET_AUGMENTATIONS`. Renaming them is a\n * contract change for every facet module, not a tidy-up of this file.\n *\n * ## Adding one\n *\n * Author `<vendor>.system-of-record.ts` beside this file and add its row below. The key and the\n * facet's own `vendor` must agree; `systems-of-record.exports.test.ts` asserts it, because a\n * hand-written key beside a declared id is exactly the shape that drifts.\n */\nimport type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';\n\nimport { MicrosoftDataverseSystemOfRecordFacet } from './microsoft-dataverse.system-of-record';\nimport { MicrosoftDynamics365BusinessCentralSystemOfRecordFacet } from './microsoft-dynamics-365-business-central.system-of-record';\nimport { NetsuiteSystemOfRecordFacet } from './netsuite.system-of-record';\nimport { OdataSystemOfRecordFacet } from './odata.system-of-record';\nimport { OdooSystemOfRecordFacet } from './odoo.system-of-record';\nimport { SalesforceSystemOfRecordFacet } from './salesforce.system-of-record';\nimport { SapS4hanaSystemOfRecordFacet } from './sap-s4hana.system-of-record';\n\n/**\n * Every vendor the framework can serve as a system of record, keyed by the id an instance names.\n *\n * Far fewer vendors than there are providers, because being reachable over HTTP is not the same as\n * being a deployment an application reads its own rows out of. A vendor absent from here is not\n * refused by this record — it is a vendor the framework does not describe; the generic `odata` facet\n * serves any OData v4 service.\n */\nexport const PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_FACETS: Readonly<Record<string, SystemOfRecordVendorFacet>> = {\n 'microsoft-dataverse': MicrosoftDataverseSystemOfRecordFacet,\n 'microsoft-dynamics-365-business-central': MicrosoftDynamics365BusinessCentralSystemOfRecordFacet,\n netsuite: NetsuiteSystemOfRecordFacet,\n odata: OdataSystemOfRecordFacet,\n odoo: OdooSystemOfRecordFacet,\n salesforce: SalesforceSystemOfRecordFacet,\n 'sap-s4hana': SapS4hanaSystemOfRecordFacet,\n};\n\n/**\n * Vendor ids that are ANOTHER vendor's, and which one — so a refusal can point rather than just say no.\n *\n * Not misspellings. Each of these is a real provider id, and an author naming one has made a\n * reasonable mistake: the provider catalogue held `microsoft-dataverse`,\n * `microsoft-dynamics-365` and `microsoft-dynamics-crm` as three separate connector refs, and they\n * are three names for ONE Web API. Only one of them can carry the facet, and a refusal that says\n * \"no facet\" without saying which sibling has it leaves the author to guess between the other two.\n *\n * Lives HERE rather than in the engine for the reason the facets do: which vendor ids are the same\n * product is vendor knowledge, and adding a vendor tomorrow adds its aliases in the same edit.\n *\n * Every value MUST be a key of the facet record above — asserted by `systems-of-record.exports.test.ts`,\n * because an alias pointing at a vendor that has no facet sends the author from one refusal to another.\n */\nexport const PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_VENDOR_ALIASES: Readonly<Record<string, string>> = {\n 'microsoft-dynamics-365': 'microsoft-dataverse',\n 'microsoft-dynamics-crm': 'microsoft-dataverse',\n};\n\n/** The vendor ids above, sorted — what a sync-time discovery records without loading a facet's shape. */\nexport const PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_VENDORS: readonly string[] =\n Object.keys(PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_FACETS).sort();\n"]}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@wildo-ai/systems-of-record",
3
+ "version": "1.1.6",
4
+ "license": "SEE LICENSE IN LICENSE",
5
+ "description": "Wildo system-of-record vendor facets (Odoo, SAP S/4HANA, Microsoft Dataverse, Business Central, NetSuite, Salesforce, generic OData): an opt-in library an application links when it declares systemsOfRecord",
6
+ "author": "Wildo AI",
7
+ "type": "module",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ "./systems-of-record": {
11
+ "types": "./dist/esm/systems-of-record.exports.d.ts",
12
+ "import": "./dist/esm/systems-of-record.exports.js",
13
+ "default": "./dist/esm/systems-of-record.exports.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist/"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.build.json && node ../../scripts/run-tsc-alias-safe.mjs --project tsconfig.build.json --dist dist/esm",
21
+ "dev": "node ../../scripts/run-tsc-watch-single-writer.mjs --dist dist/esm --exec tsc-watch -- -p tsconfig.build.json --preserveWatchOutput --onSuccess \"node ../../scripts/run-tsc-alias-safe.mjs --project tsconfig.build.json --dist dist/esm\"",
22
+ "rebuild": "pnpm clean && pnpm build",
23
+ "clean": "rimraf dist",
24
+ "watch": "node ../../scripts/run-tsc-watch-single-writer.mjs --dist dist/esm --exec tsc-watch -- -p tsconfig.build.json --preserveWatchOutput --onSuccess \"node ../../scripts/run-tsc-alias-safe.mjs --project tsconfig.build.json --dist dist/esm\"",
25
+ "typecheck": "tsc --noEmit",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest"
28
+ },
29
+ "dependencies": {
30
+ "@wildo-ai/saas-models": "1.1.6"
31
+ },
32
+ "devDependencies": {
33
+ "rimraf": "^6.0.1",
34
+ "tsc-watch": "7.2.1",
35
+ "typescript": "5.9.3",
36
+ "vitest": "^1.6.1"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/wildo-ai/wildo-ai.git",
41
+ "directory": "engine/systems-of-record"
42
+ }
43
+ }