Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d49d919
test(openapi): require repository license authority
seonghobae Aug 10, 2026
d5b2cdf
fix(openapi): align buyer seed license with repository
seonghobae Aug 10, 2026
ea00bfa
test(openapi): reject hard-coded buyer demo identities
seonghobae Aug 10, 2026
b9ea77a
Merge branch 'main' into fix/openapi-license-authority
opencode-agent[bot] Aug 10, 2026
deb1953
fix(openapi): remove demo identity from integration contract
seonghobae Aug 10, 2026
243216f
test(openapi): parse license metadata structurally
seonghobae Aug 10, 2026
82b7b38
test(openapi): leave structured license assertions to YAML parser
seonghobae Aug 10, 2026
7e97ee6
test(openapi): require shipped job deletion contract
seonghobae Aug 10, 2026
2b3dd03
fix(openapi): document shipped job deletion route
seonghobae Aug 10, 2026
41e26e0
test(openapi): require signed artifact byte contract
seonghobae Aug 10, 2026
92255da
fix(openapi): document signed artifact byte delivery
seonghobae Aug 10, 2026
1e1b142
test(openapi): require shipped liveness contract
seonghobae Aug 10, 2026
123dd23
fix(openapi): document protected-main liveness
seonghobae Aug 10, 2026
b6d79bc
test(openapi): validate delete path parameter semantics
seonghobae Aug 10, 2026
5218537
Merge branch 'main' into fix/openapi-license-authority
opencode-agent[bot] Aug 10, 2026
581f531
test(openapi): require artifact range and auth contract values
seonghobae Aug 10, 2026
71c1e3a
fix(openapi): declare artifact Content-Range responses
seonghobae Aug 10, 2026
40288d1
Merge branch 'main' into fix/openapi-license-authority
opencode-agent[bot] Aug 12, 2026
3d6f646
Merge branch 'main' into fix/openapi-license-authority
opencode-agent[bot] Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 130 additions & 10 deletions docs/deployment/clearfolio-buyer-connector.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ info:
description: >
Import seed for a buyer-owned gateway or Power Platform custom connector.
This spec reflects the current Clearfolio Viewer JSON API contract and the
buyer-demo signed-header scaffold. It is not a production OIDC/JWT profile.
signed tenant-header scaffold. It is not a production OIDC/JWT profile.
license:
name: Proprietary - buyer diligence use only
url: https://github.com/ContextualWisdomLab/clearfolio
name: Apache-2.0
url: https://www.apache.org/licenses/LICENSE-2.0.html
servers:
- url: https://{gatewayHost}
description: Buyer gateway in front of Clearfolio Viewer.
Expand All @@ -19,13 +19,15 @@ security:
- clearfolioTenantHeaders: []
tags:
- name: Conversion
description: Document intake, status polling, and retry operations.
description: Document intake, status polling, deletion, and retry operations.
- name: Viewer
description: Viewer bootstrap operations that return signed artifact URLs.
- name: ArtifactLinks
description: Signed artifact-link creation, revocation, and read evidence.
description: Signed artifact-link creation, revocation, byte delivery, and read evidence.
- name: Analytics
description: Tenant-scoped KPI snapshot and export evidence operations.
- name: Operations
description: Operational endpoints intentionally exposed without tenant identity.
paths:
/api/v1/convert/jobs:
post:
Expand Down Expand Up @@ -98,6 +100,28 @@ paths:
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
delete:
tags:
- Conversion
operationId: deleteConversionJob
summary: Delete a conversion job and generated artifacts.
description: Requires `job:delete`; missing and cross-tenant jobs are hidden.
parameters:
- $ref: "#/components/parameters/JobId"
- $ref: "#/components/parameters/TenantId"
- $ref: "#/components/parameters/SubjectId"
- $ref: "#/components/parameters/Permissions"
- $ref: "#/components/parameters/ClaimsIssuedAt"
- $ref: "#/components/parameters/ClaimsSignature"
responses:
"204":
description: Conversion job and generated artifacts deleted.
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
/api/v1/convert/jobs/{jobId}/retry:
post:
tags:
Expand Down Expand Up @@ -199,7 +223,7 @@ paths:
tags:
- ArtifactLinks
operationId: revokeArtifactLink
summary: Revoke a previously issued artifact link.
summary: Revoke a previously issued signed artifact link.
description: Requires `artifact-link:revoke`; intended for operators.
parameters:
- $ref: "#/components/parameters/TokenId"
Expand Down Expand Up @@ -254,6 +278,57 @@ paths:
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
/artifacts/{docId}.pdf:
get:
tags:
- ArtifactLinks
operationId: getPdfArtifact
summary: Read a converted PDF through a signed artifact token.
description: >
Serves bytes only for a succeeded conversion with an issued, unexpired,
unrevoked, document/tenant/checksum-bound artifact token. The token may
be supplied either as the `artifactToken` query parameter or as a Bearer
credential. At most one `bytes` range is accepted and verified reads are
recorded by the server.
security:
- artifactTokenQuery: []
- artifactTokenBearer: []
parameters:
- $ref: "#/components/parameters/DocId"
- $ref: "#/components/parameters/Range"
responses:
"200":
description: Full PDF artifact.
content:
application/pdf:
schema:
type: string
format: binary
"206":
description: Requested single byte range of the PDF artifact.
headers:
Content-Range:
description: Returned byte range and complete PDF length as `bytes start-end/total`.
schema:
type: string
content:
application/pdf:
schema:
type: string
format: binary
"401":
description: Signed artifact token is missing, malformed, expired, or otherwise unauthorized.
"403":
description: Signed artifact token is revoked or otherwise forbidden.
"404":
description: Conversion job or PDF artifact is absent or unavailable.
"416":
description: Range is malformed or unsatisfiable for the current PDF bytes.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
headers:
Content-Range:
description: Complete PDF length as `bytes */total`.
schema:
type: string
/api/v1/analytics/kpi-snapshot:
get:
tags:
Expand Down Expand Up @@ -304,6 +379,24 @@ paths:
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
/healthz:
get:
tags:
- Operations
operationId: getHealthz
summary: Check process liveness.
description: >
Returns only the static process-liveness payload shipped on protected main.
This endpoint intentionally requires no tenant identity and does not claim
dependency or traffic readiness.
security: []
responses:
"200":
description: Process is alive.
content:
application/json:
schema:
$ref: "#/components/schemas/HealthResponse"
components:
securitySchemes:
clearfolioTenantHeaders:
Expand All @@ -314,21 +407,31 @@ components:
Buyer gateway supplies the full signed Clearfolio tenant header set.
The signature alone is not enough without tenant, subject, permissions,
and issued-at headers.
artifactTokenQuery:
type: apiKey
in: query
name: artifactToken
description: Short-lived Clearfolio artifact token issued for this document.
artifactTokenBearer:
type: http
scheme: bearer
bearerFormat: ClearfolioArtifactToken
description: Short-lived Clearfolio artifact token supplied as an Authorization bearer credential.
parameters:
TenantId:
name: X-Clearfolio-Tenant-Id
in: header
required: true
schema:
type: string
example: buyer-demo
example: tenant-example
SubjectId:
name: X-Clearfolio-Subject-Id
in: header
required: true
schema:
type: string
example: buyer-demo-operator
example: operator-example
Permissions:
name: X-Clearfolio-Permissions
in: header
Expand All @@ -340,7 +443,7 @@ components:
The gateway must sign this canonical form, not the raw string.
schema:
type: string
example: job:create,job:read,viewer:read,artifact-link:create,analytics:read
example: job:create,job:read,job:delete,viewer:read,artifact-link:create,analytics:read
ClaimsIssuedAt:
name: X-Clearfolio-Claims-Issued-At
in: header
Expand Down Expand Up @@ -402,6 +505,13 @@ components:
required: true
schema:
type: string
Range:
name: Range
in: header
required: false
description: Optional single HTTP byte range, for example `bytes=0-1023`.
schema:
type: string
responses:
BadRequest:
description: Bad request or validation failure.
Expand Down Expand Up @@ -434,6 +544,16 @@ components:
schema:
$ref: "#/components/schemas/ApiErrorResponse"
schemas:
HealthResponse:
type: object
required:
- status
additionalProperties: false
properties:
status:
type: string
enum:
- ok
SubmitConversionResponse:
type: object
required:
Expand Down Expand Up @@ -670,4 +790,4 @@ components:
type: string
details:
type: object
additionalProperties: true
additionalProperties: true
39 changes: 39 additions & 0 deletions scripts/test_buyer_openapi_license_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""Keep buyer OpenAPI acquisition metadata aligned with repository authority."""

from __future__ import annotations

import unittest
from pathlib import Path


REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
OPENAPI_PATH = REPOSITORY_ROOT / "docs/deployment/clearfolio-buyer-connector.openapi.yaml"
LICENSE_PATH = REPOSITORY_ROOT / "LICENSE"


class BuyerOpenApiLicenseContractTest(unittest.TestCase):
"""Prevent acquisition-facing API metadata from contradicting product authority."""

def test_openapi_does_not_claim_proprietary_repository_licensing(self) -> None:
"""Reject the superseded proprietary-only buyer-diligence claim."""

repository_license = LICENSE_PATH.read_text(encoding="utf-8")
openapi = OPENAPI_PATH.read_text(encoding="utf-8")

self.assertTrue(repository_license.startswith("Apache License\nVersion 2.0"))
self.assertNotIn("Proprietary - buyer diligence use only", openapi)
Comment thread
seonghobae marked this conversation as resolved.

def test_openapi_examples_do_not_encode_buyer_demo_identity(self) -> None:
"""Keep machine-readable integration examples independent of demo authority."""

openapi = OPENAPI_PATH.read_text(encoding="utf-8")

self.assertNotIn("buyer-demo", openapi)
self.assertNotIn("buyer-demo-operator", openapi)
self.assertIn("example: tenant-example", openapi)
self.assertIn("example: operator-example", openapi)


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.clearfolio.viewer.config;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;

import org.junit.jupiter.api.Test;
import org.yaml.snakeyaml.Yaml;

/**
* Verifies acquisition-facing OpenAPI license metadata through the parsed YAML structure.
*/
class BuyerOpenApiLicenseContractTest {

private static final Path OPENAPI_PATH =
Path.of("docs/deployment/clearfolio-buyer-connector.openapi.yaml");
private static final Path LICENSE_PATH = Path.of("LICENSE");

@Test
void openApiLicenseObjectMatchesRepositoryAuthority() throws IOException {
String repositoryLicense = Files.readString(LICENSE_PATH);
assertTrue(repositoryLicense.startsWith("Apache License\nVersion 2.0"));

Object parsed;
try (Reader reader = Files.newBufferedReader(OPENAPI_PATH)) {
parsed = new Yaml().load(reader);
}

Map<?, ?> root = assertInstanceOf(Map.class, parsed);
Map<?, ?> info = assertInstanceOf(Map.class, root.get("info"));
Map<?, ?> license = assertInstanceOf(Map.class, info.get("license"));

assertEquals("Apache-2.0", license.get("name"));
assertEquals("https://www.apache.org/licenses/LICENSE-2.0.html", license.get("url"));
}
}
Loading
Loading