venafi-connector-machine 2.1.0 → 2.3.0

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 +392 -112
  2. package/package.json +1 -1
package/bundle.mjs CHANGED
@@ -29951,11 +29951,11 @@ var StdioServerTransport = class {
29951
29951
  var MACHINE_BLUEPRINT = `
29952
29952
  # Venafi Machine Connector Blueprint
29953
29953
 
29954
- This document captures the machine connector-specific architecture, patterns, and structure for building a Venafi TLS Protect Cloud machine connector. It is distilled from building multiple connectors: Splunk Enterprise (SSH), FortiGate (REST API), IBM API Connect (REST API), DataPower (REST API), and the Venafi Machine Connector Framework documentation.
29954
+ This document captures the machine connector-specific architecture, patterns, and structure for building a Venafi TLS Protect Cloud machine connector. It is distilled from building multiple connectors across SSH-based targets, REST API appliances, and API management platforms, as well as the Venafi Machine Connector Framework documentation.
29955
29955
 
29956
29956
  ## What Is a Machine Connector?
29957
29957
 
29958
- A machine connector is a containerized Go REST service that runs on a Venafi vSatellite. It acts as middleware between Venafi TLS Protect Cloud and a target system (e.g., Splunk, Apache, Nginx, HAProxy, F5). The connector discovers certificates on the target, reports them to Venafi, and provisions/renews certificates when instructed.
29958
+ A machine connector is a containerized Go REST service that runs on a Venafi vSatellite. It acts as middleware between Venafi TLS Protect Cloud and a target system (e.g., web servers, app servers, load balancers, network appliances). The connector discovers certificates on the target, reports them to Venafi, and provisions/renews certificates when instructed.
29959
29959
 
29960
29960
  - **pluginType**: "MACHINE"
29961
29961
  - **workTypes**: ["PROVISIONING", "DISCOVERY"]
@@ -30213,7 +30213,7 @@ Keystore and binding fields can be turned into dropdowns populated from the targ
30213
30213
  2. Implementing \`getTargetConfiguration\` to return actual values (not a stub)
30214
30214
  3. Adding \`x-targetConfigurationRef\` to the field in keystore/binding
30215
30215
 
30216
- **Example**: FortiGate VDOMs as a dropdown:
30216
+ **Example**: Virtual domains (partitions) as a dropdown:
30217
30217
 
30218
30218
  \`\`\`json
30219
30219
  "targetConfiguration": {
@@ -30236,7 +30236,7 @@ Keystore and binding fields can be turned into dropdowns populated from the targ
30236
30236
  }
30237
30237
  \`\`\`
30238
30238
 
30239
- The F5 BIG-IP connector uses this same pattern for partition dropdowns.
30239
+ Other reference connectors use this same pattern for partition dropdowns.
30240
30240
 
30241
30241
  ## Project Structure (Proven Pattern)
30242
30242
 
@@ -30266,7 +30266,7 @@ The F5 BIG-IP connector uses this same pattern for partition dropdowns.
30266
30266
  \u2502 \u251C\u2500\u2500 discovery/ # Certificate discovery logic
30267
30267
  \u2502 \u2502 \u251C\u2500\u2500 discovery.go # Main discovery orchestration
30268
30268
  \u2502 \u2502 \u2514\u2500\u2500 types.go # Request/response types
30269
- \u2502 \u2514\u2500\u2500 <target>/ # Target-specific logic (e.g., splunk/, apache/, nginx/)
30269
+ \u2502 \u2514\u2500\u2500 <target>/ # Target-specific logic (e.g., webserver/, appliance/)
30270
30270
  \u2502 \u251C\u2500\u2500 client.go # Connection client abstraction (SSH, API, etc.)
30271
30271
  \u2502 \u251C\u2500\u2500 detect.go # Target software detection
30272
30272
  \u2502 \u251C\u2500\u2500 install.go # Certificate file writing
@@ -30356,7 +30356,7 @@ func New() *fx.App {
30356
30356
  var LESSONS_LEARNED = `
30357
30357
  # Lessons Learned: Machine Connector Projects
30358
30358
 
30359
- This document captures everything that worked well, everything that was challenging, and every mistake or pitfall encountered while building machine connectors (Splunk SSH, FortiGate REST, IBM API Connect REST). Use this to avoid repeating mistakes and to replicate what worked.
30359
+ This document captures everything that worked well, everything that was challenging, and every mistake or pitfall encountered while building machine connectors across SSH-based targets and REST API appliances. Use this to avoid repeating mistakes and to replicate what worked.
30360
30360
 
30361
30361
  ---
30362
30362
 
@@ -30461,7 +30461,7 @@ This is critical for any values that come from user input (file paths, passwords
30461
30461
 
30462
30462
  **The reality**:
30463
30463
  - Venafi sends certificates in the \`CertificateBundle\` as **DER-encoded binary** (\`[]byte\`)
30464
- - Most target systems (Splunk, Apache, Nginx) expect **PEM format** (text with BEGIN/END headers)
30464
+ - Most target systems (web servers, app servers) expect **PEM format** (text with BEGIN/END headers)
30465
30465
  - Discovery must return certificates in **PEM format** (text strings)
30466
30466
 
30467
30467
  **The fix**: Always use \`pem.EncodeToMemory()\` when converting from Venafi's DER format to files:
@@ -30581,7 +30581,7 @@ return c.String(http.StatusBadRequest, fmt.Sprintf("SSH connection failed: %s",
30581
30581
 
30582
30582
  ### 8. Config Parsing is Inherently Fragile
30583
30583
 
30584
- **The issue**: Splunk's config files use an INI-like format, and our parser is basic:
30584
+ **The issue**: The target's config files use an INI-like format, and our parser is basic:
30585
30585
 
30586
30586
  \`\`\`go
30587
30587
  func parseINI(content string) map[string]map[string]string {
@@ -30590,34 +30590,34 @@ func parseINI(content string) map[string]map[string]string {
30590
30590
  \`\`\`
30591
30591
 
30592
30592
  **What it doesn't handle**:
30593
- - Multi-line values (rare in Splunk but possible)
30593
+ - Multi-line values (rare but possible)
30594
30594
  - Escaped characters
30595
30595
  - Include directives
30596
30596
  - App-level configs (only reads system/local and system/default)
30597
30597
 
30598
- **Why it worked anyway**: Splunk's SSL config is always simple key=value pairs. The basic parser handles 95%+ of real-world configs.
30598
+ **Why it worked anyway**: The target's SSL config is always simple key=value pairs. The basic parser handles 95%+ of real-world configs.
30599
30599
 
30600
30600
  **Recommendation for the next connector**: Match the parser complexity to the target's config format. If the target uses JSON/YAML, use Go's standard \`encoding/json\` or a YAML library. Don't build custom parsers for well-known formats.
30601
30601
 
30602
30602
  ### 9. Container Detection Based on Image Name
30603
30603
 
30604
- **The issue**: Container Splunk detection uses \`grep -i splunk\` on the image name:
30604
+ **The issue**: Container detection uses \`grep -i <product>\` on the image name:
30605
30605
 
30606
30606
  \`\`\`go
30607
- cs.RunCommand(client, fmt.Sprintf("%s ps --format '{{.Names}} {{.Image}}' | grep -i splunk", runtime))
30607
+ cs.RunCommand(client, fmt.Sprintf("%s ps --format '{{.Names}} {{.Image}}' | grep -i <product>", runtime))
30608
30608
  \`\`\`
30609
30609
 
30610
- **The problem**: This misses containers with custom image names that don't include "splunk" and may false-positive on unrelated containers that happen to have "splunk" in the name.
30610
+ **The problem**: This misses containers with custom image names that don't include the product name and may false-positive on unrelated containers that happen to match.
30611
30611
 
30612
30612
  **Recommendation for next connector**: Be specific about what you're detecting. If the target software has a known process name, binary path, or config file, check for those inside the container rather than relying on image name.
30613
30613
 
30614
30614
  ### 10. GetTargetConfiguration Can Power Dynamic Dropdowns
30615
30615
 
30616
- **The original issue**: The Splunk connector returned an empty response from \`getTargetConfiguration\`.
30616
+ **The original issue**: The SSH-based connector returned an empty response from \`getTargetConfiguration\`.
30617
30617
 
30618
- **Updated learning from FortiGate**: \`getTargetConfiguration\` is NOT just a stub. It can return dynamic values that populate dropdown fields in the Venafi UI. This is done via \`x-targetConfigurationRef\` in the manifest.
30618
+ **Updated learning from a REST API appliance connector**: \`getTargetConfiguration\` is NOT just a stub. It can return dynamic values that populate dropdown fields in the Venafi UI. This is done via \`x-targetConfigurationRef\` in the manifest.
30619
30619
 
30620
- **Pattern**: When the target has scopes/partitions/VDOMs, return them from \`getTargetConfiguration\`:
30620
+ **Pattern**: When the target has scopes/partitions/virtual domains, return them from \`getTargetConfiguration\`:
30621
30621
 
30622
30622
  \`\`\`go
30623
30623
  type GetTargetConfigurationResponse struct {
@@ -30646,7 +30646,7 @@ Then in the manifest, add a \`targetConfiguration\` domainSchema and reference t
30646
30646
  }
30647
30647
  \`\`\`
30648
30648
 
30649
- This creates a dropdown in the keystore form populated with actual values from the target system. The F5 BIG-IP connector uses this same pattern for partition dropdowns.
30649
+ This creates a dropdown in the keystore form populated with actual values from the target system. Other reference connectors use this same pattern for partition dropdowns.
30650
30650
 
30651
30651
  **Recommendation**: Use \`getTargetConfiguration\` whenever the target has enumerable scopes, profiles, or resource lists that users need to select from. Keep it as a stub only if there's nothing dynamic to populate.
30652
30652
 
@@ -30663,7 +30663,7 @@ This creates a dropdown in the keystore form populated with actual values from t
30663
30663
 
30664
30664
  **The problem**: The Venafi platform silently rejects this format. Discovery appears to succeed but returns: "The webhook invocation for discovering certificates failed because the discovery result is empty."
30665
30665
 
30666
- **The correct format** (confirmed by PAN Panorama, Citrix ADC, and FortiGate connectors):
30666
+ **The correct format** (confirmed by multiple reference connectors):
30667
30667
 
30668
30668
  \`\`\`json
30669
30669
  {
@@ -30688,9 +30688,9 @@ Key differences:
30688
30688
 
30689
30689
  **Why this is so dangerous**: The error message gives no clue about the wrong format. You'll spend hours debugging connectivity when the real issue is JSON structure. Always verify your response matches the types in the \`discovery-types.go\` template.
30690
30690
 
30691
- ### 12. Multi-Scope Discovery Pattern (VDOMs, Partitions, Tenants)
30691
+ ### 12. Multi-Scope Discovery Pattern (Virtual Domains, Partitions, Tenants)
30692
30692
 
30693
- **The pattern**: Many network appliances have scoping concepts (FortiGate VDOMs, F5 partitions, AVI tenants). Discovery should support both "discover all" and "discover specific scope."
30693
+ **The pattern**: Many network appliances have scoping concepts (virtual domains, partitions, tenants). Discovery should support both "discover all" and "discover specific scope."
30694
30694
 
30695
30695
  **Implementation**:
30696
30696
  1. The discovery request includes a scope field (e.g., \`vdom\`)
@@ -30737,7 +30737,7 @@ With localization: \`"vdomLabel": "VDOM (leave blank to discover all)"\`
30737
30737
  **What actually happens on network appliances**: Certificate data is split across APIs:
30738
30738
  - **Operational/Monitor APIs** return rich metadata (subject, issuer, serial number, fingerprint, validity dates) but NOT the PEM certificate content
30739
30739
  - **Configuration/CMDB APIs** return the actual PEM content, but only when fetching individual certificates \u2014 **batch/list endpoints strip heavy blob fields** to keep responses small
30740
- - This split is common across FortiGate (CMDB vs Monitor), F5 BIG-IP (mgmt vs stats), and similar REST API appliances
30740
+ - This split is common across REST API appliances (e.g., CMDB vs Monitor endpoints, mgmt vs stats endpoints)
30741
30741
 
30742
30742
  **The symptom in Venafi**: Discovery succeeds with no errors from the connector, but the Venafi Cloud activity log shows: **"Discovered certificate cannot be read"** for every certificate. This means the \`certificate\` field in the discovery response is empty or not valid PEM. The platform accepted the \`messages\` format but couldn't parse the cert data inside.
30743
30743
 
@@ -30773,7 +30773,7 @@ for _, cert := range certDetails {
30773
30773
 
30774
30774
  **Two approaches based on target type:**
30775
30775
 
30776
- **Approach A \u2014 Service-type filtering (FortiGate pattern):** For network appliances where certificates are assigned to different service types (admin HTTPS, SSL VPN, IPsec VPN, VIPs, SSL inspection), the \`discoveryTypes\` match the appliance's service categories:
30776
+ **Approach A \u2014 Service-type filtering (network appliance pattern):** For network appliances where certificates are assigned to different service types (admin HTTPS, SSL VPN, IPsec VPN, VIPs, SSL inspection), the \`discoveryTypes\` match the appliance's service categories:
30777
30777
 
30778
30778
  \`\`\`json
30779
30779
  "oneOf": [
@@ -30787,7 +30787,7 @@ for _, cert := range certDetails {
30787
30787
 
30788
30788
  These map 1:1 to the binding types. The connector queries only the selected service endpoints.
30789
30789
 
30790
- **Approach B \u2014 Usage-based filtering (APIC pattern):** For API management platforms where certificates serve different infrastructure roles, the \`discoveryTypes\` match certificate usage categories that the admin would recognize from their platform:
30790
+ **Approach B \u2014 Usage-based filtering (API management platform pattern):** For API management platforms where certificates serve different infrastructure roles, the \`discoveryTypes\` match certificate usage categories that the admin would recognize from their platform:
30791
30791
 
30792
30792
  \`\`\`json
30793
30793
  "oneOf": [
@@ -30800,7 +30800,7 @@ These map 1:1 to the binding types. The connector queries only the selected serv
30800
30800
 
30801
30801
  Here, the first three types all come from the same data source (keystores), but are classified after enumeration based on how they're used. The fourth comes from a separate data source (consumer apps). This approach replaces a less intuitive "profileBoundOnly" toggle \u2014 unchecking "Unassigned Keystores" achieves the same filtering in platform-native terms.
30802
30802
 
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.
30803
+ **Key: choosing the right categories.** Ask: "How would an admin of this platform describe their certificates?" For a network appliance, it's by service type (VPN, firewall, management). For an API management platform, it's by infrastructure role (gateway, backend, unassigned, consumer). For a load balancer, it might be by partition or virtual server type. The checkboxes should match the admin's mental model.
30804
30804
 
30805
30805
  **Implementation \u2014 binding metadata with per-type dropdown fields:**
30806
30806
 
@@ -30902,9 +30902,9 @@ Reporting configuration-level TLS settings (what the platform *says* it supports
30902
30902
  - Regular fields (no readOnly) \u2014 same result: stored but invisible in the UI
30903
30903
  - Fields added to \`x-primaryKey\` \u2014 **visible** in the UI immediately
30904
30904
 
30905
- **The rule**: The Venafi UI installation detail view renders ONLY the fields listed in the manifest's \`x-primaryKey\` arrays for both keystore and binding sections. No exceptions. No production connector (FortiGate, F5, Citrix ADC, A10) uses \`readOnly: true\` \u2014 all visible metadata goes through \`x-primaryKey\`.
30905
+ **The rule**: The Venafi UI installation detail view renders ONLY the fields listed in the manifest's \`x-primaryKey\` arrays for both keystore and binding sections. No exceptions. No production connector uses \`readOnly: true\` \u2014 all visible metadata goes through \`x-primaryKey\`.
30906
30906
 
30907
- **Verification**: FortiGate shows \`certificateName\`, \`vdom\`, \`bindingType\`, and \`targetName\` in the UI \u2014 every one of them is in \`x-primaryKey\`. Fields not in \`x-primaryKey\` are never shown regardless of any other manifest attributes.
30907
+ **Verification**: Reference connectors show \`certificateName\`, \`vdom\`, \`bindingType\`, and \`targetName\` in the UI \u2014 every one of them is in \`x-primaryKey\`. Fields not in \`x-primaryKey\` are never shown regardless of any other manifest attributes.
30908
30908
 
30909
30909
  **What administrators actually want**: When a platform admin looks at Venafi, they want to see the same context they see in their platform admin UI:
30910
30910
  - **Certificate usage**: "api-gateway" vs "outbound-mtls" vs "keystore-only"
@@ -30941,7 +30941,7 @@ Reporting configuration-level TLS settings (what the platform *says* it supports
30941
30941
  **What actually happens with readOnly fields**:
30942
30942
  - The Venafi platform **stores** the data correctly (confirmed via REST API queries)
30943
30943
  - The Venafi UI **does NOT display** readOnly fields in the installation detail view
30944
- - No production machine connector (FortiGate, F5, Citrix ADC, A10, PAN Panorama) uses \`readOnly: true\`
30944
+ - No production machine connector uses \`readOnly: true\`
30945
30945
  - The UI only renders fields listed in \`x-primaryKey\` (see section 16)
30946
30946
 
30947
30947
  **When readOnly might be useful**: Only for fields that are genuinely for internal use \u2014 like a platform UUID (\`keystoreId\`) used by the connector code during provisioning to target the correct resource, but not needed by human users in the UI. Even then, consider whether the field is worth including at all if users can't see it.
@@ -31042,7 +31042,7 @@ if binding.ApiNames == "" {
31042
31042
  4. Cross-reference with connector's discovery response \u2014 find MIs where a primaryKey field is empty
31043
31043
  5. Add a default value for the empty field, rebuild, and re-deploy
31044
31044
 
31045
- **Proven defaults from APIC connector**:
31045
+ **Proven defaults from an API management platform connector**:
31046
31046
  - \`catalogName\`: "org-level" for keystores/profiles not deployed to any catalog
31047
31047
  - \`productName\`: "N/A" for catalogs without published products
31048
31048
  - \`apiNames\`: "N/A" for catalogs without published APIs
@@ -31060,7 +31060,7 @@ if binding.ApiNames == "" {
31060
31060
 
31061
31061
  With catalog-level enrichment, BOTH entries show: Product = "Product Discovery, Customer Platform", APIs = "Product Catalog API, Customer API, Order API". This is misleading and unhelpful.
31062
31062
 
31063
- **The fix \u2014 Profile-level enrichment via title matching**: When the target platform doesn't provide a direct API-to-profile mapping (e.g., APIC's \`tls_client_profile_urls\` is empty on published APIs), use the TLS profile title as a heuristic to match against product names. Platform administrators typically follow naming conventions where profile titles encode the product/service context:
31063
+ **The fix \u2014 Profile-level enrichment via title matching**: When the target platform doesn't provide a direct API-to-profile mapping (e.g., the platform's \`tls_client_profile_urls\` is empty on published APIs), use the TLS profile title as a heuristic to match against product names. Platform administrators typically follow naming conventions where profile titles encode the product/service context:
31064
31064
 
31065
31065
  \`\`\`go
31066
31066
  // matchProfileToProduct matches a TLS profile to a specific product
@@ -31102,7 +31102,7 @@ func matchProfileToProduct(profileTitle string, cp catalogProductInfo) (string,
31102
31102
  - **\`discoveryTypes\` (array field with oneOf checkboxes)**: \`x-labelLocalizationKey\` is NOT resolved \u2014 the raw key text appears as the label in the UI (e.g., "discovery.typesLabel" shows literally). You MUST use the \`"title"\` property instead.
31103
31103
  - **Boolean discovery fields** (e.g., \`excludeExpiredCertificates\`): \`x-labelLocalizationKey\` DOES work, but ONLY with the nested \`"discovery.xxx"\` pattern.
31104
31104
 
31105
- **The correct pattern (confirmed by FortiGate, verified on APIC):**
31105
+ **The correct pattern (confirmed across multiple connectors):**
31106
31106
 
31107
31107
  \`\`\`json
31108
31108
  "discovery": {
@@ -31207,11 +31207,11 @@ for _, chainDER := range bundle.CertificateChain {
31207
31207
  2. Is the root CA ever included, or only intermediates?
31208
31208
  3. Should connectors assume the order and pass through, or re-sort based on issuer/subject matching?
31209
31209
 
31210
- This matters for targets like F5, Citrix ADC, and Java keystores where incorrect chain order causes TLS handshake failures. Until documented, connectors should pass through in the order received and note this assumption in their documentation.
31210
+ This matters for targets like load balancers, ADCs, and Java keystores where incorrect chain order causes TLS handshake failures. Until documented, connectors should pass through in the order received and note this assumption in their documentation.
31211
31211
 
31212
31212
  ### 25. DER-to-PEM Conversion for REST API Discovery
31213
31213
 
31214
- **The pattern**: When a REST API target returns certificates as base64-encoded DER (common for appliances like DataPower, FortiGate), the discovery response must convert to PEM. This conversion is easy to miss because the API response looks like a base64 string that could be passed through directly.
31214
+ **The pattern**: When a REST API target returns certificates as base64-encoded DER (common for REST API appliances), the discovery response must convert to PEM. This conversion is easy to miss because the API response looks like a base64 string that could be passed through directly.
31215
31215
 
31216
31216
  \`\`\`go
31217
31217
  // REST API returns base64 DER \u2014 MUST convert to PEM for discovery response
@@ -31309,7 +31309,7 @@ Getting these wrong can cause the target service to refuse to start.
31309
31309
 
31310
31310
  ### 1. Unit Tests
31311
31311
 
31312
- The Splunk connector has zero unit tests. This was acceptable for an MVP but should be addressed from the start:
31312
+ The SSH-based connector has zero unit tests. This was acceptable for an MVP but should be addressed from the start:
31313
31313
 
31314
31314
  - Mock \`ClientServices\` to test handlers without SSH
31315
31315
  - Test certificate parsing with known PEM files
@@ -31357,13 +31357,13 @@ Or return \`result: false\` with a helpful message if the target software isn't
31357
31357
 
31358
31358
  ---
31359
31359
 
31360
- ## REST API Appliance Patterns (FortiGate, APIC, DataPower)
31360
+ ## REST API Appliance Patterns
31361
31361
 
31362
31362
  These patterns apply to machine connectors that communicate via REST API rather than SSH.
31363
31363
 
31364
31364
  ### 20. PKCS12 Is the Dominant Provisioning Format for REST API Targets
31365
31365
 
31366
- **The pattern**: Most REST API appliances (FortiGate, IBM APIC, F5, Citrix ADC, PAN Panorama) accept certificate bundles as **PKCS12** files, not raw PEM files. The connector must build a PKCS12 bundle from Venafi's DER-encoded certificate bundle.
31366
+ **The pattern**: Most REST API appliances accept certificate bundles as **PKCS12** files, not raw PEM files. The connector must build a PKCS12 bundle from Venafi's DER-encoded certificate bundle.
31367
31367
 
31368
31368
  **Implementation** (using \`go-pkcs12\`):
31369
31369
 
@@ -31410,7 +31410,7 @@ func parsePrivateKey(keyDER []byte) (crypto.PrivateKey, error) {
31410
31410
  }
31411
31411
  \`\`\`
31412
31412
 
31413
- This sequence is proven across FortiGate, APIC, and DataPower connectors. Never assume RSA.
31413
+ This sequence is proven across multiple REST API appliance connectors. Never assume RSA.
31414
31414
 
31415
31415
  ### 22. CRITICAL: Binding Must Never Be Nil in Discovery Results
31416
31416
 
@@ -31446,7 +31446,7 @@ if hasBindings {
31446
31446
  2. **If yes**: Upload a new cert with a unique name (e.g., \`certName_YYMMDD_serial\`), update the binding to point to the new cert, then optionally delete the old cert
31447
31447
  3. **If no**: Simply upload the cert and create the binding reference
31448
31448
 
31449
- This is the proven pattern for FortiGate and PAN Panorama where certificates are immutable once created. APIC handles this differently \u2014 its PATCH API preserves UUIDs when updating keystore content in-place.
31449
+ This is the proven pattern for appliances where certificates are immutable once created. Some API management platforms handle this differently \u2014 their PATCH API preserves UUIDs when updating keystore content in-place.
31450
31450
 
31451
31451
  **Key**: The \`installCertificateBundle\` handler returns the updated keystore (with the new cert name), which is passed to \`configureInstallationEndpoint\`. Use the keystore fields to carry the cert name between activities.
31452
31452
 
@@ -31490,9 +31490,9 @@ if pageEnd < len(allCerts) {
31490
31490
  // page = nil means "done"
31491
31491
  \`\`\`
31492
31492
 
31493
- ### 25. OAuth2 Token Exchange (APIC Pattern)
31493
+ ### 25. OAuth2 Token Exchange (API Management Platform Pattern)
31494
31494
 
31495
- **The pattern**: Some REST API targets (IBM APIC) use OAuth2 token exchange instead of static API keys. The connector exchanges credentials for a bearer token at the start of each endpoint call.
31495
+ **The pattern**: Some REST API targets (API management platforms) use OAuth2 token exchange instead of static API keys. The connector exchanges credentials for a bearer token at the start of each endpoint call.
31496
31496
 
31497
31497
  \`\`\`go
31498
31498
  func (h *Handler) authenticate() error {
@@ -31516,34 +31516,34 @@ func (h *Handler) authenticate() error {
31516
31516
  \`\`\`
31517
31517
 
31518
31518
  **IMPORTANT \u2014 Token endpoint body format varies by target**:
31519
- - **IBM APIC**: Requires \`application/json\` body (NOT form-urlencoded). Sending form-urlencoded silently fails.
31519
+ - **Some API platforms**: Require \`application/json\` body (NOT form-urlencoded). Sending form-urlencoded silently fails.
31520
31520
  - **Standard OAuth2**: Uses \`application/x-www-form-urlencoded\` per RFC 6749 (use \`SetFormData()\`).
31521
31521
  - Always check the target API documentation for the expected content type.
31522
31522
 
31523
- **Key**: Token has a TTL (8 hours for APIC). For connectors where each endpoint call is stateless, exchange the token at the start of each call \u2014 don't cache across calls.
31523
+ **Key**: Token has a TTL (e.g., 8 hours). For connectors where each endpoint call is stateless, exchange the token at the start of each call \u2014 don't cache across calls.
31524
31524
 
31525
31525
  ### 26. REST API Targets May Use PATCH Instead of PUT
31526
31526
 
31527
- **The learning**: IBM APIC hosted environments return 405 (Method Not Allowed) for PUT on keystore updates. APIC requires PATCH. FortiGate requires PUT for most updates. DataPower uses PUT for config objects.
31527
+ **The learning**: Some API management platforms return 405 (Method Not Allowed) for PUT on keystore updates and require PATCH instead. Other REST API appliances require PUT for most updates.
31528
31528
 
31529
31529
  **The fix**: Don't assume PUT or PATCH \u2014 check the target API documentation and test both during development. Make the HTTP method part of your helper function:
31530
31530
 
31531
31531
  \`\`\`go
31532
31532
  func (h *Handler) UpdateResource(path string, body interface{}) (*resty.Response, error) {
31533
- // APIC: PATCH; FortiGate: PUT; DataPower: PUT
31533
+ // some targets require PATCH; others use PUT
31534
31534
  return h.client.R().SetBody(body).Patch(h.baseURL + path) // or .Put()
31535
31535
  }
31536
31536
  \`\`\`
31537
31537
 
31538
31538
  ### 27. Protocols Field Must Never Be Empty Array for Machine Identities
31539
31539
 
31540
- **The learning from APIC**: Venafi platform silently drops machine identities with \`protocols: []\` (empty array) in the discovery response. Omitting \`protocols\` entirely (with \`omitempty\`) causes hard discovery failure.
31540
+ **The learning**: Venafi platform silently drops machine identities with \`protocols: []\` (empty array) in the discovery response. Omitting \`protocols\` entirely (with \`omitempty\`) causes hard discovery failure.
31541
31541
 
31542
31542
  **The fix**: For onboard discovery, do NOT set protocols at all. Leave the field out of the response struct entirely (don't even define it). Protocols are for network discovery connectors that observe live TLS connections, not for onboard discovery that reads platform inventory.
31543
31543
 
31544
31544
  ### 28. Null-Safe JSON Arrays in Discovery Responses
31545
31545
 
31546
- **The learning from APIC**: Go's \`encoding/json\` marshals a nil slice as \`null\`, but the Venafi platform expects empty arrays to be \`[]\` (not \`null\`). A \`null\` value for \`certificateChain\` or \`machineIdentities\` can cause discovery failures or silent data loss.
31546
+ **The learning**: Go's \`encoding/json\` marshals a nil slice as \`null\`, but the Venafi platform expects empty arrays to be \`[]\` (not \`null\`). A \`null\` value for \`certificateChain\` or \`machineIdentities\` can cause discovery failures or silent data loss.
31547
31547
 
31548
31548
  **The symptom**: Discovery responses with \`"certificateChain": null\` or \`"machineIdentities": null\` are silently rejected or produce incomplete results.
31549
31549
 
@@ -31660,7 +31660,7 @@ func truncateMetadata(s string, totalCount int) string {
31660
31660
 
31661
31661
  **The mistake**: Using modern PKCS12 encoding (AES-256-CBC + SHA-256) when building PKCS12 bundles for certificate import to network appliances.
31662
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).
31663
+ **The problem**: Many network appliances cannot parse modern PKCS12 encoding. The import API returns success or a vague error, but the certificate is not usable. These appliances specifically require the legacy PKCS12 format (3DES + SHA1).
31664
31664
 
31665
31665
  **The fix**: When building PKCS12 bundles in Go, use the Legacy encoder:
31666
31666
 
@@ -31670,7 +31670,7 @@ import gopkcs12 "software.sslmate.com/src/go-pkcs12"
31670
31670
  // CORRECT: Legacy encoding (3DES/SHA1) \u2014 compatible with network appliances
31671
31671
  p12Data, err := gopkcs12.Legacy.Encode(key, cert, caCerts, password)
31672
31672
 
31673
- // WRONG: Modern encoding (AES-256/SHA-256) \u2014 rejected by FortiGate and similar appliances
31673
+ // WRONG: Modern encoding (AES-256/SHA-256) \u2014 rejected by many network appliances
31674
31674
  // p12Data, err := gopkcs12.Modern.Encode(key, cert, caCerts, password)
31675
31675
  \`\`\`
31676
31676
 
@@ -31911,7 +31911,7 @@ if _, err := x509.ParsePKCS8PrivateKey(keyDER); err == nil {
31911
31911
 
31912
31912
  **When to use which approach**:
31913
31913
  - **Target accepts PEM** (GCP, AWS, cloud APIs): Use the no-re-marshal approach above. Just detect the type for the PEM header and wrap the original DER bytes.
31914
- - **Target requires PKCS12** (FortiGate, network appliances): You MUST parse into Go types to pass to \`pkcs12.Encode()\`. Re-marshaling is unavoidable and acceptable here because the PKCS12 encoder produces its own encoding.
31914
+ - **Target requires PKCS12** (network appliances): You MUST parse into Go types to pass to \`pkcs12.Encode()\`. Re-marshaling is unavoidable and acceptable here because the PKCS12 encoder produces its own encoding.
31915
31915
 
31916
31916
  ### 42. CRITICAL: Handle Both DER and PEM Input in Certificate Bundle
31917
31917
 
@@ -32671,8 +32671,8 @@ var TEMPLATES = {
32671
32671
  "// File: internal/app/domain/binding.go",
32672
32672
  "// CUSTOMIZE: Replace fields with your target-specific service data.",
32673
32673
  "// Choose the pattern that fits your target:",
32674
- "// SSH-based targets: ServiceType + ServicePort (e.g., Splunk, Apache, Nginx)",
32675
- "// REST API appliances: BindingType + TargetName (e.g., FortiGate, F5, Citrix ADC)",
32674
+ "// SSH-based targets: ServiceType + ServicePort (e.g., web server, app server)",
32675
+ "// REST API appliances: BindingType + TargetName (e.g., load balancer, firewall, ADC)",
32676
32676
  "// ============================================================",
32677
32677
  "",
32678
32678
  "package domain",
@@ -32685,7 +32685,7 @@ var TEMPLATES = {
32685
32685
  '// ServicePort int `json:"servicePort"` // port the service listens on',
32686
32686
  "// }",
32687
32687
  "",
32688
- "// --- Option B: REST API appliance binding (FortiGate pattern) ---",
32688
+ "// --- Option B: REST API appliance binding (network appliance pattern) ---",
32689
32689
  "",
32690
32690
  "// Binding represents how a certificate is used by a target service.",
32691
32691
  '// BindingType identifies the service category (e.g., "admin-https", "ssl-vpn",',
@@ -32702,17 +32702,49 @@ var TEMPLATES = {
32702
32702
  "// ============================================================",
32703
32703
  "// File: internal/app/domain/certificate_bundle.go",
32704
32704
  "// DO NOT MODIFY: This is standard across all connectors.",
32705
+ "//",
32706
+ '// IMPORTANT: The manifest schema uses "contentEncoding": "base64" and',
32707
+ '// "x-encrypted-base64": true on the privateKey field.',
32708
+ "// Venafi Cloud sends certificateBundle fields as base64-encoded DER.",
32709
+ "//",
32710
+ "// Choose ONE option below:",
32711
+ "//",
32712
+ "// Option A (RECOMMENDED for REST API connectors):",
32713
+ "// Use string fields and explicitly base64.StdEncoding.DecodeString() each field.",
32714
+ "// This is the most explicit pattern and proven across multiple connector builds.",
32715
+ "// After decoding, you have DER bytes \u2014 convert to PEM with pem.EncodeToMemory().",
32716
+ "//",
32717
+ "// Option B (works for SSH connectors):",
32718
+ "// Use []byte fields \u2014 Go's JSON unmarshaler auto-decodes base64 into []byte.",
32719
+ "// After unmarshaling, fields already contain DER bytes.",
32720
+ "//",
32721
+ "// Both options produce the same result: DER-encoded binary data.",
32705
32722
  "// ============================================================",
32706
32723
  "",
32707
32724
  "package domain",
32708
32725
  "",
32709
32726
  "// CertificateBundle represents the certificate data sent by Venafi for provisioning.",
32727
+ "//",
32728
+ "// Option A: string fields (RECOMMENDED \u2014 explicit base64 decode)",
32729
+ "// certDER, _ := base64.StdEncoding.DecodeString(bundle.Certificate)",
32730
+ "// keyDER, _ := base64.StdEncoding.DecodeString(bundle.PrivateKey)",
32731
+ "// for _, chainEntry := range bundle.CertificateChain {",
32732
+ "// chainDER, _ := base64.StdEncoding.DecodeString(chainEntry)",
32733
+ '// chainPEM = append(chainPEM, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: chainDER})...)',
32734
+ "// }",
32710
32735
  "type CertificateBundle struct {",
32711
- ' Certificate []byte `json:"certificate"`',
32712
- ' PrivateKey []byte `json:"privateKey"`',
32713
- ' CertificateChain [][]byte `json:"certificateChain"`',
32736
+ ' Certificate string `json:"certificate"`',
32737
+ ' PrivateKey string `json:"privateKey"`',
32738
+ ' CertificateChain []string `json:"certificateChain"`',
32714
32739
  "}",
32715
32740
  "",
32741
+ "// Option B: []byte fields (Go auto-decodes base64 \u2014 fields contain DER bytes directly)",
32742
+ "// type CertificateBundle struct {",
32743
+ '// Certificate []byte `json:"certificate"`',
32744
+ '// PrivateKey []byte `json:"privateKey"`',
32745
+ '// CertificateChain [][]byte `json:"certificateChain"`',
32746
+ "// }",
32747
+ "",
32716
32748
  "// ============================================================",
32717
32749
  "// File: internal/app/domain/client.go",
32718
32750
  "// This is the same across all SSH-based connectors.",
@@ -32845,6 +32877,20 @@ var TEMPLATES = {
32845
32877
  content: [
32846
32878
  "// TEMPLATE: Discovery request/response types. Standard across all connectors.",
32847
32879
  "// File: internal/app/discovery/types.go",
32880
+ "//",
32881
+ "// CRITICAL RULES:",
32882
+ '// 1. The response top-level key MUST be "messages" \u2014 the platform silently rejects other names.',
32883
+ "// 2. Use VALUE types (not pointers) for Keystore, Binding, and slice elements.",
32884
+ "// Pointers serialize to null when nil, which the platform may reject.",
32885
+ "// 3. Initialize all slices with make() to ensure JSON [] not null.",
32886
+ "// 4. The certificate field must contain a PEM string (not DER, not base64).",
32887
+ "// Always normalize through pem.EncodeToMemory() before returning \u2014 even if",
32888
+ "// the source is already PEM \u2014 to guarantee trailing newline and line wrapping.",
32889
+ '// 5. discoveryPage: null in the response signals "done" to the platform.',
32890
+ '// 6. Each DiscoveredCertificate MUST include an "installations" array with at least',
32891
+ "// one entry containing hostname, ipAddress, and port. Without this field, the",
32892
+ "// platform silently discards discovered certificates \u2014 no error, discovery shows",
32893
+ '// as "complete" but no certificates appear in the UI.',
32848
32894
  "",
32849
32895
  "package discovery",
32850
32896
  "",
@@ -32860,11 +32906,10 @@ var TEMPLATES = {
32860
32906
  "type DiscoverCertificatesConfiguration struct {",
32861
32907
  " // DiscoveryTypes lets users toggle which service types to discover.",
32862
32908
  " // For REST API appliances, this filters which binding endpoints are queried.",
32863
- ' // Example values: "admin-https", "ssl-vpn", "ipsec-vpn", "vip", "ssl-inspection"',
32864
32909
  ' // An empty slice means "discover all types".',
32865
32910
  ' DiscoveryTypes []string `json:"discoveryTypes"`',
32866
32911
  ' ExcludeExpiredCertificates bool `json:"excludeExpiredCertificates"`',
32867
- " // Scope field for multi-tenant targets (e.g., VDOM, partition, tenant).",
32912
+ " // Scope field for multi-tenant targets (e.g., partition, tenant, virtual domain).",
32868
32913
  " // Leave blank to auto-discover all scopes.",
32869
32914
  ' Scope string `json:"scope"`',
32870
32915
  "}",
@@ -32878,29 +32923,82 @@ var TEMPLATES = {
32878
32923
  "}",
32879
32924
  "",
32880
32925
  "// DiscoveryPage represents the current pagination state.",
32926
+ '// Return nil to signal "discovery complete" to the platform.',
32927
+ "// Return a non-nil DiscoveryPage to request another page.",
32881
32928
  "type DiscoveryPage struct {",
32882
- ' DiscoveryType *string `json:"discoveryType,omitempty"`',
32883
- ' Paginator string `json:"paginator"`',
32929
+ ' DiscoveryType string `json:"discoveryType"`',
32930
+ ' Paginator string `json:"paginator"`',
32884
32931
  "}",
32885
32932
  "",
32886
32933
  "// DiscoverCertificatesResponse represents the response to a discovery request.",
32934
+ '// CRITICAL: The top-level JSON key MUST be "messages" \u2014 the platform ignores other keys.',
32887
32935
  "type DiscoverCertificatesResponse struct {",
32936
+ ' Messages []DiscoveredCertificate `json:"messages"`',
32888
32937
  ' Page *DiscoveryPage `json:"discoveryPage"`',
32889
- ' Messages []*DiscoveredCertificate `json:"messages"`',
32890
32938
  "}",
32891
32939
  "",
32892
32940
  "// DiscoveredCertificate represents a single certificate found during discovery.",
32941
+ "// CRITICAL: Use VALUE types (not pointers) \u2014 pointers serialize to null which the platform rejects.",
32942
+ "// CRITICAL: The Installations field is REQUIRED \u2014 without it, the platform silently discards",
32943
+ '// the certificate. Discovery will show as "complete" but no certs appear in the UI.',
32893
32944
  "type DiscoveredCertificate struct {",
32894
- ' Certificate string `json:"certificate"`',
32895
- ' CertificateChain []string `json:"certificateChain"`',
32896
- ' MachineIdentities []*MachineIdentity `json:"machineIdentities"`',
32945
+ ' Certificate string `json:"certificate"` // PEM string (use pem.EncodeToMemory)',
32946
+ ' CertificateChain []string `json:"certificateChain"` // PEM strings, initialize with make()',
32947
+ ' MachineIdentities []MachineIdentity `json:"machineIdentities"` // VALUE type slice, initialize with make()',
32948
+ ' Installations []Installation `json:"installations"` // REQUIRED \u2014 at least one entry',
32949
+ "}",
32950
+ "",
32951
+ "// Installation identifies where the certificate was found (host + port).",
32952
+ "// CRITICAL: At least one Installation is required per DiscoveredCertificate.",
32953
+ "// Without this field, the platform silently drops the certificate from results.",
32954
+ "type Installation struct {",
32955
+ ' Hostname string `json:"hostname"`',
32956
+ ' IPAddress string `json:"ipAddress"`',
32957
+ ' Port int `json:"port"`',
32897
32958
  "}",
32898
32959
  "",
32899
32960
  "// MachineIdentity represents a certificate usage found during discovery.",
32961
+ "// CRITICAL: Use VALUE types for Keystore and Binding \u2014 never pointers.",
32962
+ '// Binding must NEVER be empty \u2014 use a default like "unbound" for certs with no bindings.',
32963
+ "// All x-primaryKey fields must have non-empty values or the identity is silently dropped.",
32900
32964
  "type MachineIdentity struct {",
32901
- ' Keystore *domain.Keystore `json:"keystore"`',
32902
- ' Binding *domain.Binding `json:"binding"`',
32903
- "}"
32965
+ ' Keystore domain.Keystore `json:"keystore"`',
32966
+ ' Binding domain.Binding `json:"binding"`',
32967
+ "}",
32968
+ "",
32969
+ "// ============================================================",
32970
+ "// Example: Building a discovery response",
32971
+ "// ============================================================",
32972
+ "//",
32973
+ "// messages := make([]DiscoveredCertificate, 0) // MUST use make() \u2014 nil serializes as null",
32974
+ "// chainPEMs := make([]string, 0) // same for chains",
32975
+ "//",
32976
+ "// // Always normalize PEM through pem.EncodeToMemory, even if source is already PEM:",
32977
+ "// block, _ := pem.Decode([]byte(rawPEM))",
32978
+ "// normalizedPEM := string(pem.EncodeToMemory(block))",
32979
+ "//",
32980
+ "// // For REST API targets returning base64 DER:",
32981
+ "// derBytes, _ := base64.StdEncoding.DecodeString(apiResponse.Base64)",
32982
+ '// certPEM := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}))',
32983
+ "//",
32984
+ "// messages = append(messages, DiscoveredCertificate{",
32985
+ "// Certificate: normalizedPEM,",
32986
+ "// CertificateChain: chainPEMs,",
32987
+ "// MachineIdentities: []MachineIdentity{{",
32988
+ '// Keystore: domain.Keystore{CertificateName: "my-cert"},',
32989
+ '// Binding: domain.Binding{BindingType: "unbound"}, // never empty',
32990
+ "// }},",
32991
+ "// Installations: []Installation{{",
32992
+ "// Hostname: connection.HostnameOrAddress,",
32993
+ "// IPAddress: connection.HostnameOrAddress,",
32994
+ "// Port: connection.Port,",
32995
+ "// }}, // REQUIRED \u2014 without this, certs are silently dropped",
32996
+ "// })",
32997
+ "//",
32998
+ "// return c.JSON(http.StatusOK, DiscoverCertificatesResponse{",
32999
+ "// Messages: messages,",
33000
+ "// Page: nil, // nil = done; non-nil = call again",
33001
+ "// })"
32904
33002
  ].join("\n"),
32905
33003
  customizable: true
32906
33004
  },
@@ -32910,12 +33008,14 @@ var TEMPLATES = {
32910
33008
  targetPath: "internal/app/<target>/install_helpers.go",
32911
33009
  content: [
32912
33010
  "// TEMPLATE: Helper functions for certificate installation.",
32913
- "// These are proven patterns from the Splunk connector that work correctly.",
33011
+ "// These are proven patterns from multiple connector builds.",
32914
33012
  "// Include these in your install.go or a helpers.go file.",
32915
33013
  "",
32916
33014
  "package <target>",
32917
33015
  "",
32918
33016
  "import (",
33017
+ ' "crypto/ecdsa"',
33018
+ ' "crypto/rsa"',
32919
33019
  ' "crypto/x509"',
32920
33020
  ' "encoding/pem"',
32921
33021
  ' "fmt"',
@@ -32925,27 +33025,52 @@ var TEMPLATES = {
32925
33025
  ' "go.uber.org/zap"',
32926
33026
  ")",
32927
33027
  "",
32928
- "// privateKeyPEMType detects the private key type and returns the correct PEM header.",
32929
- '// IMPORTANT: Do NOT hardcode "RSA PRIVATE KEY" - Venafi may issue EC keys.',
32930
- "func privateKeyPEMType(keyBytes []byte) string {",
32931
- " // Try PKCS#1 RSA",
32932
- " if _, err := x509.ParsePKCS1PrivateKey(keyBytes); err == nil {",
32933
- ' return "RSA PRIVATE KEY"',
33028
+ "// encodeKeyToPEM detects the key type and encodes to PEM with the correct header.",
33029
+ "//",
33030
+ "// CRITICAL: When PKCS8 wraps a key, re-marshal to the native format.",
33031
+ "// Some targets validate that the DER encoding matches the PEM header.",
33032
+ '// PKCS8 DER with "RSA PRIVATE KEY" header = INVALID (DER is PKCS8, header says PKCS1).',
33033
+ '// PKCS1 DER with "RSA PRIVATE KEY" header = VALID (both agree on format).',
33034
+ "//",
33035
+ '// Do NOT hardcode "RSA PRIVATE KEY" \u2014 Venafi may issue EC or other key types.',
33036
+ "func encodeKeyToPEM(keyDER []byte) []byte {",
33037
+ " // Try PKCS8 first (most common from Venafi)",
33038
+ " key, err := x509.ParsePKCS8PrivateKey(keyDER)",
33039
+ " if err == nil {",
33040
+ " switch k := key.(type) {",
33041
+ " case *rsa.PrivateKey:",
33042
+ ' // Re-marshal to PKCS1 so DER matches "RSA PRIVATE KEY" header',
33043
+ " pkcs1DER := x509.MarshalPKCS1PrivateKey(k)",
33044
+ ' return pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: pkcs1DER})',
33045
+ " case *ecdsa.PrivateKey:",
33046
+ " ecDER, ecErr := x509.MarshalECPrivateKey(k)",
33047
+ " if ecErr == nil {",
33048
+ ' return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: ecDER})',
33049
+ " }",
33050
+ ' return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})',
33051
+ " default:",
33052
+ ' return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})',
33053
+ " }",
32934
33054
  " }",
32935
- " // Try EC",
32936
- " if _, err := x509.ParseECPrivateKey(keyBytes); err == nil {",
32937
- ' return "EC PRIVATE KEY"',
33055
+ "",
33056
+ " // Try RSA PKCS1",
33057
+ " if _, err := x509.ParsePKCS1PrivateKey(keyDER); err == nil {",
33058
+ ' return pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyDER})',
32938
33059
  " }",
32939
- " // Try PKCS#8 (wraps either RSA or EC)",
32940
- " if _, err := x509.ParsePKCS8PrivateKey(keyBytes); err == nil {",
32941
- ' return "PRIVATE KEY"',
33060
+ "",
33061
+ " // Try EC",
33062
+ " if _, err := x509.ParseECPrivateKey(keyDER); err == nil {",
33063
+ ' return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})',
32942
33064
  " }",
32943
- " // Default to PKCS#8 header (most compatible)",
32944
- ' return "PRIVATE KEY"',
33065
+ "",
33066
+ " // Fallback \u2014 generic PRIVATE KEY header",
33067
+ ' return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})',
32945
33068
  "}",
32946
33069
  "",
32947
33070
  "// buildCombinedPEM creates a combined PEM file with cert + chain + key.",
32948
33071
  "// Used when the target expects all components in a single file.",
33072
+ "// NOTE: This assumes CertificateBundle uses []byte fields (Option B).",
33073
+ "// If using string fields (Option A), decode from base64 first.",
32949
33074
  "func buildCombinedPEM(bundle *domain.CertificateBundle) ([]byte, error) {",
32950
33075
  " var pemContent strings.Builder",
32951
33076
  "",
@@ -32963,12 +33088,8 @@ var TEMPLATES = {
32963
33088
  " })))",
32964
33089
  " }",
32965
33090
  "",
32966
- " // Private key (with correct type detection)",
32967
- " keyType := privateKeyPEMType(bundle.PrivateKey)",
32968
- " pemContent.WriteString(string(pem.EncodeToMemory(&pem.Block{",
32969
- " Type: keyType,",
32970
- " Bytes: bundle.PrivateKey,",
32971
- " })))",
33091
+ " // Private key (with PKCS8 re-marshaling)",
33092
+ " pemContent.WriteString(string(encodeKeyToPEM(bundle.PrivateKey)))",
32972
33093
  "",
32973
33094
  " return []byte(pemContent.String()), nil",
32974
33095
  "}",
@@ -32996,12 +33117,9 @@ var TEMPLATES = {
32996
33117
  "}",
32997
33118
  "",
32998
33119
  "// buildKeyPEM creates a PEM file with just the private key.",
33120
+ "// Re-marshals PKCS8 keys to native format for PEM header correctness.",
32999
33121
  "func buildKeyPEM(bundle *domain.CertificateBundle) []byte {",
33000
- " keyType := privateKeyPEMType(bundle.PrivateKey)",
33001
- " return pem.EncodeToMemory(&pem.Block{",
33002
- " Type: keyType,",
33003
- " Bytes: bundle.PrivateKey,",
33004
- " })",
33122
+ " return encodeKeyToPEM(bundle.PrivateKey)",
33005
33123
  "}",
33006
33124
  "",
33007
33125
  "// backupFile creates a backup of an existing file before overwriting.",
@@ -33247,7 +33365,7 @@ func configureLogger() (*zap.Logger, error) {
33247
33365
  },
33248
33366
  "rest-client.go": {
33249
33367
  name: "rest-client.go",
33250
- description: "REST API client (handler) pattern for network appliance connectors (FortiGate, F5, Citrix ADC). Uses go-resty with multi-auth support (API token, session, PKI).",
33368
+ description: "REST API client (handler) pattern for network appliance connectors (firewalls, load balancers, ADCs). Uses go-resty with multi-auth support (API token, session, PKI).",
33251
33369
  targetPath: "internal/app/<target>/handler.go",
33252
33370
  content: [
33253
33371
  "package <target>",
@@ -33384,7 +33502,7 @@ func configureLogger() (*zap.Logger, error) {
33384
33502
  },
33385
33503
  "rest-api-service-pattern.go": {
33386
33504
  name: "rest-api-service-pattern.go",
33387
- description: "3-service decomposition pattern for REST API connectors (PAN Panorama architecture). Connection, Provisioning, and Discovery as separate services.",
33505
+ description: "3-service decomposition pattern for REST API connectors. Connection, Provisioning, and Discovery as separate services.",
33388
33506
  targetPath: "internal/app/<target>/<target>.go",
33389
33507
  content: [
33390
33508
  "package <target>",
@@ -33557,14 +33675,17 @@ ${content}`;
33557
33675
  function getMachineEndpoints(args) {
33558
33676
  const webTemplate = TEMPLATES["machine-web.go"];
33559
33677
  const testConnTemplate = TEMPLATES["test-connection.go"];
33678
+ const discoveryTypesTemplate = TEMPLATES["discovery-types.go"];
33560
33679
  let webContent = webTemplate.content;
33561
33680
  let testConnContent = testConnTemplate.content;
33681
+ let discoveryTypesContent = discoveryTypesTemplate.content;
33562
33682
  if (args?.connectorName || args?.modulePath || args?.targetPackage) {
33563
33683
  const cn = args?.connectorName || "<CONNECTOR_NAME>";
33564
33684
  const mp = args?.modulePath || "<CONNECTOR_MODULE>";
33565
33685
  const tp = args?.targetPackage || "<target>";
33566
33686
  webContent = substituteTemplate(webContent, cn, mp, tp);
33567
33687
  testConnContent = substituteTemplate(testConnContent, cn, mp, tp);
33688
+ discoveryTypesContent = substituteTemplate(discoveryTypesContent, cn, mp, tp);
33568
33689
  }
33569
33690
  return `# Machine Connector Endpoint Templates
33570
33691
 
@@ -33576,9 +33697,9 @@ A machine connector implements 5 endpoints (plus /healthz). All are POST endpoin
33576
33697
  |---|---|---|
33577
33698
  | POST /v1/testconnection | HandleTestConnection | Validate connectivity, detect target software |
33578
33699
  | POST /v1/discovercertificates | HandleDiscoverCertificates | Find certificates, return with keystore/binding metadata |
33579
- | POST /v1/installcertificatebundle | HandleInstallCertificateBundle | Write cert+chain+key files to target |
33580
- | POST /v1/configureinstallationendpoint | HandleConfigureInstallationEndpoint | Restart/reload target service |
33581
- | POST /v1/gettargetconfiguration | HandleGetTargetConfiguration | Return target info (stub is OK) |
33700
+ | POST /v1/installcertificatebundle | HandleInstallCertificateBundle | Install cert+chain+key on target |
33701
+ | POST /v1/configureinstallationendpoint | HandleConfigureInstallationEndpoint | Configure service binding for the installed cert |
33702
+ | POST /v1/gettargetconfiguration | HandleGetTargetConfiguration | Return target info for UI dropdowns (stub is OK) |
33582
33703
  | GET /healthz | (inline) | Kubernetes liveness probe |
33583
33704
 
33584
33705
  ## WebhookService Interface (web.go)
@@ -33597,17 +33718,172 @@ This is a complete test connection handler with SSH validation. Customize step 4
33597
33718
  ${testConnContent}
33598
33719
  \`\`\`
33599
33720
 
33721
+ ## Discovery Response Types (discovery/types.go)
33722
+
33723
+ These types define the discovery request/response structures. The response format is critical \u2014 incorrect types cause silent failures.
33724
+
33725
+ \`\`\`go
33726
+ ${discoveryTypesContent}
33727
+ \`\`\`
33728
+
33729
+ ## Install Certificate Bundle Handler
33730
+
33731
+ CRITICAL: The response MUST wrap the updated keystore in \`{"keystore": {...}}\`.
33732
+ Returning the keystore directly (without the wrapper) causes: \`"missing json schema in plugin manifest for entity keystoreId"\`.
33733
+
33734
+ \`\`\`go
33735
+ // File: internal/app/<target>/install_certificate_bundle.go
33736
+
33737
+ // InstallCertificateBundleRequest contains the request for installing a certificate.
33738
+ type InstallCertificateBundleRequest struct {
33739
+ Connection *domain.Connection \`json:"connection"\`
33740
+ Keystore *domain.Keystore \`json:"keystore"\`
33741
+ CertificateBundle *domain.CertificateBundle \`json:"certificateBundle"\`
33742
+ }
33743
+
33744
+ // InstallCertificateBundleResponse wraps the updated keystore.
33745
+ // CRITICAL: Response MUST be {"keystore": {...}} \u2014 platform validates against manifest schema.
33746
+ type InstallCertificateBundleResponse struct {
33747
+ Keystore domain.Keystore \`json:"keystore"\`
33748
+ }
33749
+
33750
+ func (svc *WebhookServiceImpl) HandleInstallCertificateBundle(c echo.Context) error {
33751
+ zap.L().Info("installCertificateBundle workflow activity started")
33752
+
33753
+ // Step 1: Parse request
33754
+ req := InstallCertificateBundleRequest{}
33755
+ if err := c.Bind(&req); err != nil {
33756
+ zap.L().Error("install step 1 failed: invalid request", zap.Error(err))
33757
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
33758
+ }
33759
+
33760
+ // Step 2: Connect to target
33761
+ client := svc.ClientServices.NewClient(req.Connection)
33762
+ if err := svc.ClientServices.Connect(client); err != nil {
33763
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
33764
+ }
33765
+ defer svc.ClientServices.Close(client)
33766
+
33767
+ // Step 3: Install the certificate
33768
+ // CUSTOMIZE: Implement your target-specific installation logic here.
33769
+ // For SSH targets: write PEM files to disk, set permissions.
33770
+ // For REST API targets: upload via API (PKCS12, PEM, or target-specific format).
33771
+ //
33772
+ // Certificate bundle fields are base64-encoded DER (if using string fields):
33773
+ // certDER, _ := base64.StdEncoding.DecodeString(req.CertificateBundle.Certificate)
33774
+ // keyDER, _ := base64.StdEncoding.DecodeString(req.CertificateBundle.PrivateKey)
33775
+ // for _, entry := range req.CertificateBundle.CertificateChain {
33776
+ // chainDER, _ := base64.StdEncoding.DecodeString(entry)
33777
+ // }
33778
+ //
33779
+ // If using []byte fields, Go auto-decodes \u2014 fields already contain DER bytes.
33780
+
33781
+ updatedKeystore := *req.Keystore // Copy and update as needed
33782
+
33783
+ zap.L().Info("installCertificateBundle completed",
33784
+ zap.Any("keystore", updatedKeystore),
33785
+ )
33786
+
33787
+ // CRITICAL: Wrap in {"keystore": {...}} \u2014 do NOT return the keystore directly
33788
+ return c.JSON(http.StatusOK, InstallCertificateBundleResponse{Keystore: updatedKeystore})
33789
+ }
33790
+ \`\`\`
33791
+
33792
+ ## Configure Installation Endpoint Handler
33793
+
33794
+ CRITICAL: The response MUST wrap the binding in \`{"binding": {...}}\`.
33795
+
33796
+ \`\`\`go
33797
+ // File: internal/app/<target>/configure_installation_endpoint.go
33798
+
33799
+ // ConfigureInstallationEndpointRequest contains the request for configuring the binding.
33800
+ type ConfigureInstallationEndpointRequest struct {
33801
+ Connection *domain.Connection \`json:"connection"\`
33802
+ Keystore *domain.Keystore \`json:"keystore"\`
33803
+ Binding *domain.Binding \`json:"binding"\`
33804
+ }
33805
+
33806
+ // ConfigureInstallationEndpointResponse wraps the binding.
33807
+ // CRITICAL: Response MUST be {"binding": {...}} \u2014 platform validates against manifest schema.
33808
+ type ConfigureInstallationEndpointResponse struct {
33809
+ Binding domain.Binding \`json:"binding"\`
33810
+ }
33811
+
33812
+ func (svc *WebhookServiceImpl) HandleConfigureInstallationEndpoint(c echo.Context) error {
33813
+ zap.L().Info("configureInstallationEndpoint workflow activity started")
33814
+
33815
+ // Step 1: Parse request
33816
+ req := ConfigureInstallationEndpointRequest{}
33817
+ if err := c.Bind(&req); err != nil {
33818
+ zap.L().Error("configure step 1 failed: invalid request", zap.Error(err))
33819
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
33820
+ }
33821
+
33822
+ // Step 2: Connect to target
33823
+ client := svc.ClientServices.NewClient(req.Connection)
33824
+ if err := svc.ClientServices.Connect(client); err != nil {
33825
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
33826
+ }
33827
+ defer svc.ClientServices.Close(client)
33828
+
33829
+ // Step 3: Configure the binding
33830
+ // CUSTOMIZE: Implement your target-specific binding logic here.
33831
+ // For SSH targets: restart/reload the service (systemctl restart, service reload, etc.).
33832
+ // For REST API targets: update the binding via API (assign cert to VIP, profile, etc.).
33833
+
33834
+ zap.L().Info("configureInstallationEndpoint completed",
33835
+ zap.Any("binding", req.Binding),
33836
+ )
33837
+
33838
+ // CRITICAL: Wrap in {"binding": {...}} \u2014 do NOT return the binding directly
33839
+ return c.JSON(http.StatusOK, ConfigureInstallationEndpointResponse{Binding: *req.Binding})
33840
+ }
33841
+ \`\`\`
33842
+
33843
+ ## Get Target Configuration Handler
33844
+
33845
+ Returns target metadata for UI dropdowns. Can be a stub initially.
33846
+
33847
+ \`\`\`go
33848
+ // File: internal/app/<target>/get_target_configuration.go
33849
+
33850
+ type GetTargetConfigurationRequest struct {
33851
+ Connection *domain.Connection \`json:"connection"\`
33852
+ }
33853
+
33854
+ func (svc *WebhookServiceImpl) HandleGetTargetConfiguration(c echo.Context) error {
33855
+ zap.L().Info("getTargetConfiguration workflow activity started")
33856
+
33857
+ req := GetTargetConfigurationRequest{}
33858
+ if err := c.Bind(&req); err != nil {
33859
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
33860
+ }
33861
+
33862
+ // CUSTOMIZE: Return target configuration for UI dropdowns.
33863
+ // Fields returned here populate x-targetConfigurationRef dropdowns in the manifest.
33864
+ // Return an empty object if no dynamic configuration is needed.
33865
+ return c.JSON(http.StatusOK, map[string]interface{}{})
33866
+ }
33867
+ \`\`\`
33868
+
33600
33869
  ## Handler Pattern (Use for ALL Endpoints)
33601
33870
 
33602
33871
  Every handler follows these steps:
33603
33872
  1. Parse and validate the request
33604
33873
  2. Establish connection (with defer Close)
33605
33874
  3. Do the actual work
33606
- 4. Return response
33875
+ 4. Return response as JSON
33876
+
33877
+ ### Critical Response Rules
33878
+ - **testConnection**: Return \`c.JSON(http.StatusOK, TestConnectionResponse{Result: true})\`
33879
+ - **installCertificateBundle**: Return \`c.JSON(http.StatusOK, InstallCertificateBundleResponse{Keystore: ...})\` \u2014 MUST wrap in \`{"keystore": ...}\`
33880
+ - **configureInstallationEndpoint**: Return \`c.JSON(http.StatusOK, ConfigureInstallationEndpointResponse{Binding: ...})\` \u2014 MUST wrap in \`{"binding": ...}\`
33881
+ - **discoverCertificates**: Return \`c.JSON(http.StatusOK, DiscoverCertificatesResponse{Messages: ...})\` \u2014 MUST use \`"messages"\` key
33882
+ - **ALL responses must be JSON** \u2014 the vSatellite unmarshals every webhook response as JSON. Returning plain text (\`c.String(200, "OK")\`) causes: \`"invalid character 'O' looking for beginning of value"\`
33883
+ - Return HTTP 400 for ALL errors (not 500)
33607
33884
 
33608
- Key rules:
33885
+ ### Other Rules
33609
33886
  - Always defer Close() immediately after Connect()
33610
- - Return HTTP 400 for ALL errors (not 500)
33611
33887
  - Log every step with structured fields
33612
33888
  - Each endpoint is stateless \u2014 connect, do work, disconnect
33613
33889
  `;
@@ -33615,7 +33891,7 @@ Key rules:
33615
33891
  function getMachineBestPractices() {
33616
33892
  return `# Machine Connector Best Practices
33617
33893
 
33618
- These best practices are distilled from building multiple machine connectors: Splunk Enterprise (SSH), FortiGate (REST API), IBM APIC (REST API), and DataPower (REST API). They cover what worked, what failed, and what to avoid.
33894
+ These best practices are distilled from building multiple machine connectors across SSH-based and REST API-based targets. They cover what worked, what failed, and what to avoid.
33619
33895
 
33620
33896
  ${LESSONS_LEARNED}
33621
33897
 
@@ -33641,12 +33917,18 @@ ${LESSONS_LEARNED}
33641
33917
  18. **Retired certificates in Venafi silently block discovery** \u2014 query certificateStatus for RETIRED records when MI count is low
33642
33918
  19. **Null-safe JSON arrays** \u2014 initialize slices to \`[]string{}\` not nil, as \`null\` in JSON causes silent discovery failures
33643
33919
  20. **discoveryTypes label MUST use "title"** \u2014 \`x-labelLocalizationKey\` does NOT work for array discovery fields (shows raw key text). Use \`"title": "Certificate Types"\` on the property and \`"title"\` on each oneOf item. Boolean discovery fields CAN use \`x-labelLocalizationKey\` with the \`"discovery.xxx"\` pattern
33644
- 21. **Discovery type checkboxes should match the target platform's certificate categories** \u2014 ask "how would an admin describe their certificates?" (FortiGate: by service type; APIC: by usage role). Don't use generic/internal labels
33920
+ 21. **Discovery type checkboxes should match the target platform's certificate categories** \u2014 ask "how would an admin describe their certificates?" (network appliance: by service type; API platform: by usage role). Don't use generic/internal labels
33645
33921
  22. **x-labelLocalizationKey must be two-level dot paths** \u2014 \`"fieldName.label"\` works, but \`"section.fieldName.label"\` (three levels) does NOT resolve and shows raw key text in the UI. Use flat prefixes like \`"dpDomain.label"\` instead of \`"keystore.domain.label"\`
33646
33922
  23. **certificateBundle.certificateChain is an ARRAY** \u2014 Venafi Cloud sends it as \`["base64cert1", "base64cert2"]\`, not a single string. Manifest must declare \`{ "type": "array", "items": { "contentEncoding": "base64", "type": "string" } }\`. Go struct uses \`[][]byte\` (auto base64 decode) or \`[]string\` (manual decode)
33647
33923
  24. **Certificate chain ordering is undocumented** \u2014 Venafi Cloud sends the chain array but the ordering guarantee is not documented. Pass through in received order and note this assumption. Targets with strict ordering requirements may need issuer/subject-based re-sorting
33648
33924
  25. **REST API discovery: convert base64 DER to PEM** \u2014 when a target API returns certs as base64-encoded DER, you MUST convert to PEM for the discovery response (\`base64.Decode \u2192 pem.EncodeToMemory\`). Validate with \`strings.HasPrefix(cert, "-----BEGIN CERTIFICATE-----")\`
33649
33925
  26. **Default binding must be assigned BEFORE discovery type filtering** \u2014 if "unbound" (or any fallback binding) is assigned after the filter runs, unbound certs are silently dropped. Always: find bindings \u2192 assign default if empty \u2192 then filter
33926
+ 27. **Discovery messages MUST include an \`installations\` array** \u2014 each DiscoveredCertificate must have at least one entry with hostname, ipAddress, and port. Without this field, the platform silently discards the certificate \u2014 discovery shows "complete" but no certs appear in the UI. This is the most common cause of "discovery works but nothing shows up"
33927
+ 28. **Connection reset after cert import = SUCCESS** \u2014 some targets restart their web gateway or management interface after a certificate is installed, causing connection resets, timeouts, or HTTP errors. These MUST be caught and treated as success, not failure. Add a short retry/delay if you need to verify the install afterward
33928
+ 29. **Idempotent install detection** \u2014 HTTP 409 or response body containing "already exists" / "identical" during cert import should be treated as success, not error. The certificate was already installed (e.g., from a previous attempt that reported a connection error)
33929
+ 30. **Error messages must attribute the component** \u2014 prefix all errors so users can identify which system caused the failure: \`"Target API error: ..."\` (target returned an error), \`"Target connection error: ..."\` (cannot reach target), \`"Connector error: ..."\` (invalid request or internal processing error). This saves significant support/debugging time
33930
+ 31. **Binding is optional in the manifest** \u2014 if your target has no binding concept (single-cert targets), you can omit binding from the domainSchema. However, if binding was ever defined and machines exist with binding data, you MUST keep at least \`{"properties": {}, "type": "object"}\` or the Venafi UI will crash when editing those machines. \`x-primaryKey\` is optional on binding when it has no properties, but always required on keystore
33931
+ 32. **Dropdowns use \`oneOf\` with \`const\`, not \`enum\`** \u2014 the correct manifest pattern for dropdown fields is \`"oneOf": [{"const": "value", "x-labelLocalizationKey": "field.value"}]\`. Using \`enum\` alone does not render labels in the UI
33650
33932
  `;
33651
33933
  }
33652
33934
  function getRESTClientPattern(args) {
@@ -33666,7 +33948,7 @@ function getRESTClientPattern(args) {
33666
33948
  }
33667
33949
  return `# REST API Client Pattern for Machine Connectors
33668
33950
 
33669
- The REST API client pattern is for network appliance connectors (FortiGate, F5, Citrix ADC, PAN Panorama) that communicate via HTTPS REST API instead of SSH.
33951
+ The REST API client pattern is for network appliance connectors (firewalls, load balancers, ADCs, management platforms) that communicate via HTTPS REST API instead of SSH.
33670
33952
 
33671
33953
  ## When to Use This Pattern
33672
33954
  - Target is a network appliance (firewall, load balancer, ADC)
@@ -33681,7 +33963,7 @@ The REST API client pattern is for network appliance connectors (FortiGate, F5,
33681
33963
  - No sudo, no file system ops \u2014 everything is API calls
33682
33964
  - Each request creates a fresh handler (stateless per endpoint call)
33683
33965
 
33684
- ## Architecture: 3-Service Decomposition (PAN Panorama Pattern)
33966
+ ## Architecture: 3-Service Decomposition
33685
33967
 
33686
33968
  Instead of a single \`ClientServices\` interface, REST API connectors use three services:
33687
33969
  - **ConnectionService** \u2014 handles \`testConnection\`
@@ -33711,7 +33993,7 @@ ${appContent}
33711
33993
  ## Network Appliance Patterns
33712
33994
 
33713
33995
  ### Certificate Cannot Be Updated In-Place
33714
- Many appliances (FortiGate, PAN) cannot update a certificate's content in-place. The replacement pattern is:
33996
+ Many appliances cannot update a certificate's content in-place. The replacement pattern is:
33715
33997
  1. Upload new cert with a **different name** (e.g., \`name_YYMonDD_serial\`)
33716
33998
  2. Update all bindings to reference the new name
33717
33999
  3. Delete the old certificate (only if no bindings remain)
@@ -33741,10 +34023,8 @@ Changing the management interface certificate drops the active HTTPS session.
33741
34023
  Catch the connection error and treat it as success.
33742
34024
 
33743
34025
  ## Reference Connectors
33744
- - **PAN Panorama** \u2014 cleanest architecture, 3-service decomposition, config locking
33745
- - **Citrix ADC** \u2014 NITRO REST API, partition switching, SNI handling, comprehensive tests
33746
- - **F5** \u2014 simpler flat structure, go-bigip SDK, file upload pattern
33747
34026
  - **VMware AVI** (github.com/Venafi/vmware-avi-connector) \u2014 public REST API reference
34027
+ - Reference connectors using 3-service decomposition, partition switching, and SNI handling are available internally
33748
34028
  `;
33749
34029
  }
33750
34030
  function getSSHClientPattern(args) {
@@ -33857,7 +34137,7 @@ This means: no shared state between calls. Each call connects, does work, discon
33857
34137
  - \`get_machine_manifest\` \u2014 Get the machine connector manifest.json template with all sections explained
33858
34138
  - \`get_machine_domain_types\` \u2014 Get Go domain type templates (Connection, Keystore, Binding, CertificateBundle, Client)
33859
34139
  - \`get_machine_endpoints\` \u2014 Get handler/service interface templates for all 5 machine endpoints
33860
- - \`get_machine_best_practices\` \u2014 Get lessons learned and best practices from building the Splunk connector
34140
+ - \`get_machine_best_practices\` \u2014 Get lessons learned and best practices from building reference connectors
33861
34141
  - \`get_ssh_client_pattern\` \u2014 Get the SSH client abstraction code with sudo, file I/O, and command execution
33862
34142
 
33863
34143
  ## What I Need From You
@@ -33889,7 +34169,7 @@ server.resource("machine-blueprint", "venafi://connector-machine/blueprint", {
33889
34169
  ]
33890
34170
  }));
33891
34171
  server.resource("lessons-learned", "venafi://connector-machine/lessons-learned", {
33892
- description: "What worked, what failed, and mistakes to avoid \u2014 from building the Splunk SSH connector. Covers SSH patterns, discovery challenges, certificate format issues, and more.",
34172
+ description: "What worked, what failed, and mistakes to avoid \u2014 from building multiple machine connectors. Covers SSH and REST API patterns, discovery challenges, certificate format issues, and more.",
33893
34173
  mimeType: "text/markdown"
33894
34174
  }, async () => ({
33895
34175
  contents: [
@@ -33944,7 +34224,7 @@ server.tool("get_machine_endpoints", "Return handler and service interface templ
33944
34224
  }
33945
34225
  ]
33946
34226
  }));
33947
- server.tool("get_machine_best_practices", "Return lessons learned and best practices from building the Splunk SSH connector. Covers: what worked (Avi reference, logging, interfaces, DI), mistakes (DER vs PEM, key types, pagination, error handling), and things that need improvement (unit tests, integration tests, error context, timeouts).", {}, async () => ({
34227
+ server.tool("get_machine_best_practices", "Return lessons learned and best practices from building multiple machine connectors (SSH and REST API). Covers: what worked (Avi reference, logging, interfaces, DI), mistakes (DER vs PEM, key types, pagination, error handling, response format), and things that need improvement (unit tests, integration tests, error context, timeouts).", {}, async () => ({
33948
34228
  content: [
33949
34229
  {
33950
34230
  type: "text",
@@ -33964,7 +34244,7 @@ server.tool("get_ssh_client_pattern", "Return the SSH client abstraction code fo
33964
34244
  }
33965
34245
  ]
33966
34246
  }));
33967
- server.tool("get_rest_client_pattern", "Return the REST API client abstraction code for network appliance machine connectors (FortiGate, F5, Citrix ADC, PAN Panorama). Includes the Handler interface with multi-auth support (API token, username/password, PKI client cert), 3-service decomposition (ConnectionService, ProvisioningService, DiscoveryService), and uber/fx DI wiring. Use this instead of get_ssh_client_pattern when the target is a network appliance with a REST API.", {
34247
+ server.tool("get_rest_client_pattern", "Return the REST API client abstraction code for network appliance machine connectors (firewalls, load balancers, ADCs, management platforms). Includes the Handler interface with multi-auth support (API token, username/password, PKI client cert), 3-service decomposition (ConnectionService, ProvisioningService, DiscoveryService), and uber/fx DI wiring. Use this instead of get_ssh_client_pattern when the target is a network appliance with a REST API.", {
33968
34248
  connectorName: external_exports3.string().optional().describe("Connector name (e.g., 'fortigate-connector'). Replaces <CONNECTOR_NAME>."),
33969
34249
  modulePath: external_exports3.string().optional().describe("Go module path (e.g., 'github.com/venafi/fortigate-connector'). Replaces <CONNECTOR_MODULE>."),
33970
34250
  targetPackage: external_exports3.string().optional().describe("Target Go package name (e.g., 'fortigate'). Replaces <target>.")
@@ -33977,7 +34257,7 @@ server.tool("get_rest_client_pattern", "Return the REST API client abstraction c
33977
34257
  ]
33978
34258
  }));
33979
34259
  server.prompt("new_machine_connector", "Start building a new Venafi machine connector. Provides all context, tools, and instructions needed to begin building a machine connector for a specific target software.", {
33980
- targetSoftware: external_exports3.string().describe("The target software (e.g., 'Apache HTTP Server', 'Nginx', 'HAProxy', 'Splunk')"),
34260
+ targetSoftware: external_exports3.string().describe("The target software (e.g., 'Apache HTTP Server', 'Nginx', 'HAProxy', 'network appliance')"),
33981
34261
  connectionMethod: external_exports3.string().optional().default("ssh").describe("Connection method: 'ssh' or 'api'")
33982
34262
  }, async (args) => ({
33983
34263
  messages: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "venafi-connector-machine",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "MCP server providing machine connector-specific knowledge, templates, and tools for building Venafi machine connectors",
5
5
  "main": "bundle.mjs",
6
6
  "type": "module",