diff --git a/server/src/schemas/catalog-openapi.ts b/server/src/schemas/catalog-openapi.ts index 22301f80df..d41504b6a3 100644 --- a/server/src/schemas/catalog-openapi.ts +++ b/server/src/schemas/catalog-openapi.ts @@ -161,3 +161,89 @@ registry.registerPath({ 404: { description: 'Dispute not found', content: { 'application/json': { schema: ErrorSchema } } }, }, }); + +// ── GET /api/registry/catalog (browse) ────────────────────────── +// The read/consume side of the fact loop: resolve identifiers → sync/browse +// the catalog locally → build lists. + +const CatalogBrowseEntrySchema = z.object({ + property_rid: z.string(), + property_id: z.string().nullable(), + classification: z.string().openapi({ example: 'property' }), + source: z.string().openapi({ example: 'adagents_json' }), + status: z.string().openapi({ example: 'active' }), + identifiers: z.array(CatalogIdentifierSchema), +}).openapi('CatalogBrowseEntry'); + +const CatalogBrowseResponseSchema = z.object({ + entries: z.array(CatalogBrowseEntrySchema), + total: z.number().int(), + next_cursor: z.string().nullable().openapi({ description: 'Opaque cursor for the next page; null when exhausted.' }), +}).openapi('CatalogBrowseResponse'); + +registry.registerPath({ + method: 'get', + path: '/api/registry/catalog', + operationId: 'browseCatalog', + summary: 'Browse the property catalog', + description: + 'Browse the property fact-graph with filters. Cursor-paginated (opaque `cursor` → `next_cursor`). Each entry carries the property\'s identifiers. `property_rid` is a non-authoritative join/match handle, never an authorization credential.', + tags: ['Property Catalog'], + request: { + query: z.object({ + classification: z.string().optional().openapi({ example: 'property' }), + source: z.string().optional(), + status: z.string().optional().openapi({ example: 'active' }), + identifier_type: z.string().optional().openapi({ example: 'domain' }), + search: z.string().optional(), + min_resolves: z.string().optional().openapi({ description: 'Minimum lifetime resolve count (integer).' }), + active_since: z.string().optional().openapi({ description: 'ISO 8601 — only properties with resolve activity since this time.' }), + limit: z.string().optional().openapi({ description: 'Page size (integer).' }), + cursor: z.string().optional().openapi({ description: 'Opaque pagination cursor from a prior `next_cursor`.' }), + }), + }, + responses: { + 200: { description: 'Catalog page', content: { 'application/json': { schema: CatalogBrowseResponseSchema } } }, + }, +}); + +// ── GET /api/registry/catalog/sync (delta) ────────────────────── + +const CatalogSyncEntrySchema = z.object({ + property_rid: z.string(), + property_id: z.string().nullable(), + classification: z.string(), + source: z.string(), + status: z.string(), + adagents_url: z.string().nullable(), + created_by: z.string().nullable(), + created_at: z.string(), + updated_at: z.string(), + source_updated_at: z.string(), +}).openapi('CatalogSyncEntry'); + +const CatalogSyncResponseSchema = z.object({ + entries: z.array(CatalogSyncEntrySchema), + server_timestamp: z.string().openapi({ description: 'Watermark to pass as `since` on the next sync (avoids clock skew).' }), + next_cursor: z.string().nullable(), +}).openapi('CatalogSyncResponse'); + +registry.registerPath({ + method: 'get', + path: '/api/registry/catalog/sync', + operationId: 'syncCatalog', + summary: 'Sync catalog changes since a watermark', + description: + 'Delta sync for local mirrors: returns catalog entries created/updated since `since` (a prior `server_timestamp`), capped at 10,000 per page. Pagination is by the returned `server_timestamp` watermark, distinct from browseCatalog\'s opaque cursor.', + tags: ['Property Catalog'], + request: { + query: z.object({ + since: z.string().openapi({ description: 'Watermark from a prior response `server_timestamp` (required).', example: '2026-03-27T10:00:00Z' }), + limit: z.string().optional().openapi({ description: 'Page size, capped at 10,000.' }), + }), + }, + responses: { + 200: { description: 'Catalog delta', content: { 'application/json': { schema: CatalogSyncResponseSchema } } }, + 400: { description: 'Missing required `since` parameter', content: { 'application/json': { schema: ErrorSchema } } }, + }, +}); diff --git a/static/openapi/registry.yaml b/static/openapi/registry.yaml index 56b3bcc5d7..83038a48f8 100644 --- a/static/openapi/registry.yaml +++ b/static/openapi/registry.yaml @@ -3415,6 +3415,111 @@ components: - status - created_at additionalProperties: {} + CatalogBrowseResponse: + type: object + properties: + entries: + type: array + items: + $ref: "#/components/schemas/CatalogBrowseEntry" + total: + type: integer + next_cursor: + type: + - string + - "null" + description: Opaque cursor for the next page; null when exhausted. + required: + - entries + - total + - next_cursor + CatalogBrowseEntry: + type: object + properties: + property_rid: + type: string + property_id: + type: + - string + - "null" + classification: + type: string + example: property + source: + type: string + example: adagents_json + status: + type: string + example: active + identifiers: + type: array + items: + $ref: "#/components/schemas/CatalogIdentifier" + required: + - property_rid + - property_id + - classification + - source + - status + - identifiers + CatalogSyncResponse: + type: object + properties: + entries: + type: array + items: + $ref: "#/components/schemas/CatalogSyncEntry" + server_timestamp: + type: string + description: Watermark to pass as `since` on the next sync (avoids clock skew). + next_cursor: + type: + - string + - "null" + required: + - entries + - server_timestamp + - next_cursor + CatalogSyncEntry: + type: object + properties: + property_rid: + type: string + property_id: + type: + - string + - "null" + classification: + type: string + source: + type: string + status: + type: string + adagents_url: + type: + - string + - "null" + created_by: + type: + - string + - "null" + created_at: + type: string + updated_at: + type: string + source_updated_at: + type: string + required: + - property_rid + - property_id + - classification + - source + - status + - adagents_url + - created_by + - created_at + - updated_at + - source_updated_at AdagentsJson: type: object properties: @@ -10647,6 +10752,113 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + /api/registry/catalog: + get: + operationId: browseCatalog + summary: Browse the property catalog + description: Browse the property fact-graph with filters. Cursor-paginated (opaque `cursor` → `next_cursor`). Each entry carries the property's identifiers. `property_rid` is a non-authoritative join/match handle, never an authorization credential. + tags: + - Property Catalog + parameters: + - schema: + type: string + example: property + required: false + name: classification + in: query + - schema: + type: string + required: false + name: source + in: query + - schema: + type: string + example: active + required: false + name: status + in: query + - schema: + type: string + example: domain + required: false + name: identifier_type + in: query + - schema: + type: string + required: false + name: search + in: query + - schema: + type: string + description: Minimum lifetime resolve count (integer). + required: false + description: Minimum lifetime resolve count (integer). + name: min_resolves + in: query + - schema: + type: string + description: ISO 8601 — only properties with resolve activity since this time. + required: false + description: ISO 8601 — only properties with resolve activity since this time. + name: active_since + in: query + - schema: + type: string + description: Page size (integer). + required: false + description: Page size (integer). + name: limit + in: query + - schema: + type: string + description: Opaque pagination cursor from a prior `next_cursor`. + required: false + description: Opaque pagination cursor from a prior `next_cursor`. + name: cursor + in: query + responses: + "200": + description: Catalog page + content: + application/json: + schema: + $ref: "#/components/schemas/CatalogBrowseResponse" + /api/registry/catalog/sync: + get: + operationId: syncCatalog + summary: Sync catalog changes since a watermark + description: "Delta sync for local mirrors: returns catalog entries created/updated since `since` (a prior `server_timestamp`), capped at 10,000 per page. Pagination is by the returned `server_timestamp` watermark, distinct from browseCatalog's opaque cursor." + tags: + - Property Catalog + parameters: + - schema: + type: string + description: Watermark from a prior response `server_timestamp` (required). + example: 2026-03-27T10:00:00Z + required: true + description: Watermark from a prior response `server_timestamp` (required). + name: since + in: query + - schema: + type: string + description: Page size, capped at 10,000. + required: false + description: Page size, capped at 10,000. + name: limit + in: query + responses: + "200": + description: Catalog delta + content: + application/json: + schema: + $ref: "#/components/schemas/CatalogSyncResponse" + "400": + description: Missing required `since` parameter + content: + application/json: + schema: + $ref: "#/components/schemas/Error" tags: - name: Onboarding description: Explicitly bootstrap a third-party integration into the AAO registry. Most callers don't need this tag — `POST /api/me/agents` auto-creates the org (for fresh users) and the member profile (for first-time agent registration) without a separate round trip. Use `POST /api/organizations` only when you need to override the auto-derived org name / company_type / revenue_tier. Tier transitions happen via the billing flow only; the Stripe webhook is the sole writer of `organizations.membership_tier`.