@proteos/sdk 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/LICENSE +40 -0
  2. package/dist/chunk-7RGN4E22.cjs +1185 -0
  3. package/dist/chunk-7RGN4E22.cjs.map +1 -0
  4. package/dist/chunk-XJP5WCRZ.js +1125 -0
  5. package/dist/chunk-XJP5WCRZ.js.map +1 -0
  6. package/dist/index.cjs +2384 -0
  7. package/dist/index.cjs.map +1 -0
  8. package/dist/index.d.cts +5225 -0
  9. package/dist/index.d.ts +5225 -0
  10. package/dist/index.js +2146 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/meta/index.cjs +204 -0
  13. package/dist/meta/index.cjs.map +1 -0
  14. package/dist/meta/index.d.cts +2 -0
  15. package/dist/meta/index.d.ts +2 -0
  16. package/dist/meta/index.js +3 -0
  17. package/dist/meta/index.js.map +1 -0
  18. package/dist/types-BNsjfU8N.d.cts +3299 -0
  19. package/dist/types-BNsjfU8N.d.ts +3299 -0
  20. package/package.json +86 -0
  21. package/src/agent/agents.ts +53 -0
  22. package/src/agent/index.ts +134 -0
  23. package/src/agent/mcp-servers.ts +102 -0
  24. package/src/agent/prompts.ts +80 -0
  25. package/src/agent/session-types.ts +397 -0
  26. package/src/agent/sessions.ts +197 -0
  27. package/src/agent/skills.ts +89 -0
  28. package/src/agent/tools.ts +53 -0
  29. package/src/agent/types.ts +362 -0
  30. package/src/auth/index.ts +111 -0
  31. package/src/auth/me.ts +46 -0
  32. package/src/auth/organizations.ts +128 -0
  33. package/src/auth/platform-entities.ts +78 -0
  34. package/src/auth/roles.ts +213 -0
  35. package/src/auth/types.ts +294 -0
  36. package/src/auth/users.ts +226 -0
  37. package/src/client.ts +441 -0
  38. package/src/connector/index.ts +120 -0
  39. package/src/connector/types.ts +150 -0
  40. package/src/conversation/index.ts +297 -0
  41. package/src/conversation/types.ts +590 -0
  42. package/src/conversation/voice.ts +123 -0
  43. package/src/data/index.ts +53 -0
  44. package/src/data/queries.ts +66 -0
  45. package/src/data/records.ts +122 -0
  46. package/src/data/types.ts +89 -0
  47. package/src/errors.ts +148 -0
  48. package/src/events/index.ts +172 -0
  49. package/src/events/types.ts +77 -0
  50. package/src/functions/actions.ts +95 -0
  51. package/src/functions/index.ts +32 -0
  52. package/src/functions/types.ts +71 -0
  53. package/src/http/index.ts +2 -0
  54. package/src/http/query-params.ts +106 -0
  55. package/src/index.ts +598 -0
  56. package/src/iterator.ts +183 -0
  57. package/src/knowledge/graph.ts +35 -0
  58. package/src/knowledge/index.ts +104 -0
  59. package/src/knowledge/labels.ts +70 -0
  60. package/src/knowledge/links.ts +65 -0
  61. package/src/knowledge/nodes.ts +198 -0
  62. package/src/knowledge/record-links.ts +66 -0
  63. package/src/knowledge/types.ts +569 -0
  64. package/src/meta/apps.ts +107 -0
  65. package/src/meta/components.ts +124 -0
  66. package/src/meta/currency/index.ts +202 -0
  67. package/src/meta/entities.ts +193 -0
  68. package/src/meta/filters.ts +76 -0
  69. package/src/meta/index.ts +227 -0
  70. package/src/meta/layout/common-props.ts +93 -0
  71. package/src/meta/layout/control-registry.json +70 -0
  72. package/src/meta/layout/control-registry.ts +92 -0
  73. package/src/meta/layout/elements.ts +203 -0
  74. package/src/meta/layout/index.ts +41 -0
  75. package/src/meta/layout/page-layout.ts +35 -0
  76. package/src/meta/layout/size-value.ts +27 -0
  77. package/src/meta/list-views.ts +109 -0
  78. package/src/meta/lists.ts +104 -0
  79. package/src/meta/menu-configurations.ts +128 -0
  80. package/src/meta/modules.ts +159 -0
  81. package/src/meta/pages.ts +106 -0
  82. package/src/meta/types.ts +1115 -0
  83. package/src/meta/variables.ts +98 -0
  84. package/src/storage/files.ts +183 -0
  85. package/src/storage/index.ts +33 -0
  86. package/src/storage/types.ts +70 -0
  87. package/src/types/common.ts +143 -0
  88. package/src/types/index.ts +28 -0
  89. package/src/types/options.ts +95 -0
  90. package/src/workflow/executions.ts +99 -0
  91. package/src/workflow/index.ts +109 -0
  92. package/src/workflow/node-types.ts +50 -0
  93. package/src/workflow/types.ts +658 -0
  94. package/src/workflow/workflows.ts +152 -0
@@ -0,0 +1,53 @@
1
+ import type { ProteosClient } from '../client.js'
2
+ import { type QueryService, QueryServiceImpl } from './queries.js'
3
+ import { type RecordService, RecordServiceImpl } from './records.js'
4
+
5
+ /**
6
+ * Facade for the data-service (records + raw-SQL query API).
7
+ *
8
+ * Usage:
9
+ * ```ts
10
+ * const client = new ProteosClient({ baseUrl, tokenProvider });
11
+ * const data = new DataClient(client);
12
+ * const page = await data.records.listPage('customer');
13
+ * const result = await data.queries.execute('SELECT id, stage FROM opportunity');
14
+ * ```
15
+ */
16
+ export class DataClient {
17
+ readonly records: RecordService
18
+ readonly queries: QueryService
19
+
20
+ constructor(client: ProteosClient) {
21
+ this.records = new RecordServiceImpl(client)
22
+ this.queries = new QueryServiceImpl(client)
23
+ }
24
+ }
25
+
26
+ // Re-export service interfaces
27
+ // Re-export types
28
+ export type {
29
+ QueryExecuteMeta,
30
+ QueryExecuteResponse,
31
+ QueryRow,
32
+ QueryService,
33
+ QueryValidateMeta,
34
+ QueryValidateResponse,
35
+ } from './queries.js'
36
+ export type { RecordService } from './records.js'
37
+ export type {
38
+ BatchTransactionError,
39
+ BatchTransactionStatus,
40
+ BatchUpsertRecordsResponse,
41
+ BatchUpsertTransaction,
42
+ BatchUpsertTransactionResult,
43
+ ListRecordsOptions,
44
+ RecordData,
45
+ } from './types.js'
46
+
47
+ // Re-export Zod schemas
48
+ export {
49
+ BatchTransactionErrorSchema,
50
+ BatchUpsertRecordsResponseSchema,
51
+ BatchUpsertTransactionResultSchema,
52
+ BatchUpsertTransactionSchema,
53
+ } from './types.js'
@@ -0,0 +1,66 @@
1
+ import type { ProteosClient } from '../client.js'
2
+
3
+ const QUERY_BASE_PATH = '/data/v1/query'
4
+
5
+ /**
6
+ * A single row from a query result. Keys match the result-set columns
7
+ * (camelCase, with `id`/`created_at`/`updated_at` reserved). The data-service
8
+ * hoists JSONB columns into nested objects, so values can be primitives,
9
+ * nested records, or arrays.
10
+ */
11
+ export type QueryRow = Record<string, unknown>
12
+
13
+ export interface QueryExecuteMeta {
14
+ columns: string[]
15
+ items: number
16
+ limit_applied: number
17
+ /** Only present when `true`. */
18
+ was_default_limit_applied?: boolean
19
+ execution_time_ms: number
20
+ }
21
+
22
+ export interface QueryExecuteResponse {
23
+ data: QueryRow[]
24
+ meta?: QueryExecuteMeta
25
+ }
26
+
27
+ export interface QueryValidateMeta {
28
+ limit_applied: number
29
+ was_default_limit_applied?: boolean
30
+ }
31
+
32
+ export interface QueryValidateResponse {
33
+ valid: boolean
34
+ /** The SQL the server would execute (entity-rewritten, schema-qualified). */
35
+ rewritten_sql?: string
36
+ tables?: string[]
37
+ meta?: QueryValidateMeta
38
+ }
39
+
40
+ /**
41
+ * Service for the raw-SQL query endpoint of the data-service.
42
+ *
43
+ * Accepts SELECT-only Postgres SQL with bare attribute references; the
44
+ * server rewrites bare attributes into JSONB accessors against the
45
+ * tenant's schema and runs per-table OpenFGA authorization checks.
46
+ */
47
+ export interface QueryService {
48
+ /** Executes the SQL and returns the result rows + meta. */
49
+ execute(sql: string): Promise<QueryExecuteResponse>
50
+ /** Parses + authorizes the SQL without running it. */
51
+ validate(sql: string): Promise<QueryValidateResponse>
52
+ }
53
+
54
+ export class QueryServiceImpl implements QueryService {
55
+ constructor(private readonly client: ProteosClient) {}
56
+
57
+ async execute(sql: string): Promise<QueryExecuteResponse> {
58
+ return this.client.request<QueryExecuteResponse>('POST', `${QUERY_BASE_PATH}/execute`, { sql })
59
+ }
60
+
61
+ async validate(sql: string): Promise<QueryValidateResponse> {
62
+ return this.client.request<QueryValidateResponse>('POST', `${QUERY_BASE_PATH}/validate`, {
63
+ sql,
64
+ })
65
+ }
66
+ }
@@ -0,0 +1,122 @@
1
+ import type { ProteosClient } from '../client.js'
2
+ import { PageIterator } from '../iterator.js'
3
+ import type { ListResult } from '../types/common.js'
4
+ import type {
5
+ BatchUpsertRecordsResponse,
6
+ BatchUpsertTransaction,
7
+ ListRecordsOptions,
8
+ RecordData,
9
+ } from './types.js'
10
+
11
+ const RECORDS_BASE_PATH = '/data/v1/records'
12
+ const BATCH_RECORDS_BASE_PATH = '/data/v1/batch/records'
13
+
14
+ /**
15
+ * Service for managing records (per-entity data rows) via the data-service.
16
+ *
17
+ * Records are opaque JSON objects whose schema is defined per-entity in the
18
+ * metadata service. An entity must be registered (via metadata-service, which
19
+ * in turn provisions a Postgres table) before records for it can be created.
20
+ */
21
+ export interface RecordService {
22
+ /**
23
+ * Lists records for an entity as an async iterator. Pages are 0-indexed
24
+ * (first page is 0); `PageIterator` starts there automatically.
25
+ */
26
+ list(
27
+ entitySlug: string,
28
+ options?: ListRecordsOptions,
29
+ ): PageIterator<RecordData, ListRecordsOptions>
30
+
31
+ /**
32
+ * Fetches a single page of records.
33
+ */
34
+ listPage(entitySlug: string, options?: ListRecordsOptions): Promise<ListResult<RecordData>>
35
+
36
+ /**
37
+ * Gets a single record by id.
38
+ * @throws {ProteosError} If the record is not found (404).
39
+ */
40
+ get(entitySlug: string, id: string): Promise<RecordData>
41
+
42
+ /**
43
+ * Creates a new record for the given entity.
44
+ */
45
+ create(entitySlug: string, data: RecordData): Promise<RecordData>
46
+
47
+ /**
48
+ * Partially updates a record.
49
+ */
50
+ update(entitySlug: string, id: string, data: Partial<RecordData>): Promise<RecordData>
51
+
52
+ /**
53
+ * Deletes a record.
54
+ */
55
+ delete(entitySlug: string, id: string): Promise<void>
56
+
57
+ /**
58
+ * Batch-upserts records. Each transaction succeeds/fails independently;
59
+ * the response contains a per-transaction status so callers (e.g. import
60
+ * flows) can render per-row feedback.
61
+ */
62
+ batchUpsert(
63
+ entitySlug: string,
64
+ transactions: BatchUpsertTransaction[],
65
+ ): Promise<BatchUpsertRecordsResponse>
66
+ }
67
+
68
+ /**
69
+ * Implementation of RecordService.
70
+ */
71
+ export class RecordServiceImpl implements RecordService {
72
+ constructor(private readonly client: ProteosClient) {}
73
+
74
+ list(
75
+ entitySlug: string,
76
+ options: ListRecordsOptions = {},
77
+ ): PageIterator<RecordData, ListRecordsOptions> {
78
+ return new PageIterator((opts) => this.listPage(entitySlug, opts), options)
79
+ }
80
+
81
+ async listPage(
82
+ entitySlug: string,
83
+ options: ListRecordsOptions = {},
84
+ ): Promise<ListResult<RecordData>> {
85
+ return this.client.requestWithQuery<ListResult<RecordData>>(
86
+ 'GET',
87
+ `${RECORDS_BASE_PATH}/${entitySlug}`,
88
+ options,
89
+ )
90
+ }
91
+
92
+ async get(entitySlug: string, id: string): Promise<RecordData> {
93
+ return this.client.request<RecordData>('GET', `${RECORDS_BASE_PATH}/${entitySlug}/${id}`)
94
+ }
95
+
96
+ async create(entitySlug: string, data: RecordData): Promise<RecordData> {
97
+ return this.client.request<RecordData>('POST', `${RECORDS_BASE_PATH}/${entitySlug}`, data)
98
+ }
99
+
100
+ async update(entitySlug: string, id: string, data: Partial<RecordData>): Promise<RecordData> {
101
+ return this.client.request<RecordData>(
102
+ 'PATCH',
103
+ `${RECORDS_BASE_PATH}/${entitySlug}/${id}`,
104
+ data,
105
+ )
106
+ }
107
+
108
+ async delete(entitySlug: string, id: string): Promise<void> {
109
+ await this.client.request<void>('DELETE', `${RECORDS_BASE_PATH}/${entitySlug}/${id}`)
110
+ }
111
+
112
+ async batchUpsert(
113
+ entitySlug: string,
114
+ transactions: BatchUpsertTransaction[],
115
+ ): Promise<BatchUpsertRecordsResponse> {
116
+ return this.client.request<BatchUpsertRecordsResponse>(
117
+ 'POST',
118
+ `${BATCH_RECORDS_BASE_PATH}/${entitySlug}/upsert`,
119
+ transactions,
120
+ )
121
+ }
122
+ }
@@ -0,0 +1,89 @@
1
+ import { z } from 'zod'
2
+
3
+ /**
4
+ * A record is an open-shape JSON object whose keys are the entity's attribute
5
+ * names plus system fields (`id`, `created_at`, `updated_at`, `created_by`,
6
+ * `updated_by`). Because attributes vary per entity, values are typed as `any`
7
+ * at the SDK layer — consumers narrow or cast as needed.
8
+ */
9
+ export type RecordData = Record<string, any>
10
+
11
+ /**
12
+ * Options for listing records.
13
+ *
14
+ * Sorting: use `sort: "field:asc"` for single-column; pipe-separated for
15
+ * multi-column (`"field1:asc|field2:desc"`). Defaults to `created_at:desc`.
16
+ *
17
+ * Arbitrary filter keys pass through as query params. Supported operators
18
+ * (via bracket syntax) include `[eq]`, `[ne]`, `[gt]`, `[gte]`, `[lt]`,
19
+ * `[lte]`, `[in]` (pipe-separated), `[not_in]`, `[contains]`, `[starts_with]`,
20
+ * `[ends_with]`, `[empty]`, `[not_empty]`. Missing operator defaults to `[eq]`.
21
+ */
22
+ export interface ListRecordsOptions {
23
+ /** Page number (0-indexed). First page is 0. */
24
+ page?: number
25
+ /** Number of items per page (default: 20 per backend) */
26
+ page_size?: number
27
+ /**
28
+ * Sort spec. Format: `"field:direction"`, or pipe-separated for multi-column:
29
+ * `"field1:asc|field2:desc"`. Direction is `asc` | `desc`.
30
+ */
31
+ sort?: string
32
+ /** Arbitrary attribute filters (flat query-string params). */
33
+ [key: string]: unknown
34
+ }
35
+
36
+ // ============================================================================
37
+ // Batch operations
38
+ // ============================================================================
39
+
40
+ /**
41
+ * One transaction within a batch operation.
42
+ * `transaction_id` is client-supplied so you can match results back to your
43
+ * source rows (used e.g. by import flows).
44
+ */
45
+ export interface BatchUpsertTransaction {
46
+ transaction_id: string
47
+ data: Record<string, unknown>
48
+ }
49
+
50
+ export type BatchTransactionStatus = 'success' | 'error'
51
+
52
+ export interface BatchTransactionError {
53
+ code: string
54
+ message: string
55
+ }
56
+
57
+ export interface BatchUpsertTransactionResult {
58
+ transaction_id: string
59
+ status: BatchTransactionStatus
60
+ record?: RecordData
61
+ error?: BatchTransactionError
62
+ }
63
+
64
+ export interface BatchUpsertRecordsResponse {
65
+ results: BatchUpsertTransactionResult[]
66
+ }
67
+
68
+ // Zod schemas for runtime validation (optional, for consumers who want it)
69
+
70
+ export const BatchUpsertTransactionSchema = z.object({
71
+ transaction_id: z.string(),
72
+ data: z.record(z.unknown()),
73
+ })
74
+
75
+ export const BatchTransactionErrorSchema = z.object({
76
+ code: z.string(),
77
+ message: z.string(),
78
+ })
79
+
80
+ export const BatchUpsertTransactionResultSchema = z.object({
81
+ transaction_id: z.string(),
82
+ status: z.enum(['success', 'error']),
83
+ record: z.record(z.unknown()).optional(),
84
+ error: BatchTransactionErrorSchema.optional(),
85
+ })
86
+
87
+ export const BatchUpsertRecordsResponseSchema = z.object({
88
+ results: z.array(BatchUpsertTransactionResultSchema),
89
+ })
package/src/errors.ts ADDED
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Common error codes returned by the Proteos API.
3
+ */
4
+ export const ErrorCode = {
5
+ NOT_FOUND: 'not_found',
6
+ UNAUTHORIZED: 'unauthorized',
7
+ FORBIDDEN: 'forbidden',
8
+ BAD_REQUEST: 'bad_request',
9
+ CONFLICT: 'conflict',
10
+ INTERNAL_SERVER_ERROR: 'internal_server_error',
11
+ INVALID_PAYLOAD: 'invalid_payload',
12
+ INVALID_PERSONA: 'invalid_persona',
13
+ } as const
14
+
15
+ export type ErrorCodeType = (typeof ErrorCode)[keyof typeof ErrorCode]
16
+
17
+ /**
18
+ * Error returned by the Proteos API.
19
+ * Contains HTTP status code, error code, and human-readable message.
20
+ */
21
+ export class ProteosError extends Error {
22
+ /** HTTP status code of the response */
23
+ readonly httpStatus: number
24
+ /** API error code (e.g., 'not_found', 'unauthorized') */
25
+ readonly code: ErrorCodeType | string
26
+
27
+ constructor(message: string, httpStatus: number, code: ErrorCodeType | string) {
28
+ super(message)
29
+ this.name = 'ProteosError'
30
+ this.httpStatus = httpStatus
31
+ this.code = code
32
+
33
+ // V8-specific API for cleaner stack traces; not present in all runtimes
34
+ const v8Capture = (Error as unknown as { captureStackTrace?: (t: object, c: unknown) => void })
35
+ .captureStackTrace
36
+ if (v8Capture) v8Capture(this, this.constructor)
37
+ }
38
+
39
+ /**
40
+ * Returns a formatted string representation of the error.
41
+ */
42
+ override toString(): string {
43
+ return `${this.code}: ${this.message} (HTTP ${this.httpStatus})`
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Type guard to check if an error is a ProteosError.
49
+ */
50
+ export function isProteosError(error: unknown): error is ProteosError {
51
+ return error instanceof ProteosError
52
+ }
53
+
54
+ /**
55
+ * Type guard to check if an error is a 404 Not Found error.
56
+ */
57
+ export function isNotFound(error: unknown): error is ProteosError {
58
+ return isProteosError(error) && error.httpStatus === 404
59
+ }
60
+
61
+ /**
62
+ * Type guard to check if an error is a 401 Unauthorized error.
63
+ */
64
+ export function isUnauthorized(error: unknown): error is ProteosError {
65
+ return isProteosError(error) && error.httpStatus === 401
66
+ }
67
+
68
+ /**
69
+ * Type guard to check if an error is a 403 Forbidden error.
70
+ */
71
+ export function isForbidden(error: unknown): error is ProteosError {
72
+ return isProteosError(error) && error.httpStatus === 403
73
+ }
74
+
75
+ /**
76
+ * Type guard to check if an error is a 400 Bad Request error.
77
+ */
78
+ export function isBadRequest(error: unknown): error is ProteosError {
79
+ return isProteosError(error) && error.httpStatus === 400
80
+ }
81
+
82
+ /**
83
+ * Type guard to check if an error is a 409 Conflict error.
84
+ */
85
+ export function isConflict(error: unknown): error is ProteosError {
86
+ return isProteosError(error) && error.httpStatus === 409
87
+ }
88
+
89
+ /**
90
+ * Maps HTTP status codes to default error codes.
91
+ */
92
+ export function getDefaultErrorCode(httpStatus: number): ErrorCodeType | string {
93
+ switch (httpStatus) {
94
+ case 400:
95
+ return ErrorCode.BAD_REQUEST
96
+ case 401:
97
+ return ErrorCode.UNAUTHORIZED
98
+ case 403:
99
+ return ErrorCode.FORBIDDEN
100
+ case 404:
101
+ return ErrorCode.NOT_FOUND
102
+ case 409:
103
+ return ErrorCode.CONFLICT
104
+ default:
105
+ return ErrorCode.INTERNAL_SERVER_ERROR
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Response shape for API error responses.
111
+ */
112
+ export interface ApiErrorResponse {
113
+ code?: string
114
+ message?: string
115
+ }
116
+
117
+ /**
118
+ * Parses an error response from the API and creates a ProteosError.
119
+ */
120
+ export async function parseErrorResponse(response: Response): Promise<ProteosError> {
121
+ const httpStatus = response.status
122
+ let code: string = getDefaultErrorCode(httpStatus)
123
+ let message = 'Unknown error'
124
+
125
+ try {
126
+ const body = await response.text()
127
+
128
+ // Try to parse as JSON
129
+ try {
130
+ const json = JSON.parse(body) as ApiErrorResponse
131
+ if (json.code) {
132
+ code = json.code
133
+ }
134
+ if (json.message) {
135
+ message = json.message
136
+ } else {
137
+ message = body || `HTTP ${httpStatus}`
138
+ }
139
+ } catch {
140
+ // Not JSON, use body as message
141
+ message = body.trim() || `HTTP ${httpStatus}`
142
+ }
143
+ } catch {
144
+ message = `HTTP ${httpStatus}`
145
+ }
146
+
147
+ return new ProteosError(message, httpStatus, code)
148
+ }
@@ -0,0 +1,172 @@
1
+ import type { ProteosClient } from '../client.js'
2
+ import type {
3
+ ListEventsOptions,
4
+ PlatformEvent,
5
+ PublishEventRequest,
6
+ RedriveResult,
7
+ TailOptions,
8
+ Topic,
9
+ } from './types.js'
10
+
11
+ const EVENTS_BASE_PATH = '/events/v1'
12
+
13
+ // A live tail is a long-poll; override the client's default request timeout so
14
+ // it isn't aborted mid-stream. ~24h is well under the setTimeout overflow bound
15
+ // and acts as a practical cap on a single tail session. Real teardown is the
16
+ // caller's AbortSignal.
17
+ const TAIL_TIMEOUT_MS = 24 * 60 * 60 * 1000
18
+
19
+ /**
20
+ * Facade for event-service: a read/admin view over the Redis-Streams bus.
21
+ * Everything is topic-centric, so the surface lives on `topics`.
22
+ *
23
+ * ```ts
24
+ * const events = new EventsClient(client)
25
+ * const topics = await events.topics.list()
26
+ * const recent = await events.topics.events('record.contact.events', { limit: 50 })
27
+ * for await (const msg of events.topics.tail('record.contact.events', { signal })) {
28
+ * // append msg
29
+ * }
30
+ * ```
31
+ */
32
+ export class EventsClient {
33
+ readonly topics: TopicService
34
+
35
+ constructor(client: ProteosClient) {
36
+ this.topics = new TopicServiceImpl(client)
37
+ }
38
+ }
39
+
40
+ /** Topic discovery, event reads + live tail, and admin actions. */
41
+ export interface TopicService {
42
+ /** List the caller-org's topics. */
43
+ list(): Promise<Topic[]>
44
+ /** Read the most-recent events on a topic (newest first). */
45
+ events(topic: string, options?: ListEventsOptions): Promise<PlatformEvent[]>
46
+ /**
47
+ * Live-tail new events on a topic until the AbortSignal fires. Yields each
48
+ * event as it arrives (chunked NDJSON under the hood).
49
+ */
50
+ tail(topic: string, options?: TailOptions): AsyncGenerator<PlatformEvent, void, unknown>
51
+ /** Publish a test event onto a topic — delivered to real consumers. */
52
+ publish(topic: string, request: PublishEventRequest): Promise<PlatformEvent>
53
+ /** Move a dead-letter stream back to its source topic for re-processing. */
54
+ redrive(topic: string): Promise<RedriveResult>
55
+ /** Empty a topic's stream (keeps consumer groups). Destructive. */
56
+ purge(topic: string): Promise<void>
57
+ }
58
+
59
+ class TopicServiceImpl implements TopicService {
60
+ constructor(private readonly client: ProteosClient) {}
61
+
62
+ async list(): Promise<Topic[]> {
63
+ const response = await this.client.request<{ data: Topic[] }>(
64
+ 'GET',
65
+ `${EVENTS_BASE_PATH}/topics`,
66
+ )
67
+ return response.data ?? []
68
+ }
69
+
70
+ async events(topic: string, options: ListEventsOptions = {}): Promise<PlatformEvent[]> {
71
+ const response = await this.client.requestWithQuery<{ data: PlatformEvent[] }, ListEventsOptions>(
72
+ 'GET',
73
+ `${EVENTS_BASE_PATH}/topics/${encodeURIComponent(topic)}/events`,
74
+ options,
75
+ )
76
+ return response.data ?? []
77
+ }
78
+
79
+ async *tail(topic: string, options: TailOptions = {}): AsyncGenerator<PlatformEvent, void, unknown> {
80
+ const path = `${EVENTS_BASE_PATH}/topics/${encodeURIComponent(topic)}/events?follow=true`
81
+ let body: ReadableStream<Uint8Array> | null
82
+ try {
83
+ ;({ body } = await this.client.requestRaw('GET', path, undefined, {
84
+ timeout: TAIL_TIMEOUT_MS,
85
+ ...(options.signal ? { signal: options.signal } : {}),
86
+ headers: { Accept: 'application/x-ndjson' },
87
+ }))
88
+ } catch (error) {
89
+ if (isAbort(error)) return
90
+ throw error
91
+ }
92
+ if (!body) return
93
+
94
+ const reader = body.getReader()
95
+ const decoder = new TextDecoder()
96
+ let buffer = ''
97
+ try {
98
+ while (true) {
99
+ const { done, value } = await reader.read()
100
+ if (done) break
101
+ buffer += decoder.decode(value, { stream: true })
102
+ let newlineIndex = buffer.indexOf('\n')
103
+ while (newlineIndex !== -1) {
104
+ const line = buffer.slice(0, newlineIndex).trim()
105
+ buffer = buffer.slice(newlineIndex + 1)
106
+ const event = parseLine(line)
107
+ if (event) yield event
108
+ newlineIndex = buffer.indexOf('\n')
109
+ }
110
+ }
111
+ const remainder = parseLine(buffer.trim())
112
+ if (remainder) yield remainder
113
+ } catch (error) {
114
+ if (!isAbort(error)) throw error
115
+ } finally {
116
+ reader.releaseLock()
117
+ void body.cancel().catch(() => {})
118
+ }
119
+ }
120
+
121
+ async publish(topic: string, request: PublishEventRequest): Promise<PlatformEvent> {
122
+ return this.client.request<PlatformEvent>(
123
+ 'POST',
124
+ `${EVENTS_BASE_PATH}/topics/${encodeURIComponent(topic)}/events`,
125
+ request,
126
+ )
127
+ }
128
+
129
+ async redrive(topic: string): Promise<RedriveResult> {
130
+ return this.client.request<RedriveResult>(
131
+ 'POST',
132
+ `${EVENTS_BASE_PATH}/topics/${encodeURIComponent(topic)}/redrive`,
133
+ )
134
+ }
135
+
136
+ async purge(topic: string): Promise<void> {
137
+ await this.client.request<void>(
138
+ 'POST',
139
+ `${EVENTS_BASE_PATH}/topics/${encodeURIComponent(topic)}/purge`,
140
+ )
141
+ }
142
+ }
143
+
144
+ /** True when an error is an AbortError (the tail's normal stop path). */
145
+ function isAbort(error: unknown): boolean {
146
+ return error instanceof DOMException && error.name === 'AbortError'
147
+ }
148
+
149
+ /**
150
+ * Parses one NDJSON line into a PlatformEvent, returning null for a blank or
151
+ * unparseable line. A single bad frame should be skipped, not tear down the
152
+ * whole tail.
153
+ */
154
+ function parseLine(line: string): PlatformEvent | null {
155
+ if (!line) return null
156
+ try {
157
+ return JSON.parse(line) as PlatformEvent
158
+ } catch {
159
+ return null
160
+ }
161
+ }
162
+
163
+ export type {
164
+ ConsumerGroup,
165
+ ListEventsOptions,
166
+ PlatformEvent,
167
+ PublishEventRequest,
168
+ RedriveResult,
169
+ TailOptions,
170
+ Topic,
171
+ TopicKind,
172
+ } from './types.js'