openapi: 3.0.3 info: title: StellaOps Graph Gateway (draft) version: 0.0.3-pre description: | Draft API surface for graph search/query/paths/diff/export with streaming tiles, cost budgets, overlays, and RBAC headers. Aligns with sprint 0207 Wave 1 outline (GRAPH-API-28-001..011). servers: - url: https://gateway.local/api security: - bearerAuth: [] paths: /graph/search: post: summary: Search graph nodes with prefix/exact semantics and filters security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/TenantHeader' - $ref: '#/components/parameters/RequestIdHeader' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SearchRequest' responses: '200': description: Stream of search tiles (NDJSON) content: application/x-ndjson: schema: $ref: '#/components/schemas/TileEnvelope' examples: sample: summary: Node + cursor tiles value: | {"type":"node","seq":0,"data":{"id":"gn:tenant:component:abc","kind":"component","tenant":"acme","attributes":{"purl":"pkg:npm/lodash@4.17.21"}},"cost":{"limit":1000,"remaining":999,"consumed":1}} {"type":"cursor","seq":1,"data":{"token":"cursor-123","resumeUrl":"https://gateway.local/api/graph/query?cursor=cursor-123"}} '400': { $ref: '#/components/responses/ValidationError' } '401': { $ref: '#/components/responses/Unauthorized' } '429': { $ref: '#/components/responses/BudgetExceeded' } responses: '200': description: Stream of search tiles (NDJSON) content: application/x-ndjson: schema: $ref: '#/components/schemas/TileEnvelope' examples: sample: summary: Node + cursor tiles value: | {"type":"node","seq":0,"data":{"id":"gn:tenant:component:abc","kind":"component","tenant":"acme","attributes":{"purl":"pkg:npm/lodash@4.17.21"}},"cost":{"limit":1000,"remaining":999,"consumed":1}} {"type":"cursor","seq":1,"data":{"token":"cursor-123","resumeUrl":"https://gateway.local/api/graph/search?cursor=cursor-123"}} headers: X-RateLimit-Remaining: description: Remaining request budget within the window. schema: type: integer Retry-After: description: Seconds until next request is allowed when rate limited. schema: type: integer /graph/query: post: summary: Execute graph query with budgeted streaming tiles security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/TenantHeader' - $ref: '#/components/parameters/RequestIdHeader' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/QueryRequest' responses: '200': description: Stream of query tiles (NDJSON) content: application/x-ndjson: schema: $ref: '#/components/schemas/TileEnvelope' examples: mixedTiles: summary: Node + edge + stats tiles value: | {"type":"node","seq":0,"data":{"id":"gn:tenant:artifact:sha256:...","tenant":"acme","kind":"artifact","attributes":{"sbom_digest":"sha256:abc"}}} {"type":"edge","seq":1,"data":{"id":"ge:tenant:CONTAINS:...","sourceId":"gn:tenant:artifact:...","targetId":"gn:tenant:component:...","kind":"CONTAINS"}} {"type":"stats","seq":2,"data":{"nodesEmitted":1,"edgesEmitted":1,"depthReached":2,"cacheHitRatio":0.8}} '400': { $ref: '#/components/responses/ValidationError' } '401': { $ref: '#/components/responses/Unauthorized' } '429': { $ref: '#/components/responses/BudgetExceeded' } responses: '200': description: Stream of query tiles (NDJSON) content: application/x-ndjson: schema: $ref: '#/components/schemas/TileEnvelope' examples: mixedTiles: summary: Node + edge + stats tiles value: | {"type":"node","seq":0,"data":{"id":"gn:tenant:artifact:sha256:...","tenant":"acme","kind":"artifact","attributes":{"sbom_digest":"sha256:abc"}}} {"type":"edge","seq":1,"data":{"id":"ge:tenant:CONTAINS:...","sourceId":"gn:tenant:artifact:...","targetId":"gn:tenant:component:...","kind":"CONTAINS"}} {"type":"stats","seq":2,"data":{"nodesEmitted":1,"edgesEmitted":1,"depthReached":2,"cacheHitRatio":0.8}} headers: X-RateLimit-Remaining: description: Remaining request budget within the window. schema: type: integer Retry-After: description: Seconds until next request is allowed when rate limited. schema: type: integer /graph/paths: post: summary: Find constrained paths between node sets (depth ≤ 6) security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/TenantHeader' - $ref: '#/components/parameters/RequestIdHeader' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PathsRequest' responses: '200': description: Stream of path tiles ordered by hop content: application/x-ndjson: schema: $ref: '#/components/schemas/TileEnvelope' examples: pathTiles: summary: Path tiles grouped by hop value: | {"type":"node","seq":0,"data":{"id":"gn:tenant:component:src","kind":"component","tenant":"acme"}} {"type":"edge","seq":1,"data":{"id":"ge:tenant:DEPENDS_ON:1","sourceId":"gn:tenant:component:src","targetId":"gn:tenant:component:dst","kind":"DEPENDS_ON"}} {"type":"stats","seq":2,"data":{"nodesEmitted":2,"edgesEmitted":1,"depthReached":1}} '400': { $ref: '#/components/responses/ValidationError' } '401': { $ref: '#/components/responses/Unauthorized' } '429': { $ref: '#/components/responses/BudgetExceeded' } responses: '200': description: Stream of path tiles ordered by hop content: application/x-ndjson: schema: $ref: '#/components/schemas/TileEnvelope' examples: pathTiles: summary: Path tiles grouped by hop value: | {"type":"node","seq":0,"data":{"id":"gn:tenant:component:src","kind":"component","tenant":"acme","attributes":{"purl":"pkg:npm/demo@1.0.0"},"pathHop":0}} {"type":"edge","seq":1,"data":{"id":"ge:tenant:DEPENDS_ON:1","sourceId":"gn:tenant:component:src","targetId":"gn:tenant:component:dst","kind":"DEPENDS_ON","pathHop":1}} {"type":"stats","seq":2,"data":{"nodesEmitted":2,"edgesEmitted":1,"depthReached":1}} /graph/diff: post: summary: Stream diff between two graph snapshots with overlay deltas security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/TenantHeader' - $ref: '#/components/parameters/RequestIdHeader' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DiffRequest' responses: '200': description: Stream of diff tiles (added/removed/changed) content: application/x-ndjson: schema: $ref: '#/components/schemas/TileEnvelope' examples: diffTiles: summary: Added/removed tiles value: | {"type":"node","seq":0,"data":{"id":"gn:tenant:component:new","kind":"component","tenant":"acme","attributes":{"purl":"pkg:npm/new@1.0.0"}}} {"type":"diagnostic","seq":1,"data":{"level":"info","message":"snapshot diff complete"}} '400': { $ref: '#/components/responses/ValidationError' } '401': { $ref: '#/components/responses/Unauthorized' } '429': { $ref: '#/components/responses/BudgetExceeded' } /graph/export/{jobId}/manifest: get: summary: Download deterministic checksum manifest for a completed export job security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/TenantHeader' - $ref: '#/components/parameters/RequestIdHeader' - name: jobId in: path required: true schema: type: string responses: '200': description: Deterministic manifest content: application/json: schema: type: object properties: files: type: array items: type: object properties: path: { type: string } sha256: { type: string } size: { type: integer } exportId: { type: string } '404': { description: Manifest not ready or job missing } /graph/export: post: summary: Request export job for snapshot or query result security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/TenantHeader' - $ref: '#/components/parameters/RequestIdHeader' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ExportRequest' responses: '202': description: Export job accepted content: application/json: schema: $ref: '#/components/schemas/ExportJob' '400': { $ref: '#/components/responses/ValidationError' } '401': { $ref: '#/components/responses/Unauthorized' } /graph/export/{jobId}: get: summary: Check export job status or download manifest security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/TenantHeader' - $ref: '#/components/parameters/RequestIdHeader' - name: jobId in: path required: true schema: type: string responses: '200': description: Job status content: application/json: schema: $ref: '#/components/schemas/ExportJob' '404': description: Job not found content: application/json: schema: $ref: '#/components/schemas/Error' components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT parameters: TenantHeader: name: X-Stella-Tenant in: header required: true schema: type: string description: Tenant identifier enforced on all routes. RequestIdHeader: name: X-Request-Id in: header required: false schema: type: string description: Optional caller-provided correlation id, echoed in responses. schemas: OverlayPayload: type: object description: Overlay content injected into node/edge tiles when requested (policy/vex/advisory). properties: kind: type: string enum: [policy, vex, advisory] version: type: string description: Contract version of the overlay payload. data: type: object additionalProperties: true required: [kind, version, data] CostBudget: type: object properties: limit: type: integer minimum: 1 example: 1000 remaining: type: integer minimum: 0 example: 995 consumed: type: integer minimum: 0 example: 5 required: [limit, remaining, consumed] TileEnvelope: type: object properties: type: type: string enum: [node, edge, stats, cursor, diagnostic] seq: type: integer minimum: 0 example: 0 cost: $ref: '#/components/schemas/CostBudget' data: description: Payload varies by tile type. oneOf: - $ref: '#/components/schemas/NodeTile' - $ref: '#/components/schemas/EdgeTile' - $ref: '#/components/schemas/StatsTile' - $ref: '#/components/schemas/CursorTile' - $ref: '#/components/schemas/DiagnosticTile' required: [type, seq] NodeTile: type: object properties: id: { type: string } kind: { type: string } tenant: { type: string } attributes: { type: object } pathHop: type: integer description: Hop depth for path streaming responses. overlays: type: object description: Optional overlay payloads (policy/vex/advisory) keyed by overlay kind. additionalProperties: $ref: '#/components/schemas/OverlayPayload' required: [id, kind, tenant] EdgeTile: type: object properties: id: { type: string } kind: { type: string } sourceId: { type: string } targetId: { type: string } tenant: { type: string } attributes: { type: object } pathHop: type: integer description: Hop depth for path streaming responses. overlays: type: object additionalProperties: $ref: '#/components/schemas/OverlayPayload' required: [id, kind, sourceId, targetId, tenant] StatsTile: type: object properties: nodesEmitted: { type: integer, minimum: 0 } edgesEmitted: { type: integer, minimum: 0 } depthReached: { type: integer, minimum: 0 } cacheHitRatio: { type: number, minimum: 0, maximum: 1 } required: [nodesEmitted, edgesEmitted] CursorTile: type: object properties: token: { type: string } resumeUrl: { type: string, format: uri } required: [token] DiagnosticTile: type: object properties: level: { type: string, enum: [info, warn, error] } message: { type: string } details: { type: object } required: [level, message] SearchRequest: type: object properties: query: type: string description: Prefix or exact text; required unless filters present. kinds: type: array items: type: string limit: type: integer default: 50 maximum: 500 filters: type: object additionalProperties: true ordering: type: string enum: [relevance, id] cursor: type: string description: Resume token from prior search response. required: [kinds] QueryRequest: type: object properties: dsl: type: string description: DSL expression for graph traversal (mutually exclusive with filter). filter: type: object description: Structured filter alternative to DSL. budget: type: object properties: nodeCap: { type: integer } edgeCap: { type: integer } timeMs: { type: integer } overlays: type: array items: type: string enum: [policy, vex, advisory] explain: type: string enum: [none, minimal, full] default: none cursor: type: string description: Resume token from prior query response. anyOf: - required: [dsl] - required: [filter] PathsRequest: type: object properties: sourceIds: type: array items: { type: string } minItems: 1 targetIds: type: array items: { type: string } minItems: 1 maxDepth: type: integer maximum: 6 default: 4 constraints: type: object properties: edgeKinds: type: array items: { type: string } fanoutCap: type: integer overlays: type: array items: { type: string } required: [sourceIds, targetIds] DiffRequest: type: object properties: snapshotA: { type: string } snapshotB: { type: string } filters: type: object additionalProperties: true cursor: type: string description: Resume token from prior diff stream. required: [snapshotA, snapshotB] ExportRequest: type: object properties: snapshotId: type: string queryRef: type: string formats: type: array items: type: string enum: [graphml, csv, ndjson, png, svg] includeOverlays: type: boolean default: false anyOf: - required: [snapshotId] - required: [queryRef] required: [formats] ExportJob: type: object properties: jobId: { type: string } status: { type: string, enum: [pending, running, succeeded, failed] } checksumManifestUrl: { type: string, format: uri } downloadUrl: { type: string, format: uri } createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } message: { type: string } expiresAt: { type: string, format: date-time, description: "Optional expiry for download links." } required: [jobId, status] Error: type: object properties: error: type: string enum: [GRAPH_BUDGET_EXCEEDED, GRAPH_VALIDATION_FAILED, GRAPH_RATE_LIMITED, GRAPH_UNAUTHORIZED] message: type: string details: type: object request_id: type: string required: [error, message] responses: ValidationError: description: Request failed validation content: application/json: schema: $ref: '#/components/schemas/Error' Unauthorized: description: Missing or invalid credentials content: application/json: schema: $ref: '#/components/schemas/Error' BudgetExceeded: description: Budget exhausted mid-stream; includes partial cursor details content: application/json: schema: $ref: '#/components/schemas/Error' headers: Retry-After: description: Seconds until budgets refresh. schema: type: integer