venafi-connector-machine 1.0.4 → 1.0.5

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 (2) hide show
  1. package/bundle.mjs +393 -16
  2. package/package.json +1 -1
package/bundle.mjs CHANGED
@@ -30802,22 +30802,83 @@ Here, the first three types all come from the same data source (keystores), but
30802
30802
 
30803
30803
  **Key: choosing the right categories.** Ask: "How would an admin of this platform describe their certificates?" For FortiGate, it's by service type (VPN, firewall, management). For APIC, it's by infrastructure role (gateway, backend, unassigned, consumer). For F5, it might be by partition or virtual server type. The checkboxes should match the admin's mental model.
30804
30804
 
30805
- **Implementation \u2014 binding metadata:**
30805
+ **Implementation \u2014 binding metadata with per-type dropdown fields:**
30806
30806
 
30807
- Use \`BindingType\` (service category) + \`TargetName\` (specific named object) as the binding fields:
30808
- - For singleton/global services (admin HTTPS, SSL VPN): set \`TargetName\` to \`"(global setting)"\` so the UI doesn't show an empty field
30809
- - For named services (IPsec tunnels, VIPs, profiles): set \`TargetName\` to the actual object name
30807
+ Instead of a generic \`targetName\` field, use **per-type dropdown fields** that are conditionally shown based on \`bindingType\`. Each field links to the \`getTargetConfiguration\` response via \`x-targetConfigurationRef\`, creating actual dropdowns populated from the target system:
30808
+
30809
+ \`\`\`json
30810
+ "binding": {
30811
+ "properties": {
30812
+ "bindingType": {
30813
+ "oneOf": [
30814
+ { "const": "management", "x-labelLocalizationKey": "bindingType.management" },
30815
+ { "const": "https-listener", "x-labelLocalizationKey": "bindingType.httpsListener" },
30816
+ { "const": "outbound-tls", "x-labelLocalizationKey": "bindingType.outboundTls" }
30817
+ ]
30818
+ },
30819
+ "vipName": {
30820
+ "type": "string",
30821
+ "default": "",
30822
+ "x-targetConfigurationRef": "/vips",
30823
+ "x-rule": {
30824
+ "effect": "SHOW",
30825
+ "condition": { "scope": "#/properties/bindingType", "schema": { "const": "https-listener" } }
30826
+ }
30827
+ },
30828
+ "tunnelName": {
30829
+ "type": "string",
30830
+ "default": "",
30831
+ "x-targetConfigurationRef": "/ipsecTunnels",
30832
+ "x-rule": {
30833
+ "effect": "SHOW",
30834
+ "condition": { "scope": "#/properties/bindingType", "schema": { "const": "outbound-tls" } }
30835
+ }
30836
+ },
30837
+ "guiPath": {
30838
+ "type": "string",
30839
+ "default": "",
30840
+ "readOnly": true,
30841
+ "x-hidden": true
30842
+ }
30843
+ },
30844
+ "x-primaryKey": ["#/bindingType", "#/vipName", "#/tunnelName", "#/guiPath"]
30845
+ }
30846
+ \`\`\`
30847
+
30848
+ Key features of this pattern:
30849
+ - **\`x-targetConfigurationRef\`** links a field to a JSON path in the \`getTargetConfiguration\` response, populating a dropdown
30850
+ - **\`x-rule\` with \`"effect": "SHOW"\`** conditionally displays each field only when the relevant \`bindingType\` is selected
30851
+ - **\`default: ""\`** on every \`x-primaryKey\` field \u2014 CRITICAL for net new certificate provisioning (see section 30)
30852
+ - **\`x-hidden: true\`** hides metadata-only fields (like guiPath) from the provisioning UI while keeping them in the schema
30853
+ - Singleton types (management, SSL VPN) don't need a named target \u2014 the dropdown field stays hidden and empty
30854
+ - Named-target types (VIP, tunnel, profile) show a dropdown populated from the target system
30810
30855
 
30811
30856
  **Implementation \u2014 buildBindingMap:**
30812
30857
 
30813
- Query each service endpoint selectively based on the user's \`discoveryTypes\` selection. Build a \`map[string][]domain.Binding\` keyed by certificate name. Then when constructing discovery results, look up each cert's bindings and attach them as machine identities. Certs without bindings still get reported (with an empty binding) so they appear in Venafi.
30858
+ Query each service endpoint selectively based on the user's \`discoveryTypes\` selection. Build a \`map[string][]domain.Binding\` keyed by certificate name. Then when constructing discovery results, look up each cert's bindings and attach them as machine identities. Certs without bindings still get reported (with a fallback "unbound" binding) so they appear in Venafi.
30814
30859
 
30815
- **Key manifest features:**
30816
- - \`x-rule\` with \`effect: "SHOW"\` on \`targetName\` \u2014 hide it for singleton types where there's no named object to select
30817
- - \`x-primaryKey: ["#/bindingType", "#/targetName"]\` \u2014 uniquely identifies each binding
30818
- - \`discoveryTypes\` uses \`uniqueItems: true\` to prevent duplicate selections
30860
+ **Implementation \u2014 provisioning dispatch:**
30819
30861
 
30820
- **Why this matters**: Without discovery type filtering, every discovery run queries all service endpoints, which can be slow and return unwanted results. Without rich binding metadata, users can't tell which service a certificate is associated with or whether an empty "Target Name" field is intentional.
30862
+ In \`configureInstallationEndpoint\`, dispatch based on \`bindingType\` and the per-type field values:
30863
+
30864
+ \`\`\`go
30865
+ switch binding.BindingType {
30866
+ case "https-listener":
30867
+ if binding.VipName == "" {
30868
+ return handler.SetSSLVPNServerCert(certName) // global SSL VPN
30869
+ }
30870
+ return handler.SetVIPCertificate(binding.VipName, certName) // specific VIP
30871
+ case "outbound-tls":
30872
+ if binding.TunnelName == "" {
30873
+ return fmt.Errorf("tunnelName is required for outbound-tls")
30874
+ }
30875
+ return handler.SetIPsecCertificate(binding.TunnelName, certName)
30876
+ }
30877
+ \`\`\`
30878
+
30879
+ This eliminates guiPath-based dispatch and gives users explicit dropdown selection for each target type.
30880
+
30881
+ **Why this matters**: Without per-type dropdowns, users must type exact target names from memory. With \`x-targetConfigurationRef\`, they get a dropdown populated from the actual target system \u2014 reducing errors and improving the provisioning UX significantly.
30821
30882
 
30822
30883
  ### 15. Onboard Discovery vs Network Discovery \u2014 Don't Report What You Can't Observe
30823
30884
 
@@ -31004,30 +31065,35 @@ With catalog-level enrichment, BOTH entries show: Product = "Product Discovery,
31004
31065
  \`\`\`go
31005
31066
  // matchProfileToProduct matches a TLS profile to a specific product
31006
31067
  // by checking if the profile title starts with a product title.
31007
- // Falls back to all catalog products if no match found.
31068
+ // Returns empty strings if no match (caller defaults to "N/A").
31069
+ // NEVER fall back to catalog-level aggregates \u2014 a catalog with 1786 APIs
31070
+ // produces an ~80KB string that gets duplicated into every binding,
31071
+ // causing 1MB Kafka message limit failures on satellite\u2192platform transport.
31008
31072
  func matchProfileToProduct(profileTitle string, cp catalogProductInfo) (string, string) {
31009
31073
  if profileTitle != "" {
31010
31074
  for _, pd := range cp.Products {
31011
31075
  if strings.HasPrefix(profileTitle, pd.Title) {
31012
- return pd.Title, strings.Join(pd.ApiTitles, ", ")
31076
+ apiStr := strings.Join(pd.ApiTitles, ", ")
31077
+ return pd.Title, truncateMetadata(apiStr, len(pd.ApiTitles))
31013
31078
  }
31014
31079
  }
31015
31080
  }
31016
- return cp.AllProductNames, cp.AllApiNames
31081
+ return "", "" // caller sets "N/A" defaults
31017
31082
  }
31018
31083
  \`\`\`
31019
31084
 
31020
31085
  **Implementation requirements**:
31021
- 1. \`buildCatalogProductMap\` must return per-product detail (title + API list), not just catalog-level aggregates
31086
+ 1. \`buildCatalogProductMap\` must return per-product detail (title + API list) \u2014 do NOT store catalog-level aggregate strings (\`AllProductNames\`, \`AllApiNames\`)
31022
31087
  2. Products have \`api_urls\` referencing their APIs \u2014 resolve these against the API list to get per-product API titles
31023
31088
  3. The matching uses \`strings.HasPrefix\` on the profile title vs product title \u2014 reliable for standard naming conventions
31024
- 4. Fallback to catalog-level aggregates when no product matches (backends, internal profiles)
31089
+ 4. **NEVER fall back to catalog-level aggregates** \u2014 return empty strings (caller defaults to "N/A"). Catalog-level aggregates were the root cause of 1MB Kafka limit failures in production (a single catalog with 1786 APIs produced 80KB strings duplicated into every binding, creating 2MB+ pages that were silently dropped)
31025
31090
 
31026
31091
  **Result after fix**:
31027
31092
  - \`tls-gw-product-discovery\` \u2192 Product = "Product Discovery", APIs = "Product Catalog API"
31028
31093
  - \`tls-gw-customer-platform\` \u2192 Product = "Customer Platform", APIs = "Customer API, Order API"
31094
+ - Unmatched profiles \u2192 Product = "N/A", APIs = "N/A" (safe, small, and honest)
31029
31095
 
31030
- **Why this matters**: When users see duplicate-looking entries in the Venafi UI, they can't determine which platform service each certificate installation belongs to. Discovery should provide the MOST SPECIFIC context available for each binding, not the same generic data for all.
31096
+ **Why this matters**: When users see duplicate-looking entries in the Venafi UI, they can't determine which platform service each certificate installation belongs to. Discovery should provide the MOST SPECIFIC context available for each binding. If no specific match exists, "N/A" is better than dumping every product/API in the catalog \u2014 which is both misleading and a response size bomb.
31031
31097
 
31032
31098
  ### 21. CRITICAL: Discovery Field Labels \u2014 title vs x-labelLocalizationKey
31033
31099
 
@@ -31500,6 +31566,317 @@ dc := &DiscoveredCertificate{
31500
31566
  \`\`\`
31501
31567
 
31502
31568
  **Alternative**: Use a pointer with \`omitempty\` if the field is truly optional. But for \`certificateChain\` and \`machineIdentities\`, always send \`[]\` rather than omitting or sending \`null\`.
31569
+
31570
+ ### 29. CRITICAL: Discovery Response Size \u2014 1MB Kafka Limit and Metadata String Inflation
31571
+
31572
+ **The symptom**: Discovery completes page 1 successfully (connector logs show valid response with \`hasMorePages:true\` and correct paginator), but the platform never requests page 2. The discovery job stays in RUNNING state forever. No errors appear in the connector logs \u2014 the response write succeeds from the connector's perspective.
31573
+
31574
+ **The root cause**: The vSatellite relays discovery responses to the Venafi platform via **Kafka**, which has a **1MB message size limit**. When the response exceeds this limit, the message is silently dropped. The platform never receives the response (including the pagination token) and never requests the next page. Note: the connector communicates with the satellite over HTTP, and the satellite communicates with the platform over Kafka \u2014 so the connector never sees the Kafka rejection.
31575
+
31576
+ **How responses get inflated**: The #1 cause is **metadata string duplication in bindings**. Never enrich bindings with aggregated catalog-level or scope-level data (e.g., all product names or all API titles). In production, a single catalog had 1786 APIs \u2014 joining all titles into one string produced ~80KB that got copied into every binding. With 25 certs per page, that's 2MB+ just from API names, far exceeding the 1MB Kafka limit.
31577
+
31578
+ **How we discovered this**: We reproduced the issue by injecting 1786 fake API names into the enrichment path. The response grew to 2.1MB per page of 25 certs, and the Venafi platform silently stopped paginating \u2014 exactly matching the customer's symptom. Without the injection (our 9-API test environment), pages were 54KB and pagination worked perfectly.
31579
+
31580
+ **The fix \u2014 only enrich on exact match, never aggregate**:
31581
+
31582
+ The root cause was a fallback path in \`matchProfileToProduct\` that dumped ALL product/API names from the entire catalog into every binding that didn't match a specific product. The fix is simple: **if you can't match to a specific item, return empty/default values ("N/A"), never aggregate**.
31583
+
31584
+ \`\`\`go
31585
+ // CORRECT: Only return metadata on exact match. Default to empty (caller sets "N/A").
31586
+ func matchProfileToProduct(profileTitle string, cp catalogProductInfo) (string, string) {
31587
+ if profileTitle != "" {
31588
+ for _, pd := range cp.Products {
31589
+ if strings.HasPrefix(profileTitle, pd.Title) {
31590
+ apiStr := strings.Join(pd.ApiTitles, ", ")
31591
+ return pd.Title, truncateMetadata(apiStr, len(pd.ApiTitles))
31592
+ }
31593
+ }
31594
+ }
31595
+ return "", "" // caller defaults to "N/A"
31596
+ }
31597
+
31598
+ // WRONG: Never fall back to catalog-level aggregates
31599
+ // return cp.AllProductNames, cp.AllApiNames // <-- THIS CAUSED 2MB+ PAGES
31600
+ \`\`\`
31601
+
31602
+ **Defense in depth \u2014 truncateMetadata for exact matches**: Even a single product can have many APIs, so apply \`truncateMetadata()\` to cap any metadata string at 200 chars:
31603
+
31604
+ \`\`\`go
31605
+ const maxMetadataFieldLen = 200
31606
+
31607
+ func truncateMetadata(s string, totalCount int) string {
31608
+ if len(s) <= maxMetadataFieldLen {
31609
+ return s
31610
+ }
31611
+ cut := strings.LastIndex(s[:maxMetadataFieldLen], ",")
31612
+ if cut <= 0 {
31613
+ cut = maxMetadataFieldLen
31614
+ }
31615
+ shown := strings.Count(s[:cut], ",") + 1
31616
+ return fmt.Sprintf("%s (+%d more)", s[:cut], totalCount-shown)
31617
+ }
31618
+ \`\`\`
31619
+
31620
+ **Additional defensive measures**:
31621
+ 1. **Keep page size small**: Use \`maxPageSize = 10\` to provide headroom against the 1MB Kafka limit
31622
+ 2. **Do not store catalog-level aggregate strings**: The \`catalogProductInfo\` struct should only hold per-product detail (\`[]productDetail\`), never \`AllProductNames\` or \`AllApiNames\` aggregate fields \u2014 if those fields exist, someone will use them in a fallback
31623
+ 3. **Single marshal + JSONBlob**: Marshal the response once, use \`c.JSONBlob()\` to send. Avoids double-allocating multi-MB responses
31624
+ 4. **Profile every page**: Log \`responseSizeBytes\`, \`maxBindingApiNamesLen\`, \`avgMIBytes\`, and \`totalMIJSONBytes\` per batch to detect inflation early
31625
+
31626
+ **Key insight**: This failure is invisible to the connector. The HTTP response write succeeds (Echo sends the bytes to the satellite), but Kafka silently drops the oversized message before it reaches the platform. The platform never sees the response and never requests the next page. The only way to detect it is by checking \`responseSizeBytes\` in the connector logs and comparing against the 1MB Kafka limit.
31627
+
31628
+ **Rule of thumb**: Keep \`responseSizeBytes\` well under 1MB per page. If it exceeds 500KB, investigate immediately. Never put aggregated/unbounded strings into binding metadata \u2014 if you can't match to a specific item, use "N/A".
31629
+
31630
+ ### 30. CRITICAL: x-primaryKey Fields MUST Have default Values for Net New Provisioning
31631
+
31632
+ **The symptom**: Provisioning an existing (discovered) certificate works fine \u2014 the certificate is pushed to the target and the binding is configured. But provisioning a **net new certificate** (one that was not previously discovered) stalls indefinitely. The Venafi UI shows the provisioning job as RUNNING but it never completes.
31633
+
31634
+ **The root cause**: When provisioning a net new certificate, the Venafi platform constructs the binding from the manifest schema. If any \`x-primaryKey\` field lacks a \`"default"\` value, the platform returns error **20106**: *"property X is a primary key but is not specified in request and has no default value"*. This error is NOT visible in the connector logs \u2014 it happens platform-side before the request ever reaches the connector.
31635
+
31636
+ **Why discovered certs work**: When re-provisioning a discovered certificate, the binding already has values for all fields (populated during discovery). The \`default\` is only needed when the platform must construct a binding from scratch.
31637
+
31638
+ **The fix**: Every field in \`x-primaryKey\` MUST have a \`"default"\` value in the manifest schema:
31639
+
31640
+ \`\`\`json
31641
+ "binding": {
31642
+ "properties": {
31643
+ "bindingType": { "oneOf": [...] },
31644
+ "vipName": { "type": "string", "default": "" },
31645
+ "tunnelName": { "type": "string", "default": "" },
31646
+ "profileName": { "type": "string", "default": "" },
31647
+ "guiPath": { "type": "string", "default": "", "readOnly": true, "x-hidden": true }
31648
+ },
31649
+ "x-primaryKey": ["#/bindingType", "#/vipName", "#/tunnelName", "#/profileName", "#/guiPath"]
31650
+ }
31651
+ \`\`\`
31652
+
31653
+ **Debugging checklist** when net new provisioning stalls:
31654
+ 1. Check if provisioning of previously-discovered certs works (if yes, this is likely the issue)
31655
+ 2. Look for error 20106 in the Venafi platform activity log (\`POST /v1/activitylogsearch\`)
31656
+ 3. Verify every \`x-primaryKey\` field has a \`"default"\` in the manifest
31657
+ 4. Deploy a new plugin revision with the defaults added
31658
+
31659
+ ### 31. CRITICAL: PKCS12 Must Use Legacy Encoding for Network Appliances
31660
+
31661
+ **The mistake**: Using modern PKCS12 encoding (AES-256-CBC + SHA-256) when building PKCS12 bundles for certificate import to network appliances.
31662
+
31663
+ **The problem**: Many network appliances (FortiGate confirmed, likely F5 and others) cannot parse modern PKCS12 encoding. The import API returns success or a vague error, but the certificate is not usable. FortiGate specifically requires the legacy PKCS12 format (3DES + SHA1).
31664
+
31665
+ **The fix**: When building PKCS12 bundles in Go, use the Legacy encoder:
31666
+
31667
+ \`\`\`go
31668
+ import gopkcs12 "software.sslmate.com/src/go-pkcs12"
31669
+
31670
+ // CORRECT: Legacy encoding (3DES/SHA1) \u2014 compatible with network appliances
31671
+ p12Data, err := gopkcs12.Legacy.Encode(key, cert, caCerts, password)
31672
+
31673
+ // WRONG: Modern encoding (AES-256/SHA-256) \u2014 rejected by FortiGate and similar appliances
31674
+ // p12Data, err := gopkcs12.Modern.Encode(key, cert, caCerts, password)
31675
+ \`\`\`
31676
+
31677
+ **Why this happens**: The \`go-pkcs12\` library defaults to \`Modern\` encoding which uses AES-256-CBC and SHA-256. While more secure, this format is not supported by older TLS stacks. Network appliances typically use OpenSSL 1.x-era PKCS12 parsing that only understands the legacy 3DES/SHA1 format.
31678
+
31679
+ **Recommendation**: Always use \`gopkcs12.Legacy.Encode()\` for any connector targeting a network appliance. Only consider \`Modern\` for targets you've explicitly verified support it.
31680
+
31681
+ ### 32. x-hidden Hides Manifest Fields from Provisioning UI
31682
+
31683
+ **The pattern**: When a binding field is needed for schema purposes (\`x-primaryKey\`) but should not be shown to users during provisioning, use \`"x-hidden": true\`:
31684
+
31685
+ \`\`\`json
31686
+ "guiPath": {
31687
+ "type": "string",
31688
+ "default": "",
31689
+ "readOnly": true,
31690
+ "x-hidden": true,
31691
+ "x-labelLocalizationKey": "guiPath.label",
31692
+ "x-rank": 4
31693
+ }
31694
+ \`\`\`
31695
+
31696
+ This is useful for metadata fields populated by discovery (like a navigation path or internal ID) that provide context in the Venafi UI's installation detail view but shouldn't appear as an input field when a user is provisioning a new certificate.
31697
+
31698
+ **Difference from readOnly**: \`readOnly: true\` alone still renders the field in the UI (as a non-editable input). \`x-hidden: true\` prevents it from rendering entirely.
31699
+
31700
+ ### 33. Image Digest, Not Tag, for vSatellite Deployments
31701
+
31702
+ **The problem**: Using \`:latest\` tag in the plugin manifest's \`deployment.image\` field causes intermittent image pull failures on vSatellite, even after pushing a new image.
31703
+
31704
+ **The root cause**: The vSatellite's k3s runtime caches images by digest. When the manifest specifies \`:latest\`, the satellite resolves it to a digest at pull time. If the registry mirror (via \`registries.yaml\`) rewrites or caches the digest resolution, subsequent pulls may fail with \`ErrImagePull\` due to digest mismatch.
31705
+
31706
+ **The fix**: Always use \`image@sha256:...\` (digest reference) in the plugin manifest, never a tag:
31707
+
31708
+ \`\`\`json
31709
+ "deployment": {
31710
+ "image": "public.ecr.aws/example/connector@sha256:abc123...",
31711
+ "executionTarget": "vsat"
31712
+ }
31713
+ \`\`\`
31714
+
31715
+ Extract the digest from the build output:
31716
+
31717
+ \`\`\`bash
31718
+ # After docker buildx push
31719
+ DIGEST=$(jq -r '.["containerimage.digest"]' buildx-digest.json)
31720
+ \`\`\`
31721
+
31722
+ **Additional k3s gotcha**: The vSatellite's \`registries.yaml\` may have a catch-all mirror entry for \`registry.venafi.cloud\` that rewrites image pulls. If your image is in a public registry (e.g., public ECR), ensure that registry's mirror entry appears BEFORE the wildcard catch-all. A misplaced wildcard causes digest mismatch errors on pull.
31723
+
31724
+ ### 34. Plugin API \u2014 PATCH Not POST, pluginType at Top Level
31725
+
31726
+ **When registering a connector on a new tenant**: Use \`POST /v1/plugins\` with \`pluginType\` at the top level alongside \`manifest\`:
31727
+
31728
+ \`\`\`bash
31729
+ curl -X POST -H "tppl-api-key: $KEY" -H "Content-Type: application/json" \\
31730
+ -d '{"pluginType": "MACHINE", "manifest": {...}}' \\
31731
+ "https://api.venafi.cloud/v1/plugins"
31732
+ \`\`\`
31733
+
31734
+ **When updating an existing plugin**: Use \`PATCH /v1/plugins/{id}\`. POST returns error **16004** if the plugin already exists.
31735
+
31736
+ **Common mistake**: Putting \`pluginType\` inside the \`manifest\` object. The API requires it at the top level of the request body alongside \`manifest\`.
31737
+
31738
+ ### 35. x-targetConfigurationRef Powers Dropdown Fields from getTargetConfiguration
31739
+
31740
+ **The pattern**: When your target system has enumerable resources (scopes, profiles, tunnels, virtual IPs, partitions), return them from the \`getTargetConfiguration\` endpoint and reference them in the manifest to create dropdown fields:
31741
+
31742
+ **Step 1 \u2014 Define the targetConfiguration schema in manifest:**
31743
+ \`\`\`json
31744
+ "targetConfiguration": {
31745
+ "properties": {
31746
+ "vdoms": { "type": "array", "items": { "type": "string" } },
31747
+ "vips": { "type": "array", "items": { "type": "string" } },
31748
+ "tunnels": { "type": "array", "items": { "type": "string" } }
31749
+ },
31750
+ "type": "object"
31751
+ }
31752
+ \`\`\`
31753
+
31754
+ **Step 2 \u2014 Return values from the Go handler:**
31755
+ \`\`\`go
31756
+ type GetTargetConfigurationResponse struct {
31757
+ VDOMs []string \`json:"vdoms"\`
31758
+ VIPs []string \`json:"vips"\`
31759
+ Tunnels []string \`json:"tunnels"\`
31760
+ }
31761
+ \`\`\`
31762
+
31763
+ **Step 3 \u2014 Reference in binding/keystore fields:**
31764
+ \`\`\`json
31765
+ "vipName": {
31766
+ "type": "string",
31767
+ "default": "",
31768
+ "x-targetConfigurationRef": "/vips",
31769
+ "x-rule": {
31770
+ "effect": "SHOW",
31771
+ "condition": { "scope": "#/properties/bindingType", "schema": { "const": "https-listener" } }
31772
+ }
31773
+ }
31774
+ \`\`\`
31775
+
31776
+ The JSON path in \`x-targetConfigurationRef\` (e.g., \`"/vips"\`) must match the key in your \`getTargetConfiguration\` response. The UI will populate a dropdown with the returned values.
31777
+
31778
+ **Combine with x-rule**: Use \`x-rule\` to only show the dropdown when the relevant binding type is selected. This keeps the UI clean \u2014 users only see fields relevant to their selected binding type.
31779
+
31780
+ **IMPORTANT limitation**: \`x-targetConfigurationRef\` dropdowns do NOT populate during net new provisioning in the Venafi UI. The platform only calls \`getTargetConfiguration\` for discovery workflows and when editing existing installations. During net new provisioning, these fields render as plain text boxes. Users must type the exact target name. Use \`oneOf\` in the manifest schema for fields that MUST always be dropdowns (like binding type, certificate type).
31781
+
31782
+ ### 36. CRITICAL: Duplicate Certificate Handling During Provisioning
31783
+
31784
+ **The problem**: When provisioning is retried (due to timeouts, partial failures, or the user re-provisioning the same cert to a different binding), the target system may already have the certificate from a previous attempt. Attempting to import a certificate that already exists often returns an error (e.g., HTTP 500 "entry already exists").
31785
+
31786
+ **The fix**: Before importing, always check if a certificate with the target name already exists on the system. If it does, skip the import and proceed to the binding step:
31787
+
31788
+ \`\`\`go
31789
+ existingCerts, err := handler.GetCertificateDetails()
31790
+ if err == nil {
31791
+ if certExistsByName(existingCerts, certName) {
31792
+ log.Info("certificate already exists, skipping upload", "name", certName)
31793
+ keystore.CertificateName = certName
31794
+ return keystore, nil
31795
+ }
31796
+ }
31797
+ \`\`\`
31798
+
31799
+ **Name collision with different content**: If a cert with the same name exists but has different content (e.g., renewed cert), generate a unique name. Then check if THAT name also exists before importing \u2014 previous failed attempts may have already uploaded it:
31800
+
31801
+ \`\`\`go
31802
+ // Name collision \u2014 generate unique name
31803
+ uniqueName := generateUniqueName(baseName, cert)
31804
+ // Check if the unique name also exists from a previous attempt
31805
+ if certExistsByName(existingCerts, uniqueName) {
31806
+ log.Info("unique name also exists, skipping upload", "name", uniqueName)
31807
+ keystore.CertificateName = uniqueName
31808
+ return keystore, nil
31809
+ }
31810
+ // Safe to import with unique name
31811
+ \`\`\`
31812
+
31813
+ **Key insight**: The Venafi platform retries provisioning workflows. Your connector MUST be idempotent \u2014 importing the same certificate twice should not fail.
31814
+
31815
+ ### 37. CRITICAL: Fingerprint Format Varies by Target System
31816
+
31817
+ **The problem**: When comparing certificate fingerprints to detect duplicates, the target system may use a different hash algorithm than what you compute. For example, SHA-1 (40 hex chars) vs SHA-256 (64 hex chars), or different formatting (colons vs no colons, uppercase vs lowercase).
31818
+
31819
+ **The fix**: Normalize fingerprints before comparing \u2014 strip colons, uppercase, and check that lengths match (same hash algorithm) before comparing values:
31820
+
31821
+ \`\`\`go
31822
+ func fingerprintsMatch(a, b string) bool {
31823
+ normA := strings.ToUpper(strings.ReplaceAll(a, ":", ""))
31824
+ normB := strings.ToUpper(strings.ReplaceAll(b, ":", ""))
31825
+ if normA == "" || normB == "" {
31826
+ return false
31827
+ }
31828
+ if len(normA) != len(normB) {
31829
+ return false // Different hash algorithms, can't compare
31830
+ }
31831
+ return normA == normB
31832
+ }
31833
+ \`\`\`
31834
+
31835
+ If fingerprints can't be reliably compared (different hash algorithms), fall back to name-based duplicate detection rather than assuming the content is different.
31836
+
31837
+ ### 38. CRITICAL: Service-Disruptive Binding Changes Need Short Timeouts
31838
+
31839
+ **The problem**: Some binding operations cause the target system to restart a service, which drops the HTTP connection used to make the API call. The connector blocks waiting for a response that will never arrive (TCP timeout can be 30-60+ seconds). Meanwhile, the Venafi platform's workflow engine times out waiting for the connector's HTTP response, causing a \`RETRY_STATE_TIMEOUT\` failure.
31840
+
31841
+ **Examples**: Changing an admin/management HTTPS certificate, updating an SSL VPN certificate, rotating a load balancer listener cert.
31842
+
31843
+ **The fix**: Use a short request timeout (5-10 seconds) for binding operations that are known to cause service restarts. Treat any network error as success, since the change was already applied before the connection dropped:
31844
+
31845
+ \`\`\`go
31846
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
31847
+ defer cancel()
31848
+
31849
+ resp, err := client.R().
31850
+ SetContext(ctx).
31851
+ SetBody(payload).
31852
+ Put("/api/endpoint/that/restarts/service")
31853
+ if err != nil {
31854
+ // Service restarted \u2014 the change was applied before the connection dropped.
31855
+ log.Info("expected connection drop after binding change, treating as success")
31856
+ return nil
31857
+ }
31858
+ \`\`\`
31859
+
31860
+ **Why this matters**: The Venafi platform has its own timeout for workflow activities. If your connector blocks for 60 seconds waiting for a dead TCP connection, the platform will timeout and mark the provisioning as failed \u2014 even though the change succeeded on the target system.
31861
+
31862
+ ### 39. x-hidden Does NOT Hide Fields from the Provisioning UI
31863
+
31864
+ **The problem**: Setting \`"x-hidden": true\` on a manifest field does NOT prevent it from appearing in the net new provisioning form. The field still renders as a text box.
31865
+
31866
+ **What x-hidden actually does**: It may only affect discovery result display or other non-provisioning contexts. The Venafi provisioning UI renders ALL fields defined in \`properties\`, regardless of \`x-hidden\`, \`readOnly\`, or other attributes.
31867
+
31868
+ **What you CANNOT do**: Remove the field from \`properties\` if it's referenced in \`x-primaryKey\`. Doing so crashes the Venafi UI (blank page, user gets logged out). All \`x-primaryKey\` fields MUST exist in \`properties\`.
31869
+
31870
+ **Workaround**: Accept that internal/metadata fields (like GUI navigation paths) will appear as read-only text boxes in the provisioning form. Use \`"readOnly": true\` to prevent user modification, and set \`"default": ""\` so net new provisioning works. This is cosmetic \u2014 it doesn't affect functionality.
31871
+
31872
+ ### 40. x-primaryKey Field Requirements \u2014 Summary
31873
+
31874
+ All fields listed in \`x-primaryKey\` MUST:
31875
+ 1. Exist in the \`properties\` object (removing them crashes the UI)
31876
+ 2. Have \`"default": ""\` (or another default) \u2014 without a default, net new provisioning fails with error 20106
31877
+ 3. Be of type \`"string"\` \u2014 complex types are not supported in primary keys
31878
+
31879
+ The \`x-primaryKey\` array defines uniqueness for machine identity bindings during discovery. Every unique combination of primary key values creates a separate binding entry. For provisioning, unused primary key fields default to empty string.
31503
31880
  `;
31504
31881
  var MACHINE_MANIFEST_TEMPLATE = `
31505
31882
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "venafi-connector-machine",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "MCP server providing machine connector-specific knowledge, templates, and tools for building Venafi TLS Protect Cloud machine connectors",
5
5
  "main": "bundle.mjs",
6
6
  "type": "module",