{
  "info": {
    "name": "myzrok-orchestration - Custom Domains (full)",
    "description": "Every custom-domain endpoint of myzrok-orchestration, plus the session, billing-account lookup and the zrok-orchestration stack checks used while testing.\n\nRun in order: Session > Login, Billing account > Find my billing account, then Create, poll Get, add DNS, Finalize once the certificate is ISSUED, poll until Created. Login sets the MYZROK_SESSION cookie (valid 20 minutes); Postman's cookie jar resends it. If a call returns 401, run Login again.\n\nEvery POST and DELETE carries X-Requested-With: myzrok-extension (the CSRF check).\n\nSet zrokPassword in your environment or the collection's Current value only - never commit it.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "variable": [
    {
      "key": "baseUrl",
      "value": "http://localhost:8106/myzrok-orchestration/v1"
    },
    {
      "key": "stackServiceUrl",
      "value": "http://zrok-orchestration.zrok.staging:8080"
    },
    {
      "key": "csrfHeader",
      "value": "myzrok-extension"
    },
    {
      "key": "zrokEmail",
      "value": "you@example.com"
    },
    {
      "key": "zrokPassword",
      "value": ""
    },
    {
      "key": "billingAccountId",
      "value": ""
    },
    {
      "key": "domainName",
      "value": "share.example.com"
    },
    {
      "key": "frontendName",
      "value": "acme"
    },
    {
      "key": "customDomainId",
      "value": ""
    },
    {
      "key": "stackId",
      "value": ""
    },
    {
      "key": "namespaceToken",
      "value": ""
    }
  ],
  "item": [
    {
      "name": "Session",
      "item": [
        {
          "name": "Login",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('logged in (200)', () => pm.response.to.have.status(200));",
                  "pm.test('session cookie set', () => pm.expect(pm.cookies.has('MYZROK_SESSION') || pm.response.headers.has('Set-Cookie')).to.be.true);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-Requested-With",
                "value": "{{csrfHeader}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"{{zrokEmail}}\",\n  \"password\": \"{{zrokPassword}}\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/session/login",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "session",
                "login"
              ]
            },
            "description": "Checks the zrok email and password against the zrok service in io.netfoundry.zrok.serviceUrl and sets the MYZROK_SESSION cookie (20 minutes)."
          }
        },
        {
          "name": "Logout",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-Requested-With",
                "value": "{{csrfHeader}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/session/logout",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "session",
                "logout"
              ]
            }
          }
        }
      ]
    },
    {
      "name": "Billing account",
      "item": [
        {
          "name": "Find my billing account",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200', () => pm.response.to.have.status(200));",
                  "// results are scoped to the logged-in zrok account; take the first billing account found",
                  "const body = pm.response.json();",
                  "const lists = Object.values((body && body._embedded) || {}).filter(Array.isArray);",
                  "const accounts = lists.length ? lists.flat() : (Array.isArray(body) ? body : []);",
                  "pm.test('has a billing account', () => pm.expect(accounts.length).to.be.above(0));",
                  "if (accounts.length) {",
                  "  pm.collectionVariables.set('billingAccountId', accounts[0].id);",
                  "  console.log('billingAccountId =', accounts[0].id, 'zrokId =', accounts[0].zrokId);",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/billing-accounts",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "billing-accounts"
              ]
            },
            "description": "Needs a session. Sets billingAccountId for the custom-domain requests. An empty list means this zrok account has no billing account yet, and custom-domain calls will return 403."
          }
        }
      ]
    },
    {
      "name": "Custom domains",
      "item": [
        {
          "name": "List custom domains",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200', () => pm.response.to.have.status(200));",
                  "pm.response.json().forEach(d => console.log(d.id, d.domainName, d.status, d.stack ? d.stack.certificateStatus : ''));"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/billing-accounts/{{billingAccountId}}/custom-domains?embedStack=true",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "billing-accounts",
                "{{billingAccountId}}",
                "custom-domains"
              ],
              "query": [
                {
                  "key": "embedStack",
                  "value": "true"
                }
              ]
            },
            "description": "Live custom domains of the billing account, oldest first."
          }
        },
        {
          "name": "Create custom domain",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('accepted (202)', () => pm.response.to.have.status(202));",
                  "const d = pm.response.json();",
                  "pm.test('starts Creating', () => pm.expect(d.status).to.eql('Creating'));",
                  "pm.collectionVariables.set('customDomainId', d.id);",
                  "pm.collectionVariables.set('stackId', d.stackId);",
                  "console.log('customDomainId =', d.id, 'stackId =', d.stackId, 'zrokVersion =', d.zrokVersion);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-Requested-With",
                "value": "{{csrfHeader}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"domainName\": \"{{domainName}}\",\n  \"frontendName\": \"{{frontendName}}\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/billing-accounts/{{billingAccountId}}/custom-domains",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "billing-accounts",
                "{{billingAccountId}}",
                "custom-domains"
              ]
            },
            "description": "Needs an active subscription whose plan allows another domain (409 otherwise). Returns 202 with status Creating; the frontend and stack are created in the background."
          }
        },
        {
          "name": "Get custom domain (poll this)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200', () => pm.response.to.have.status(200));",
                  "const d = pm.response.json();",
                  "if (d.stackId) pm.collectionVariables.set('stackId', d.stackId);",
                  "const cert = d.stack ? d.stack.certificateStatus : null;",
                  "const ns = d.frontend && d.frontend.namespaceToken;",
                  "if (ns) pm.collectionVariables.set('namespaceToken', ns);",
                  "const next = {",
                  "  'Creating': 'Creating the frontend and stack (about 5 minutes). Poll again.',",
                  "  'Pending Validation': cert === 'ISSUED' ? 'Certificate ISSUED: run Finalize.' : 'Add the CNAME and wildcard A records below, then poll (certificate: ' + cert + ').',",
                  "  'Updating': 'Finalizing. Poll again.',",
                  "  'Created': 'Live. Share with: zrok2 share public http://localhost:8080 -n ' + ns,",
                  "  'Reissuing Certificate': 'Reissuing. Poll again.',",
                  "  'Deleting': 'Being removed. Poll until 404.'",
                  "}[d.status] || 'Error status: see the orchestration log; Recreate stack, Reissue, or Delete.';",
                  "console.log(d.status, '-', next);",
                  "if (d.cnameName) console.log('CNAME', d.cnameName, '->', d.cnameValue);",
                  "if (d.acceleratorIps) console.log('A *.' + d.domainName, '->', d.acceleratorIps.join(', '));"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/billing-accounts/{{billingAccountId}}/custom-domains/{{customDomainId}}?embedStack=true",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "billing-accounts",
                "{{billingAccountId}}",
                "custom-domains",
                "{{customDomainId}}"
              ],
              "query": [
                {
                  "key": "embedStack",
                  "value": "true"
                }
              ]
            },
            "description": "Current state with the live stack. Status flow: Creating > Pending Validation > (Finalize) Updating > Created. The console shows the next step."
          }
        },
        {
          "name": "Check CAA records",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/billing-accounts/{{billingAccountId}}/custom-domains/{{customDomainId}}/has-caa-records",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "billing-accounts",
                "{{billingAccountId}}",
                "custom-domains",
                "{{customDomainId}}",
                "has-caa-records"
              ]
            },
            "description": "Whether a CAA record could stop the certificate being issued."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200', () => pm.response.to.have.status(200));",
                  "console.log(JSON.stringify(pm.response.json()));"
                ]
              }
            }
          ]
        },
        {
          "name": "Finalize (after certificate ISSUED)",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "X-Requested-With",
                "value": "{{csrfHeader}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/billing-accounts/{{billingAccountId}}/custom-domains/{{customDomainId}}/stack",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "billing-accounts",
                "{{billingAccountId}}",
                "custom-domains",
                "{{customDomainId}}",
                "stack"
              ]
            },
            "description": "Hands the frontend identity to the stack, which deploys it: Updating, then Created. myzrok's Finalize button."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('accepted (202)', () => pm.response.to.have.status(202));",
                  "console.log('status', pm.response.json().status, '- poll Get until Created');"
                ]
              }
            }
          ]
        },
        {
          "name": "Recreate missing stack",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-Requested-With",
                "value": "{{csrfHeader}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/billing-accounts/{{billingAccountId}}/custom-domains/{{customDomainId}}/stack",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "billing-accounts",
                "{{billingAccountId}}",
                "custom-domains",
                "{{customDomainId}}",
                "stack"
              ]
            },
            "description": "After a Create Error: creates the stack if zrok-orchestration has none."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('accepted (202)', () => pm.response.to.have.status(202));"
                ]
              }
            }
          ]
        },
        {
          "name": "Reissue certificate",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('accepted (202)', () => pm.response.to.have.status(202));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-Requested-With",
                "value": "{{csrfHeader}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/billing-accounts/{{billingAccountId}}/custom-domains/{{customDomainId}}/reissue-cert",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "billing-accounts",
                "{{billingAccountId}}",
                "custom-domains",
                "{{customDomainId}}",
                "reissue-cert"
              ]
            },
            "description": "Reissuing Certificate, then Pending Validation with a new validation window. Use when the certificate FAILED or timed out."
          }
        },
        {
          "name": "Delete custom domain",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('accepted (202)', () => pm.response.to.have.status(202));",
                  "console.log('status', pm.response.json().status, '- poll Get until it returns 404');"
                ]
              }
            }
          ],
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "X-Requested-With",
                "value": "{{csrfHeader}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/billing-accounts/{{billingAccountId}}/custom-domains/{{customDomainId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "billing-accounts",
                "{{billingAccountId}}",
                "custom-domains",
                "{{customDomainId}}"
              ]
            },
            "description": "Deleting, then Deleted once the stack, grant, namespace, frontend and identity are gone. Remove your DNS records afterwards."
          }
        }
      ]
    },
    {
      "name": "Error cases",
      "item": [
        {
          "name": "Create - invalid domain name (400)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('400', () => pm.response.to.have.status(400));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-Requested-With",
                "value": "{{csrfHeader}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"domainName\": \"not a domain\",\n  \"frontendName\": \"-bad-\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/billing-accounts/{{billingAccountId}}/custom-domains",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "billing-accounts",
                "{{billingAccountId}}",
                "custom-domains"
              ]
            }
          }
        },
        {
          "name": "Create - duplicate domain name (409)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('409', () => pm.response.to.have.status(409));",
                  "console.log(pm.response.json().detail);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-Requested-With",
                "value": "{{csrfHeader}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"domainName\": \"{{domainName}}\",\n  \"frontendName\": \"{{frontendName}}\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/billing-accounts/{{billingAccountId}}/custom-domains",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "billing-accounts",
                "{{billingAccountId}}",
                "custom-domains"
              ]
            },
            "description": "Run after Create while that domain is live. Also 409: no active subscription, or plan limit reached (see detail)."
          }
        },
        {
          "name": "Get - not found (404)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('404', () => pm.response.to.have.status(404));"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/billing-accounts/{{billingAccountId}}/custom-domains/00000000-0000-0000-0000-000000000000",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "billing-accounts",
                "{{billingAccountId}}",
                "custom-domains",
                "00000000-0000-0000-0000-000000000000"
              ]
            }
          }
        },
        {
          "name": "Create - missing CSRF header (403)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('403', () => pm.response.to.have.status(403));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"domainName\": \"csrf-check.example.com\",\n  \"frontendName\": \"csrf\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/billing-accounts/{{billingAccountId}}/custom-domains",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "billing-accounts",
                "{{billingAccountId}}",
                "custom-domains"
              ]
            },
            "description": "Fails as expected only with the real ZrokAuthenticationInterceptor (not with the local testing bypass)."
          }
        },
        {
          "name": "List - no session (401)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('401', () => pm.response.to.have.status(401));"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Cookie",
                "value": "MYZROK_SESSION=invalid"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/billing-accounts/{{billingAccountId}}/custom-domains",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "billing-accounts",
                "{{billingAccountId}}",
                "custom-domains"
              ]
            },
            "description": "Fails as expected only with the real ZrokAuthenticationInterceptor (not with the local testing bypass)."
          }
        }
      ]
    },
    {
      "name": "zrok-orchestration stack service (staging, internal network)",
      "description": "Direct calls to the Go stack service, for checking what myzrok-orchestration sees. Reachable only over the internal network (Ziti Desktop Edge). The staging build accepts exactly id and domainName on GET/DELETE - add nothing else.",
      "item": [
        {
          "name": "Get stack",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const s = pm.response.json();",
                  "console.log('stackStatus', s.stackStatus, 'certificateStatus', s.certificateStatus, 'acceleratorIps', JSON.stringify(s.acceleratorIps));"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{stackServiceUrl}}/stacks?id={{stackId}}&domainName={{domainName}}",
              "host": [
                "{{stackServiceUrl}}"
              ],
              "path": [
                "stacks"
              ],
              "query": [
                {
                  "key": "id",
                  "value": "{{stackId}}"
                },
                {
                  "key": "domainName",
                  "value": "{{domainName}}"
                }
              ]
            },
            "description": "Read-only, straight to the staging stack service. The stack id is the domain's stackId (not its id). myzrok-orchestration already returns this as `stack` with embedStack=true; use this only to debug."
          }
        },
        {
          "name": "List stacks",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{stackServiceUrl}}/stacks",
              "host": [
                "{{stackServiceUrl}}"
              ],
              "path": [
                "stacks"
              ]
            },
            "description": "Read-only. All stacks known to the service."
          }
        }
      ]
    }
  ]
}
