openapi: 3.1.0
info:
  title: MRS API
  version: 0.1.0-draft
  description: |
    Metarecord Service (MRS): org-scoped opaque object store.

    Contract notes:
    - API Gateway base path: `/mrs`
    - All tenant endpoints require valid auth via either:
      - `x-session-guid` (human USM session), OR
      - `x-api-key` (USM service-account API key).
      Bearer tokens are not accepted via query string.
    - Optional cost attribution/scope: `cccode` may be provided as a request field (or query param) and/or header `x-cccode`. Format `^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$`, canonicalized to uppercase; invalid values (or body/header mismatch) return HTTP 400.
    - Org association enforced (owner OR active member). Non-associated callers receive `404 not-found` (anti-enumeration).
    - Optimistic concurrency: state-changing operations on an existing record require `expected_revision`.
      Missing → HTTP 428 `expected-revision-required`; mismatch → HTTP 409 `conflict`.
    - Inline JSON only when `content_type=application/json` and payload ≤256 KB (no gzip).
    - Presigned uploads are required for >256 KB or non-JSON; gzip is mandatory; hard cap 128 MB.
      `content_md5` values are the **hex** MD5 of the gzip payload (required on presign + complete).
  license:
    name: Proprietary
    url: https://g3nretailstack.com/license
servers:
  - url: https://api.g3nretailstack.com/mrs
security:
  - sessionGuid: []
  - apiKey: []
components:
  securitySchemes:
    sessionGuid:
      type: apiKey
      in: header
      name: x-session-guid
    apiKey:
      type: apiKey
      in: header
      name: x-api-key
  schemas:
    BuildMeta:
      type: object
      properties:
        build_major: { type: string }
        build_minor: { type: string }
        build_id: { type: string }
      required: [build_major, build_minor, build_id]
    MrsContainer:
      type: string
      description: |
        Container name (normalized to lowercase). Used in DynamoDB key segments and S3 prefixes.
        Must match `^[a-z][a-z0-9_-]{1,79}$` (2..80 chars).
      pattern: '^[a-z][a-z0-9_-]{1,79}$'
      minLength: 2
      maxLength: 80
      examples: [contract, pvm_comment_attachment_v1]
    CcCode:
      type: string
      description: Cost centre code (canonical uppercase).
      pattern: '^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'
      examples: ['ABCD-EFGH-IJKL']
    Stats:
      type: object
      properties:
        call: { type: string }
        service: { type: string, enum: [mrs] }
        timestamp_utc: { type: string, format: date-time }
        request_id: { type: string }
        build: { $ref: '#/components/schemas/BuildMeta' }
        latency_ms: { type: number }
        bandwidth_in_bytes: { type: number }
        bandwidth_out_bytes: { type: number }
        actor: { type: string }
        orgcode: { type: string }
        cccode: { $ref: '#/components/schemas/CcCode' }
        other_service_calls: { type: number }
      required: [call, service, timestamp_utc, build]
    ErrorMessage:
      type: object
      properties:
        en_US: { type: string }
      required: [en_US]
    ErrorTag:
      type: object
      properties:
        tag: { type: string }
        message: { $ref: '#/components/schemas/ErrorMessage' }
      required: [tag, message]
    Error:
      type: object
      properties:
        error_code: { type: string }
        http_status: { type: integer }
        retryable: { type: boolean }
        request_id: { type: string }
        trace_id: { type: string }
        major: { $ref: '#/components/schemas/ErrorTag' }
        minor: { $ref: '#/components/schemas/ErrorTag' }
        details: {}
      required: [major]
    Envelope:
      type: object
      properties:
        success: { type: boolean }
        data: {}
        stats: { $ref: '#/components/schemas/Stats' }
        error: { $ref: '#/components/schemas/Error' }
      required: [success]
    ErrorResponse:
      allOf:
        - $ref: '#/components/schemas/Envelope'
        - type: object
          properties:
            success: { type: boolean, const: false }
          required: [success, error]
    OkResponse:
      allOf:
        - $ref: '#/components/schemas/Envelope'
        - type: object
          properties:
            success: { type: boolean, const: true }
            data: {}
          required: [success, data, stats]
    MrsS3Pointer:
      type: object
      properties:
        bucket: { type: string }
        key: { type: string }
        version_id: { type: string }
        etag: { type: string }
      required: [bucket, key]
    MrsPresignUpload:
      type: object
      properties:
        upload_url: { type: string }
        method: { type: string }
        headers:
          type: object
          additionalProperties: { type: string }
        expires_at: { type: string, format: date-time }
      required: [upload_url, method, headers, expires_at]
    MrsPresignDownload:
      type: object
      properties:
        download_url: { type: string }
        method: { type: string }
        headers:
          type: object
          additionalProperties: { type: string }
        expires_at: { type: string, format: date-time }
      required: [download_url, method, headers, expires_at]
    MrsRecordMetadata:
      type: object
      properties:
        record_id: { type: string }
        container: { $ref: '#/components/schemas/MrsContainer' }
        orgcode: { type: string }
        cccode: { type: string }
        status: { type: string, enum: [active, doomed, pending_upload] }
        revision: { type: string }
        caption: { type: string }
        tags:
          type: array
          items: { type: string }
        doom_at: { type: string, format: date-time }
        size_bytes: { type: number }
        size_gzip_bytes: { type: number }
        content_type: { type: string }
        content_encoding: { type: string }
        content_md5: { type: string }
        s3: { $ref: '#/components/schemas/MrsS3Pointer' }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
      required: [record_id, container, orgcode, status, revision, created_at, updated_at]
    MrsRecordPutPending:
      type: object
      properties:
        record_id: { type: string }
        presign: { $ref: '#/components/schemas/MrsPresignUpload' }
        content_token: { type: string }
        max_size_bytes: { type: number }
        container: { $ref: '#/components/schemas/MrsContainer' }
        orgcode: { type: string }
        cccode: { $ref: '#/components/schemas/CcCode' }
        caption: { type: string }
        tags: { type: array, items: { type: string } }
        doom_at: { type: string, format: date-time }
        size_bytes: { type: number }
        size_gzip_bytes: { type: number }
        content_md5: { type: string }
        revision: { type: string }
      required: [record_id, presign, content_token, max_size_bytes, container, orgcode, size_bytes, size_gzip_bytes, content_md5, revision]
    MrsRecordPutResult:
      oneOf:
        - $ref: '#/components/schemas/MrsRecordMetadata'
        - $ref: '#/components/schemas/MrsRecordPutPending'
    MrsRecordGetData:
      type: object
      properties:
        metadata: { $ref: '#/components/schemas/MrsRecordMetadata' }
        payload: {}
        presign: { $ref: '#/components/schemas/MrsPresignDownload' }
      required: [metadata]
    MrsRecordHead:
      type: object
      properties:
        exists: { type: boolean }
        status: { type: string, enum: [active, doomed, pending_upload] }
        size_bytes: { type: number }
        size_gzip_bytes: { type: number }
        doom_at: { type: string, format: date-time }
      required: [exists, status]
    MrsRecordList:
      type: object
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/MrsRecordMetadata' }
        next_token: { type: string }
paths:
  /stat:
    get:
      summary: Health check (session required)
      description: |
        Health check. Requires valid session_guid or api_key. Returns service name and status.
      x-route-class: Tier A
      x-qps-target: 5
      x-concurrency-target: 2
      x-latency-p95-ms: 500
      responses:
        '200':
          description: ok
        '401':
          description: Unauthorized — session_guid or api_key required
  /record:
    post:
      summary: Create/put record (inline JSON or presign)
      description: |
        Create/put record (inline JSON or presign). Auth is via headers; orgcode lives in query on GET and
        JSON body on POST. If this updates a revisioned record, expected_revision is required (428 if
        missing; 409 on mismatch). Route class Tier D (p95 300ms, p99 600ms).
      x-route-class: Tier D
      x-qps-target: 1000
      x-concurrency-target: 2000
      x-latency-p95-ms: 300
      x-latency-p99-ms: 600
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              x-idempotency-scope: orgcode+route+idempotency_key
              x-idempotency-retention-hours: 24
              x-idempotency-replay: return_original_response
              required: [container, orgcode, content_type]
              properties:
                container: { $ref: '#/components/schemas/MrsContainer' }
                orgcode: { type: string }
                cccode: { type: string }
                record_id: { type: string }
                caption: { type: string }
                tags: { type: array, items: { type: string } }
                doom_at: { type: string, format: date-time }
                idempotency_key: { type: string }
                expected_revision: { type: string }
                content_type: { type: string }
                payload: {}
                content_encoding: { type: string, enum: [gzip] }
                size_bytes: { type: number }
                size_gzip_bytes: { type: number }
                content_md5: { type: string, description: 'hex MD5 of gzip payload' }
      responses:
        '200':
          description: OK (inline metadata or presign handle)
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Envelope'
                - type: object
                  properties:
                    data:
                      $ref: '#/components/schemas/MrsRecordPutResult'
        default:
          description: Error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
    get:
      summary: Fetch record (inline JSON or presigned download)
      description: |
        Fetch record (inline JSON or presigned download). Auth is via headers; orgcode lives in query on GET
        and JSON body on POST. Org-scoped reads may return 404 for non-associated callers
        (anti-enumeration). If this updates a revisioned record, expected_revision is required (428 if
        missing; 409 on mismatch). Route class Tier B (p95 300ms).
      x-route-class: Tier B
      x-qps-target: 300
      x-concurrency-target: 500
      x-latency-p95-ms: 300
      parameters:
        - in: query
          name: container
          required: true
          schema: { $ref: '#/components/schemas/MrsContainer' }
        - in: query
          name: orgcode
          required: true
          schema: { type: string }
        - in: query
          name: cccode
          required: false
          schema: { $ref: '#/components/schemas/CcCode' }
        - in: query
          name: record_id
          required: true
          schema: { type: string }
        - in: query
          name: include_doomed
          required: false
          schema: { type: boolean }
        - in: query
          name: inline_ok
          required: false
          schema: { type: boolean }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Envelope'
                - type: object
                  properties:
                    data:
                      $ref: '#/components/schemas/MrsRecordGetData'
        default:
          description: Error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
  /record/complete:
    post:
      summary: Complete presigned upload
      description: |
        Complete presigned upload. Auth is via headers; orgcode lives in query on GET and JSON body on POST.
        If this updates a revisioned record, expected_revision is required (428 if missing; 409 on
        mismatch). Route class Tier D (p95 300ms, p99 600ms).
      x-route-class: Tier D
      x-qps-target: 1000
      x-concurrency-target: 2000
      x-latency-p95-ms: 300
      x-latency-p99-ms: 600
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [container, record_id, orgcode, expected_revision, content_token, reported]
              properties:
                container: { $ref: '#/components/schemas/MrsContainer' }
                record_id: { type: string }
                orgcode: { type: string }
                cccode: { type: string }
                expected_revision: { type: string }
                content_token: { type: string }
                reported:
                  type: object
                  required: [size_bytes, size_gzip_bytes, etag, content_type, content_encoding, content_md5]
                  properties:
                    size_bytes: { type: number }
                    size_gzip_bytes: { type: number }
                    etag: { type: string }
                    version_id: { type: string }
                    content_type: { type: string }
                    content_encoding: { type: string, enum: [gzip] }
                    content_md5: { type: string, description: 'hex MD5 of gzip payload' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Envelope'
                - type: object
                  properties:
                    data:
                      $ref: '#/components/schemas/MrsRecordMetadata'
        default:
          description: Error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
  /record/meta:
    get:
      summary: Fetch record metadata (no content)
      description: |
        Fetch record metadata (no content). Auth is via headers; orgcode lives in query on GET and JSON body
        on POST. Org-scoped reads may return 404 for non-associated callers (anti-enumeration). If this
        updates a revisioned record, expected_revision is required (428 if missing; 409 on mismatch). Route
        class Tier B (p95 300ms).
      x-route-class: Tier B
      x-qps-target: 300
      x-concurrency-target: 500
      x-latency-p95-ms: 300
      parameters:
        - in: query
          name: container
          required: true
          schema: { $ref: '#/components/schemas/MrsContainer' }
        - in: query
          name: orgcode
          required: true
          schema: { type: string }
        - in: query
          name: cccode
          required: false
          schema: { $ref: '#/components/schemas/CcCode' }
        - in: query
          name: record_id
          required: true
          schema: { type: string }
        - in: query
          name: include_doomed
          required: false
          schema: { type: boolean }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Envelope'
                - type: object
                  properties:
                    data:
                      $ref: '#/components/schemas/MrsRecordMetadata'
        default:
          description: Error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
  /list:
    get:
      summary: List records
      description: |
        List records. Auth is via headers; orgcode lives in query on GET and JSON body on POST. Paginated
        with limit/next_token (default 8; clamp 1–256). Org-scoped reads may return 404 for non-associated
        callers (anti-enumeration). Route class Tier B (p95 300ms).
      x-route-class: Tier B
      x-qps-target: 300
      x-concurrency-target: 500
      x-latency-p95-ms: 300
      parameters:
        - in: query
          name: orgcode
          required: true
          schema: { type: string }
        - in: query
          name: cccode
          required: false
          schema: { $ref: '#/components/schemas/CcCode' }
        - in: query
          name: container
          required: false
          schema: { $ref: '#/components/schemas/MrsContainer' }
        - in: query
          name: tag
          required: false
          schema: { type: string }
        - in: query
          name: status
          required: false
          schema: { type: string, enum: [active, doomed, all] }
        - in: query
          name: include_doomed
          required: false
          schema: { type: boolean }
        - in: query
          name: caption_prefix
          required: false
          schema: { type: string }
        - in: query
          name: record_prefix
          required: false
          schema: { type: string }
        - in: query
          name: limit
          required: false
          schema: { type: integer, minimum: 1, maximum: 256 }
        - in: query
          name: next_token
          required: false
          schema: { type: string }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Envelope'
                - type: object
                  properties:
                    data:
                      $ref: '#/components/schemas/MrsRecordList'
        default:
          description: Error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
  /tag/add:
    post:
      summary: Add tags to a record
      description: |
        Add tags to a record. Auth is via headers; orgcode lives in query on GET and JSON body on POST. If
        this updates a revisioned record, expected_revision is required (428 if missing; 409 on mismatch).
        Route class Tier D (p95 300ms, p99 600ms).
      x-route-class: Tier D
      x-qps-target: 1000
      x-concurrency-target: 2000
      x-latency-p95-ms: 300
      x-latency-p99-ms: 600
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [container, record_id, orgcode, tags, expected_revision]
              properties:
                container: { $ref: '#/components/schemas/MrsContainer' }
                record_id: { type: string }
                orgcode: { type: string }
                cccode: { type: string }
                tags: { type: array, items: { type: string } }
                expected_revision: { type: string }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Envelope'
                - type: object
                  properties:
                    data:
                      $ref: '#/components/schemas/MrsRecordMetadata'
        default:
          description: Error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
  /tag/remove:
    post:
      summary: Remove tags from a record
      description: |
        Remove tags from a record. Auth is via headers; orgcode lives in query on GET and JSON body on POST.
        If this updates a revisioned record, expected_revision is required (428 if missing; 409 on
        mismatch). Route class Tier D (p95 300ms, p99 600ms).
      x-route-class: Tier D
      x-qps-target: 1000
      x-concurrency-target: 2000
      x-latency-p95-ms: 300
      x-latency-p99-ms: 600
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [container, record_id, orgcode, tags, expected_revision]
              properties:
                container: { $ref: '#/components/schemas/MrsContainer' }
                record_id: { type: string }
                orgcode: { type: string }
                cccode: { type: string }
                tags: { type: array, items: { type: string } }
                expected_revision: { type: string }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Envelope'
                - type: object
                  properties:
                    data:
                      $ref: '#/components/schemas/MrsRecordMetadata'
        default:
          description: Error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
  /doom:
    post:
      summary: Doom a record (terminal)
      description: |
        Doom a record (terminal). Auth is via headers; orgcode lives in query on GET and JSON body on POST.
        If this updates a revisioned record, expected_revision is required (428 if missing; 409 on
        mismatch). Route class Tier A (p95 500ms).
      x-route-class: Tier A
      x-qps-target: 50
      x-concurrency-target: 200
      x-latency-p95-ms: 500
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [container, record_id, orgcode, expected_revision]
              properties:
                container: { $ref: '#/components/schemas/MrsContainer' }
                record_id: { type: string }
                orgcode: { type: string }
                cccode: { type: string }
                reason: { type: string }
                expected_revision: { type: string }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Envelope'
                - type: object
                  properties:
                    data:
                      $ref: '#/components/schemas/MrsRecordMetadata'
        default:
          description: Error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
  /ttl/set:
    post:
      summary: Set doom_at (TTL)
      description: |
        Set doom_at (TTL). Auth is via headers; orgcode lives in query on GET and JSON body on POST. If this
        updates a revisioned record, expected_revision is required (428 if missing; 409 on mismatch). Route
        class Tier A (p95 500ms).
      x-route-class: Tier A
      x-qps-target: 50
      x-concurrency-target: 200
      x-latency-p95-ms: 500
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [container, record_id, orgcode, doom_at, expected_revision]
              properties:
                container: { $ref: '#/components/schemas/MrsContainer' }
                record_id: { type: string }
                orgcode: { type: string }
                cccode: { type: string }
                doom_at: { type: string, format: date-time }
                expected_revision: { type: string }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Envelope'
                - type: object
                  properties:
                    data:
                      $ref: '#/components/schemas/MrsRecordMetadata'
        default:
          description: Error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
  /head:
    get:
      summary: HEAD-like existence + minimal metadata
      description: |
        HEAD-like existence + minimal metadata. Auth is via headers; orgcode lives in query on GET and JSON
        body on POST. Org-scoped reads may return 404 for non-associated callers (anti-enumeration). Route
        class Tier B (p95 300ms).
      x-route-class: Tier B
      x-qps-target: 300
      x-concurrency-target: 500
      x-latency-p95-ms: 300
      parameters:
        - in: query
          name: container
          required: true
          schema: { $ref: '#/components/schemas/MrsContainer' }
        - in: query
          name: orgcode
          required: true
          schema: { type: string }
        - in: query
          name: cccode
          required: false
          schema: { $ref: '#/components/schemas/CcCode' }
        - in: query
          name: record_id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Envelope'
                - type: object
                  properties:
                    data:
                      $ref: '#/components/schemas/MrsRecordHead'
        default:
          description: Error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
