openapi: "3.1.0"
info:
  title: Digital Product Passport API
  version: "1.0.0"
  description: |
    Developer-first EU Digital Product Passport API — issue a battery passport,
    register it with the EU DPP Registry, and resolve it at a public URL behind
    a QR / NFC data carrier with tiered access control.

    **Three access tiers (the hard part, done right):**
    | Tier | Who | Credential |
    |---|---|---|
    | `public` | Anyone (no auth) | None |
    | `legitimate-interest` | Suppliers, recyclers, repairers, notified bodies | Bearer key (scope: `legitimate-interest`) |
    | `competent-authority` | National authorities, the Commission | Bearer key (scope: `competent-authority`) |

    Default-deny: attributes without an explicit tier assignment are treated as `competent-authority`.

    **Regulations:** Battery Reg (EU) 2023/1542 Art. 77 + Annex XIII;
    ESPR (EU) 2024/1781; DPP Registry Impl. Reg (EU) 2026/1778.

    > Not legal advice. Not the official EU DPP Registry.
  contact:
    name: Allan Niñal
    url: https://www.allanninal.dev/
  license:
    name: MIT
    url: https://opensource.org/licenses/MIT

servers:
  - url: https://apis.allanninal.dev/dpp
    description: Production (self-hosted)
  - url: http://localhost:8794
    description: Local development

tags:
  - name: passports
    description: Create and manage Digital Product Passports
  - name: resolver
    description: Resolve a passport by UID (public URL behind QR / NFC carrier)
  - name: reference
    description: Static reference data (categories, data models, access tiers, carriers)

paths:

  # ── Compute ──────────────────────────────────────────────────────────────────

  /v1/passports:
    post:
      operationId: createPassport
      summary: Create a passport
      description: |
        Validate attributes against the Annex XIII data model, mint an ISO/IEC 15459
        UID, generate a data carrier (QR / Data Matrix / NFC), and return a
        resolvable URL. Anonymous — no API key required.
      tags: [passports]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateRequest"
            example:
              category: battery-ev
              carrier: qr
              attributes:
                manufacturerName: "ACME Cells GmbH"
                manufacturerAddress: "Erfurt, Germany"
                batteryModel: "ACME-EV-75"
                batteryCategory: "EV"
                batteryStatus: "original"
                manufacturingDate: "2027-03-01"
                manufacturingPlace: "Erfurt, DE"
                ratedCapacityAh: 210
                ratedCapacityKwh: 75.0
                nominalVoltage: 400
                chemistry: "NMC811"
                stateOfHealth: 100
      responses:
        "201":
          description: Passport created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Passport"
        "400":
          $ref: "#/components/responses/BadRequest"
        "429":
          $ref: "#/components/responses/RateLimit"

    get:
      operationId: listPassports
      summary: List your passports
      description: List all passports issued under the authenticated key (paginated).
      tags: [passports]
      security:
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          schema: { type: integer, default: 20, minimum: 1, maximum: 100 }
        - name: offset
          in: query
          schema: { type: integer, default: 0, minimum: 0 }
      responses:
        "200":
          description: Paginated list
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:  { type: array, items: { $ref: "#/components/schemas/PassportSummary" } }
                  total:  { type: integer }
                  limit:  { type: integer }
                  offset: { type: integer }
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/passports/{passportId}:
    get:
      operationId: getPassport
      summary: Fetch your passport (full, all tiers)
      description: Returns the full passport including all attributes regardless of tier. Owner only.
      tags: [passports]
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/passportId"
      responses:
        "200":
          description: Full passport
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Passport"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

    patch:
      operationId: updatePassport
      summary: Update draft / create versioned amendment
      description: |
        While `draft` or `issued`: update attributes in-place.
        Once `registered`: creates a new versioned amendment preserving history.
      tags: [passports]
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/passportId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [attributes]
              properties:
                attributes:
                  type: object
                  additionalProperties: true
      responses:
        "200":
          description: Updated passport
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Passport"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/passports/{passportId}/register:
    post:
      operationId: registerPassport
      summary: Register with EU DPP Registry
      description: |
        Submit the passport's UID + mandatory registration data + high-level metadata
        to the EU DPP Registry (test env by default). Idempotent — re-submitting
        the same passport to the same environment returns the existing record.
        Registry errors are returned verbatim in `echo`.
      tags: [passports]
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/passportId"
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RegisterRequest"
      responses:
        "200":
          description: Registration record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RegistrationRecord"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/passports/{passportId}/carrier:
    get:
      operationId: getCarrier
      summary: Fetch data carrier (QR / Data Matrix / NFC)
      tags: [passports]
      parameters:
        - $ref: "#/components/parameters/passportId"
        - name: type
          in: query
          schema:
            type: string
            enum: [qr, datamatrix, nfc]
            default: qr
      responses:
        "200":
          description: Data carrier
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataCarrier"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/dpp/{uid}:
    get:
      operationId: resolvePassportJson
      summary: Resolve passport (JSON, tier-filtered)
      description: |
        Resolves a passport by UID and returns the attributes permitted for the
        caller's access tier. Public (no auth) → `public` attributes only.
        `legitimate-interest` key → additionally `legitimate-interest` attributes.
        `competent-authority` key → all attributes. Default-deny for untiered attributes.
      tags: [resolver]
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/uid"
      responses:
        "200":
          description: Tier-filtered passport
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ResolveResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimit"

  /dpp/{uid}:
    get:
      operationId: resolvePassportHtml
      summary: Resolve passport (HTML view, tier-filtered)
      description: Human-readable HTML passport view. Same tier logic as the JSON resolver.
      tags: [resolver]
      security:
        - {}
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/uid"
      responses:
        "200":
          description: HTML passport view
          content:
            text/html:
              schema: { type: string }
        "404":
          description: Passport not found

  /v1/usage:
    get:
      operationId: getUsage
      summary: API key usage and quota
      tags: [passports]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Usage stats
          content:
            application/json:
              schema:
                type: object
                properties:
                  tier:     { type: string }
                  scope:    { type: string }
                  quota:    { type: [integer, "null"] }
                  issued:   { type: integer }
                  resolved: { type: integer }
                  total:    { type: integer }

  /healthz:
    get:
      operationId: healthz
      summary: Service health
      tags: [reference]
      responses:
        "200":
          description: Healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:       { type: boolean }
                  datasets: { type: object }
                  registry: { type: object }
                  ts:       { type: string, format: date-time }

  # ── Static reference ─────────────────────────────────────────────────────────

  /api/v1/index.json:
    get:
      operationId: getIndex
      summary: Endpoint catalogue
      tags: [reference]
      responses:
        "200":
          description: Catalogue JSON
          content:
            application/json:
              schema: { type: object }

  /api/v1/meta.json:
    get:
      operationId: getMeta
      summary: Regulation metadata, key dates, registry environment
      tags: [reference]
      responses:
        "200":
          description: Meta JSON
          content:
            application/json:
              schema: { type: object }

  /api/v1/categories.json:
    get:
      operationId: getCategories
      summary: Supported product categories
      tags: [reference]
      responses:
        "200":
          description: Categories JSON
          content:
            application/json:
              schema: { type: object }

  /api/v1/data-models/{category}.json:
    get:
      operationId: getDataModel
      summary: Versioned attribute schema for a category (includes accessTier per attribute)
      tags: [reference]
      parameters:
        - name: category
          in: path
          required: true
          schema:
            type: string
            enum: [battery-ev, battery-lmt, battery-industrial]
      responses:
        "200":
          description: Data model JSON
          content:
            application/json:
              schema: { type: object }
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/access-tiers.json:
    get:
      operationId: getAccessTiers
      summary: Access tier definitions and group→tier map
      tags: [reference]
      responses:
        "200":
          description: Access tiers JSON
          content:
            application/json:
              schema: { type: object }

  /api/v1/carriers.json:
    get:
      operationId: getCarriers
      summary: Supported carrier types and identifier scheme standards
      tags: [reference]
      responses:
        "200":
          description: Carriers JSON
          content:
            application/json:
              schema: { type: object }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        API key issued by `seed-key.mjs`. Three scopes:
        - `issuer` — create/manage/register passports
        - `legitimate-interest` — resolve at the legitimate-interest tier
        - `competent-authority` — resolve at the competent-authority tier (all attributes)

  parameters:
    passportId:
      name: passportId
      in: path
      required: true
      schema: { type: string }
      description: Internal passport UUID
    uid:
      name: uid
      in: path
      required: true
      schema: { type: string }
      description: ISO/IEC 15459 unique identifier (URL-encoded)

  responses:
    BadRequest:
      description: Bad request — field-level validation error
      content:
        application/json:
          schema:
            type: object
            properties:
              error:      { type: string }
              validation: { $ref: "#/components/schemas/ValidationReport" }
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            type: object
            properties:
              error: { type: string }
    NotFound:
      description: Not found
      content:
        application/json:
          schema:
            type: object
            properties:
              error: { type: string }
    RateLimit:
      description: Rate limit exceeded
      headers:
        Retry-After:
          schema: { type: integer }
        RateLimit-Limit:
          schema: { type: integer }
        RateLimit-Remaining:
          schema: { type: integer }
        RateLimit-Reset:
          schema: { type: integer }
      content:
        application/json:
          schema:
            type: object
            properties:
              error:              { type: string }
              retryAfterSeconds:  { type: integer }

  schemas:
    CreateRequest:
      type: object
      required: [category, attributes]
      properties:
        category:
          type: string
          enum: [battery-ev, battery-lmt, battery-industrial]
        dataModelVersion:
          type: string
        carrier:
          type: string
          enum: [qr, datamatrix, nfc]
          default: qr
        attributes:
          type: object
          additionalProperties: true

    Passport:
      type: object
      properties:
        passportId:        { type: string }
        uid:               { type: string }
        category:          { type: string }
        dataModelVersion:  { type: string }
        status:
          type: string
          enum: [draft, issued, registered, amended]
        version:           { type: integer }
        dataCarrier:       { $ref: "#/components/schemas/DataCarrier" }
        resolveUrl:        { type: string, format: uri }
        validation:        { $ref: "#/components/schemas/ValidationReport" }
        attributes:
          type: object
          additionalProperties: true
        registrationId:    { type: string }
        registryEnvironment:
          type: string
          enum: [test, production]
        createdAt:         { type: string, format: date-time }
        updatedAt:         { type: string, format: date-time }
        datasetVersions:
          type: object
          properties:
            dataModel:       { type: string }
            accessTiers:     { type: string }
            registryMapping: { type: string }

    PassportSummary:
      type: object
      properties:
        passport_id:  { type: string }
        uid:          { type: string }
        category:     { type: string }
        status:       { type: string }
        version:      { type: integer }
        created_at:   { type: string }
        updated_at:   { type: string }

    DataCarrier:
      type: object
      description: |
        A scannable carrier for the resolve URL. QR symbols are ISO/IEC 18004
        byte mode at error-correction level M; Data Matrix symbols are ISO/IEC
        16022 ECC 200 with ASCII encodation. Both are deterministic: the same
        passport and type always return a byte-identical symbol.
      properties:
        type:     { type: string, enum: [qr, datamatrix, nfc] }
        standard: { type: string, examples: ["ISO/IEC 18004"] }
        payload:  { type: string, description: "The resolve URL encoded in the symbol." }
        svg:      { type: string, description: "SVG rendering (qr and datamatrix only)" }
        symbol:
          type: object
          description: "Symbol parameters actually used (qr and datamatrix only)."
          properties:
            version:         { type: integer, description: "QR version 1-10." }
            size:            { type: integer, description: "Symbol side length in modules." }
            mask:            { type: integer, description: "QR mask pattern 0-7, chosen by the standard penalty rules." }
            errorCorrection: { type: string, examples: ["M"] }
            mode:            { type: string, examples: ["byte"] }
            scheme:          { type: string, examples: ["ECC 200"] }
            encodation:      { type: string, examples: ["ASCII"] }
            dataCodewords:   { type: integer }
            ecCodewords:     { type: integer }
        ndef:
          type: object
          description: "NDEF URI record descriptor (nfc only)."
          properties:
            recordType:        { type: string, examples: ["RTD_URI"] }
            identifierCode:    { type: string, examples: ["0x04"] }
            identifierMeaning: { type: string, examples: ["https://"] }
            urlEncoded:        { type: string }
            ndefHex:           { type: string, description: "The complete NDEF record as hex — write this to the tag." }
            byteLength:        { type: integer }

    RegisterRequest:
      type: object
      properties:
        registryEnv:
          type: string
          enum: [test, production]
          default: test
        registrationData:
          type: object
          additionalProperties: true

    RegistrationRecord:
      type: object
      properties:
        registrationId:       { type: string }
        passportId:           { type: string }
        uid:                  { type: string }
        registryEnvironment:  { type: string, enum: [test, production] }
        status:               { type: string, enum: [registered, failed, pending] }
        submittedAt:          { type: string, format: date-time }
        registrySchemaVersion:{ type: string }
        idempotent:           { type: boolean }
        echo:
          type: object
          additionalProperties: true

    ResolveResponse:
      type: object
      properties:
        uid:           { type: string }
        category:      { type: string }
        status:        { type: string }
        accessTier:
          type: string
          enum: [public, legitimate-interest, competent-authority, owner]
        attributes:
          type: object
          additionalProperties: true
        withheldGroups:
          type: array
          items: { type: string }
        datasetVersions:
          type: object
        resolvedAt:    { type: string, format: date-time }

    ValidationReport:
      type: object
      properties:
        valid:    { type: boolean }
        missing:  { type: array, items: { type: string } }
        invalid:
          type: array
          items:
            type: object
            properties:
              id:     { type: string }
              reason: { type: string }
        warnings: { type: array, items: { type: string } }
