{
  "openapi": "3.1.0",
  "info": {
    "title": "Morten Nordbye — portfolio API",
    "version": "v1",
    "description": "The site serves its own content over a small JSON API. Reads are public and need no key, responses carry permissive CORS headers so they work from a browser, and every route returns the same error envelope. It runs in the same Node process as the pages you are reading, on the cluster described on the infrastructure page.",
    "contact": {
      "name": "Morten Nordbye",
      "url": "https://nordbye.it/api"
    },
    "license": {
      "name": "Apache-2.0",
      "identifier": "Apache-2.0"
    }
  },
  "servers": [
    {
      "url": "https://nordbye.it",
      "description": "Production."
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Static API key presented as a Bearer token, or via the x-api-key header."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "status": {
                "type": "integer"
              },
              "message": {
                "type": "string"
              }
            },
            "required": [
              "status",
              "message"
            ]
          }
        },
        "required": [
          "error"
        ]
      }
    }
  },
  "paths": {
    "/api/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Liveness probe.",
        "description": "Unversioned and always dynamic, so it reflects the running process rather than a build-time cache. This is the target Kubernetes uses for its probes.",
        "security": [],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ok"
                      ]
                    }
                  },
                  "required": [
                    "status"
                  ]
                },
                "example": {
                  "status": "ok"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1": {
      "get": {
        "operationId": "getIndex",
        "summary": "Discovery index.",
        "description": "The front door: a machine-readable map of every endpoint, generated from the same source as this page. Static, because the map only changes when the code does.",
        "security": [],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "version": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string",
                      "format": "uri"
                    },
                    "openapi": {
                      "type": "string",
                      "format": "uri"
                    },
                    "endpoints": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "method": {
                            "type": "string",
                            "enum": [
                              "GET",
                              "POST"
                            ]
                          },
                          "path": {
                            "type": "string"
                          },
                          "desc": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "method",
                          "path",
                          "desc"
                        ]
                      }
                    },
                    "notes": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "name",
                    "version",
                    "docs",
                    "openapi",
                    "endpoints",
                    "notes"
                  ]
                },
                "example": {
                  "name": "Morten Nordbye — portfolio API",
                  "version": "v1",
                  "docs": "https://nordbye.it/api",
                  "openapi": "https://nordbye.it/api/v1/openapi.json",
                  "endpoints": [
                    {
                      "method": "GET",
                      "path": "/api/health",
                      "desc": "Liveness probe."
                    }
                  ],
                  "notes": "Reads are public. Writes require an API key."
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/openapi.json": {
      "get": {
        "operationId": "getOpenapi",
        "summary": "OpenAPI 3.1 document.",
        "description": "The whole surface as a spec you can import into Postman, Bruno, Swagger UI or a client generator. Generated from the same endpoint definitions that render this page, so it cannot drift from the routes it describes.",
        "security": [],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "openapi": {
                      "type": "string",
                      "const": "3.1.0"
                    },
                    "info": {
                      "type": "object"
                    },
                    "servers": {
                      "type": "array"
                    },
                    "components": {
                      "type": "object"
                    },
                    "paths": {
                      "type": "object"
                    }
                  },
                  "required": [
                    "openapi",
                    "info",
                    "paths"
                  ],
                  "additionalProperties": true
                },
                "example": {
                  "openapi": "3.1.0",
                  "info": {
                    "title": "Morten Nordbye — portfolio API",
                    "version": "v1"
                  },
                  "servers": [
                    {
                      "url": "https://nordbye.it"
                    }
                  ],
                  "paths": {
                    "/api/health": {
                      "get": {
                        "operationId": "getHealth"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/profile": {
      "get": {
        "operationId": "getProfile",
        "summary": "Identity, certifications, skills and socials.",
        "description": "Served straight from the content modules that render the site itself, so the API and the pages can never disagree. Refreshed on each deploy.",
        "security": [],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "role": {
                      "type": "string"
                    },
                    "location": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "summary": {
                      "type": "string"
                    },
                    "socials": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "label": {
                            "type": "string"
                          },
                          "href": {
                            "type": "string",
                            "format": "uri"
                          }
                        },
                        "required": [
                          "label",
                          "href"
                        ]
                      }
                    },
                    "certifications": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "title": {
                            "type": "string"
                          },
                          "issuer": {
                            "type": "string"
                          },
                          "date": {
                            "type": "string"
                          },
                          "credentialId": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "title",
                          "issuer",
                          "date"
                        ]
                      }
                    },
                    "skills": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "label": {
                            "type": "string"
                          },
                          "level": {
                            "type": "integer",
                            "minimum": 0,
                            "maximum": 100
                          },
                          "group": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "label",
                          "level",
                          "group"
                        ]
                      }
                    }
                  },
                  "required": [
                    "name",
                    "role",
                    "location",
                    "url",
                    "summary",
                    "socials",
                    "certifications",
                    "skills"
                  ]
                },
                "example": {
                  "name": "Morten Nordbye",
                  "role": "Cloud Engineer & Architect",
                  "location": "Oslo, Norway",
                  "url": "https://nordbye.it",
                  "summary": "Cloud engineer working on automated, secure infrastructure…",
                  "socials": [
                    {
                      "label": "GitHub",
                      "href": "https://github.com/mortennordbye"
                    }
                  ],
                  "certifications": [
                    {
                      "title": "CKA: Certified Kubernetes Administrator",
                      "issuer": "The Linux Foundation",
                      "date": "Jan 2024"
                    }
                  ],
                  "skills": [
                    {
                      "label": "Kubernetes",
                      "level": 80,
                      "group": "platform"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/infra": {
      "get": {
        "operationId": "getInfra",
        "summary": "Live Talos cluster status and 30-day history.",
        "description": "A CronJob in the cluster writes a status ConfigMap every five minutes; it is mounted into the pod and read fresh per request. When the file is not mounted the route falls back to a baked snapshot and marks it with a source field. This is what drives the infrastructure page.",
        "security": [],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "generatedAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "build": {
                      "type": "string"
                    },
                    "deployedAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "argocd": {
                      "type": "object",
                      "properties": {
                        "sync": {
                          "type": "string"
                        },
                        "health": {
                          "type": "string"
                        },
                        "syncedAt": {
                          "type": "string",
                          "format": "date-time"
                        }
                      },
                      "required": [
                        "sync",
                        "health"
                      ]
                    },
                    "nodes": {
                      "type": "object",
                      "properties": {
                        "ready": {
                          "type": "integer"
                        },
                        "total": {
                          "type": "integer"
                        }
                      },
                      "required": [
                        "ready",
                        "total"
                      ]
                    },
                    "versions": {
                      "type": "object",
                      "properties": {
                        "kubernetes": {
                          "type": "string"
                        },
                        "talos": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "kubernetes",
                        "talos"
                      ]
                    },
                    "cert": {
                      "type": "object",
                      "properties": {
                        "notAfter": {
                          "type": "string",
                          "format": "date-time"
                        }
                      },
                      "required": [
                        "notAfter"
                      ]
                    },
                    "history": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "d": {
                            "type": "string",
                            "format": "date"
                          },
                          "ok": {
                            "type": "integer"
                          },
                          "total": {
                            "type": "integer"
                          }
                        },
                        "required": [
                          "d",
                          "ok",
                          "total"
                        ]
                      }
                    },
                    "source": {
                      "type": "string",
                      "enum": [
                        "snapshot"
                      ]
                    }
                  },
                  "required": [
                    "argocd",
                    "nodes"
                  ],
                  "additionalProperties": true
                },
                "example": {
                  "generatedAt": "2026-07-19T08:05:03Z",
                  "build": "0.0.80",
                  "argocd": {
                    "sync": "Synced",
                    "health": "Healthy"
                  },
                  "nodes": {
                    "ready": 6,
                    "total": 6
                  },
                  "versions": {
                    "kubernetes": "v1.34.0",
                    "talos": "v1.11.6"
                  },
                  "cert": {
                    "notAfter": "2026-09-25T11:41:28Z"
                  },
                  "history": [
                    {
                      "d": "2026-07-16",
                      "ok": 286,
                      "total": 288
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/blog": {
      "get": {
        "operationId": "getBlog",
        "summary": "Latest five posts from blog.nordbye.it.",
        "description": "Parses the Hugo RSS feed and revalidates hourly, which keeps the blog origin from being hit once per request. Returns 502 with the standard error envelope when the feed is unreachable.",
        "security": [],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "source": {
                      "type": "string",
                      "format": "uri"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "posts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "title": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string",
                            "format": "uri"
                          },
                          "publishedAt": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "published": {
                            "type": [
                              "string",
                              "null"
                            ]
                          }
                        },
                        "required": [
                          "title",
                          "url",
                          "publishedAt",
                          "published"
                        ]
                      }
                    }
                  },
                  "required": [
                    "source",
                    "count",
                    "posts"
                  ]
                },
                "example": {
                  "source": "https://blog.nordbye.it/index.xml",
                  "count": 5,
                  "posts": [
                    {
                      "title": "Tuning Azure WAF Without Paying Log Analytics Prices",
                      "url": "https://blog.nordbye.it/blog/lawless-waf/",
                      "publishedAt": "2026-07-16T00:00:00.000Z",
                      "published": "Thu, 16 Jul 2026 00:00:00 +0000"
                    }
                  ]
                }
              }
            }
          },
          "502": {
            "description": "The blog feed could not be reached, or answered with a non-2xx status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 502,
                    "message": "blog feed unreachable"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/echo": {
      "post": {
        "operationId": "postEcho",
        "summary": "Authenticated write example.",
        "description": "A reference write that proves the authenticated POST seam without persisting anything. The key is compared in constant time, and writes are refused outright when no key is configured on the server. Real stateful writes build on this pattern and add a datastore.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "echo": {}
                  },
                  "required": [
                    "ok",
                    "echo"
                  ]
                },
                "example": {
                  "ok": true,
                  "echo": {
                    "hello": "world"
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request body was present but not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "message": "body must be valid JSON"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 401,
                    "message": "missing or invalid API key"
                  }
                }
              }
            }
          }
        },
        "requestBody": {
          "required": false,
          "description": "Any JSON value. Returned verbatim under the echo key. An empty body echoes null.",
          "content": {
            "application/json": {
              "schema": {},
              "example": {
                "hello": "world"
              }
            }
          }
        }
      }
    }
  }
}
