@anhora/sdk 0.1.0 → 0.3.0

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.
package/README.md CHANGED
@@ -1,30 +1,48 @@
1
1
  # @anhora/sdk
2
2
 
3
- Framework-agnostic Anhora API client: connectors ingest/sync, content upsert, deployment runtime, embed release config.
4
-
5
- ```bash
6
- npm install @anhora/sdk
7
- ```
3
+ Server SDK for Anhora deployments and universal integration ingest. Connector
4
+ writes use a stable integration id, namespace, idempotent delta events, and
5
+ atomic paged snapshots.
8
6
 
9
7
  ```ts
10
- import { createAnhora } from '@anhora/sdk'
8
+ import { createAnhora } from "@anhora/sdk";
11
9
 
12
10
  const anhora = createAnhora({
13
- apiBase: 'https://api.anhora.net/api',
11
+ apiBase: process.env.ANHORA_API_BASE,
14
12
  ingestSecret: process.env.ANHORA_INGEST_SECRET,
15
- })
13
+ });
16
14
 
17
15
  await anhora.content.upsert({
18
- widgetId: '…',
19
- source: 'payload',
20
- externalId: 'page-1',
21
- title: 'Home',
22
- content: '…',
23
- })
24
-
25
- await anhora.connectors.sync({ widgetId: '…' })
26
- await anhora.deployments.getRuntime(deploymentKey, { origin: 'https://example.com' })
27
- await anhora.embed.getWidgetRelease('stable')
16
+ installationId: process.env.ANHORA_INTEGRATION_ID!,
17
+ namespace: "my_cms.content",
18
+ eventId: `page:${page.id}:${page.version}`,
19
+ document: {
20
+ externalId: page.id,
21
+ title: page.title,
22
+ content: page.plainText,
23
+ url: page.url,
24
+ sourceSequence: page.version,
25
+ sourceUpdatedAt: page.updatedAt.toISOString(),
26
+ },
27
+ });
28
28
  ```
29
29
 
30
- Docs: [anhora.net](https://anhora.net) · Payload plugin: [`@anhora/payload`](https://www.npmjs.com/package/@anhora/payload) · Next: [`@anhora/next`](https://www.npmjs.com/package/@anhora/next)
30
+ Deletes are first-class tombstones:
31
+
32
+ ```ts
33
+ await anhora.content.delete({
34
+ installationId,
35
+ namespace: "my_cms.content",
36
+ externalId: page.id,
37
+ eventId: `page:${page.id}:deleted:${page.version}`,
38
+ sourceSequence: page.version,
39
+ });
40
+ ```
41
+
42
+ Use `content.replaceAll()` for a bounded in-memory source. For large or streamed
43
+ sources, use `connectors.replaceSnapshot()` with pages of at most 500 items.
44
+ An empty snapshot is valid and removes the previous contents of that namespace.
45
+
46
+ The ingest secret is server-only. The integration id selects ownership; a widget
47
+ id is not part of the V2 write contract. See
48
+ [`docs/sdk-backend-integration.md`](../../docs/sdk-backend-integration.md).
package/dist/client.d.ts CHANGED
@@ -21,14 +21,14 @@ export type AnhoraClient = {
21
21
  * })
22
22
  *
23
23
  * await anhora.content.upsert({
24
- * widgetId: '...',
25
- * source: 'payload',
26
- * externalId: page.id,
27
- * title: page.title,
28
- * content: plainText,
24
+ * installationId: '...',
25
+ * namespace: 'payload.pages',
26
+ * document: {
27
+ * externalId: page.id,
28
+ * title: page.title,
29
+ * content: plainText,
30
+ * },
29
31
  * })
30
- *
31
- * await anhora.connectors.sync({ widgetId: '...' })
32
32
  * await anhora.deployments.getRuntime(deploymentKey, { origin })
33
33
  * await anhora.embed.getWidgetRelease('stable')
34
34
  * ```
package/dist/client.js CHANGED
@@ -16,14 +16,14 @@ const http_1 = require("./http");
16
16
  * })
17
17
  *
18
18
  * await anhora.content.upsert({
19
- * widgetId: '...',
20
- * source: 'payload',
21
- * externalId: page.id,
22
- * title: page.title,
23
- * content: plainText,
19
+ * installationId: '...',
20
+ * namespace: 'payload.pages',
21
+ * document: {
22
+ * externalId: page.id,
23
+ * title: page.title,
24
+ * content: plainText,
25
+ * },
24
26
  * })
25
- *
26
- * await anhora.connectors.sync({ widgetId: '...' })
27
27
  * await anhora.deployments.getRuntime(deploymentKey, { origin })
28
28
  * await anhora.embed.getWidgetRelease('stable')
29
29
  * ```
@@ -1,54 +1,90 @@
1
- import type { ConnectorIngestRequest, ConnectorIngestResponse, ConnectorSyncRequest, IngestCatalogItem, IngestKnowledgeItem } from '@anhora/shared-types';
1
+ import type { BeginConnectorSnapshotRequest, ConnectorEntityType, ConnectorEvent, ConnectorEventsAcceptedResponse, ConnectorSnapshotPageRequest, ConnectorDocumentMetadata } from '@anhora/shared-types';
2
2
  import type { AnhoraHttp } from './http';
3
- export type ConnectorSyncResponse = {
3
+ export type BeginSnapshotResponse = {
4
4
  ok: true;
5
- widgetId: string;
6
- source: string;
7
- results: Record<string, unknown>;
5
+ runId: string;
6
+ status: 'receiving';
7
+ maxItemsPerPage: number;
8
+ startedAt: string;
8
9
  };
9
- export type ContentUpsertInput = {
10
- widgetId: string;
11
- /** Source namespace for external ids, e.g. `payload`, `wordpress`. */
12
- source: string;
10
+ export type SnapshotPageResponse = {
11
+ ok: true;
12
+ runId: string;
13
+ page: number;
14
+ accepted: number;
15
+ duplicate: boolean;
16
+ checksum: string;
17
+ };
18
+ export type CommitSnapshotResponse = {
19
+ ok: true;
20
+ runId: string;
21
+ status: 'completed';
22
+ pages: number;
23
+ items: number;
24
+ };
25
+ export type ContentDocumentInput = {
13
26
  externalId: string;
14
27
  title: string;
15
28
  content: string;
16
29
  url?: string;
17
- /** Replace prior knowledge with this source prefix. */
18
- replace?: boolean;
19
- };
20
- export type ContentUpsertBatchInput = {
21
- widgetId: string;
22
- source: string;
23
- items: Array<{
24
- externalId: string;
25
- title: string;
26
- content: string;
27
- url?: string;
28
- }>;
29
- replace?: boolean;
30
+ locale?: string;
31
+ sourceVersion?: string;
32
+ sourceSequence?: number;
33
+ sourceUpdatedAt?: string;
34
+ document?: ConnectorDocumentMetadata;
30
35
  };
31
36
  export declare class AnhoraConnectorsApi {
32
37
  private readonly http;
33
38
  constructor(http: AnhoraHttp);
34
- ingest(body: ConnectorIngestRequest): Promise<ConnectorIngestResponse>;
35
- sync(body: ConnectorSyncRequest): Promise<ConnectorSyncResponse>;
36
- ingestCatalog(input: {
37
- widgetId: string;
38
- items: IngestCatalogItem[];
39
- replace?: boolean;
40
- }): Promise<ConnectorIngestResponse>;
41
- ingestKnowledge(input: {
42
- widgetId: string;
43
- items: IngestKnowledgeItem[];
44
- replace?: boolean;
45
- knowledgeExternalIdPrefix?: string;
46
- }): Promise<ConnectorIngestResponse>;
39
+ events(input: {
40
+ installationId: string;
41
+ namespace: string;
42
+ events: ConnectorEvent[];
43
+ }): Promise<ConnectorEventsAcceptedResponse>;
44
+ beginSnapshot(installationId: string, input: BeginConnectorSnapshotRequest): Promise<BeginSnapshotResponse>;
45
+ uploadSnapshotPage(installationId: string, runId: string, page: number, input: ConnectorSnapshotPageRequest): Promise<SnapshotPageResponse>;
46
+ commitSnapshot(installationId: string, runId: string, expectedPages: number): Promise<CommitSnapshotResponse>;
47
+ abortSnapshot(installationId: string, runId: string): Promise<void>;
48
+ replaceSnapshot(input: {
49
+ installationId: string;
50
+ namespace: string;
51
+ entityType: ConnectorEntityType;
52
+ /** Synchronous ownership/cancellation guard before each new begin/page/commit request. */
53
+ beforeRequest?: () => void;
54
+ pages: Iterable<ConnectorSnapshotPageRequest> | AsyncIterable<ConnectorSnapshotPageRequest>;
55
+ }): Promise<CommitSnapshotResponse>;
47
56
  }
48
57
  export declare class AnhoraContentApi {
49
58
  private readonly connectors;
50
59
  constructor(connectors: AnhoraConnectorsApi);
51
- /** Convenience upsert for a single CMS/document knowledge row. */
52
- upsert(input: ContentUpsertInput): Promise<ConnectorIngestResponse>;
53
- upsertMany(input: ContentUpsertBatchInput): Promise<ConnectorIngestResponse>;
60
+ upsert(input: {
61
+ installationId: string;
62
+ namespace: string;
63
+ document: ContentDocumentInput;
64
+ eventId?: string;
65
+ entityType?: 'knowledge_document' | 'content_document';
66
+ }): Promise<ConnectorEventsAcceptedResponse>;
67
+ delete(input: {
68
+ installationId: string;
69
+ namespace: string;
70
+ externalId: string;
71
+ eventId: string;
72
+ entityType?: 'knowledge_document' | 'content_document';
73
+ sourceSequence?: number;
74
+ sourceUpdatedAt?: string;
75
+ }): Promise<ConnectorEventsAcceptedResponse>;
76
+ upsertMany(input: {
77
+ installationId: string;
78
+ namespace: string;
79
+ documents: ContentDocumentInput[];
80
+ eventIdFor: (document: ContentDocumentInput) => string;
81
+ entityType?: 'knowledge_document' | 'content_document';
82
+ }): Promise<ConnectorEventsAcceptedResponse>;
83
+ replaceAll(input: {
84
+ installationId: string;
85
+ namespace: string;
86
+ documents: ContentDocumentInput[];
87
+ entityType?: 'knowledge_document' | 'content_document';
88
+ pageSize?: number;
89
+ }): Promise<CommitSnapshotResponse>;
54
90
  }
@@ -1,89 +1,154 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AnhoraContentApi = exports.AnhoraConnectorsApi = void 0;
4
- function withSourcePrefix(source, externalId) {
5
- const trimmedSource = source.trim().replace(/:+$/u, '');
6
- const trimmedId = externalId.trim();
7
- if (!trimmedSource)
8
- return trimmedId;
9
- if (trimmedId.startsWith(`${trimmedSource}:`))
10
- return trimmedId;
11
- return `${trimmedSource}:${trimmedId}`;
12
- }
13
4
  class AnhoraConnectorsApi {
14
5
  http;
15
6
  constructor(http) {
16
7
  this.http = http;
17
8
  }
18
- ingest(body) {
19
- return this.http.request('/v1/connectors/ingest', {
9
+ events(input) {
10
+ const id = encodeURIComponent(input.installationId.trim());
11
+ return this.http.request(`/v2/integrations/${id}/events`, {
20
12
  method: 'POST',
21
- body,
22
13
  auth: 'ingest',
14
+ body: {
15
+ schemaVersion: 2,
16
+ namespace: input.namespace,
17
+ events: input.events,
18
+ },
23
19
  });
24
20
  }
25
- sync(body) {
26
- return this.http.request('/v1/connectors/sync', {
27
- method: 'POST',
28
- body,
29
- auth: 'ingest',
30
- });
21
+ beginSnapshot(installationId, input) {
22
+ const id = encodeURIComponent(installationId.trim());
23
+ return this.http.request(`/v2/integrations/${id}/snapshots`, { method: 'POST', auth: 'ingest', body: input });
31
24
  }
32
- ingestCatalog(input) {
33
- return this.ingest({
34
- widgetId: input.widgetId,
35
- catalog: input.items,
36
- replace: input.replace ? { catalog: true } : undefined,
37
- });
25
+ uploadSnapshotPage(installationId, runId, page, input) {
26
+ const installation = encodeURIComponent(installationId.trim());
27
+ const run = encodeURIComponent(runId.trim());
28
+ return this.http.request(`/v2/integrations/${installation}/snapshots/${run}/pages/${page}`, { method: 'PUT', auth: 'ingest', body: input });
29
+ }
30
+ commitSnapshot(installationId, runId, expectedPages) {
31
+ const installation = encodeURIComponent(installationId.trim());
32
+ const run = encodeURIComponent(runId.trim());
33
+ return this.http.request(`/v2/integrations/${installation}/snapshots/${run}/commit`, { method: 'POST', auth: 'ingest', body: { expectedPages } });
38
34
  }
39
- ingestKnowledge(input) {
40
- return this.ingest({
41
- widgetId: input.widgetId,
42
- knowledge: input.items,
43
- replace: input.replace
44
- ? {
45
- knowledge: true,
46
- knowledgeExternalIdPrefix: input.knowledgeExternalIdPrefix,
47
- }
48
- : undefined,
35
+ abortSnapshot(installationId, runId) {
36
+ const installation = encodeURIComponent(installationId.trim());
37
+ const run = encodeURIComponent(runId.trim());
38
+ return this.http.request(`/v2/integrations/${installation}/snapshots/${run}/abort`, { method: 'POST', auth: 'ingest' });
39
+ }
40
+ async replaceSnapshot(input) {
41
+ input.beforeRequest?.();
42
+ const run = await this.beginSnapshot(input.installationId, {
43
+ schemaVersion: 2,
44
+ namespace: input.namespace,
45
+ entityType: input.entityType,
49
46
  });
47
+ let page = 0;
48
+ try {
49
+ for await (const body of input.pages) {
50
+ input.beforeRequest?.();
51
+ await this.uploadSnapshotPage(input.installationId, run.runId, page, body);
52
+ page += 1;
53
+ }
54
+ input.beforeRequest?.();
55
+ return await this.commitSnapshot(input.installationId, run.runId, page);
56
+ }
57
+ catch (error) {
58
+ await this.abortSnapshot(input.installationId, run.runId).catch(() => undefined);
59
+ throw error;
60
+ }
50
61
  }
51
62
  }
52
63
  exports.AnhoraConnectorsApi = AnhoraConnectorsApi;
64
+ function eventId(prefix, externalId, supplied) {
65
+ return supplied?.trim() || `${prefix}:${externalId}:${crypto.randomUUID()}`;
66
+ }
67
+ function contentPayload(item) {
68
+ return {
69
+ title: item.title,
70
+ content: item.content,
71
+ ...(item.url ? { url: item.url } : {}),
72
+ ...(item.locale ? { locale: item.locale } : {}),
73
+ ...(item.document ? { document: item.document } : {}),
74
+ };
75
+ }
53
76
  class AnhoraContentApi {
54
77
  connectors;
55
78
  constructor(connectors) {
56
79
  this.connectors = connectors;
57
80
  }
58
- /** Convenience upsert for a single CMS/document knowledge row. */
59
81
  upsert(input) {
60
- return this.upsertMany({
61
- widgetId: input.widgetId,
62
- source: input.source,
63
- replace: input.replace,
64
- items: [
82
+ const item = input.document;
83
+ return this.connectors.events({
84
+ installationId: input.installationId,
85
+ namespace: input.namespace,
86
+ events: [
87
+ {
88
+ eventId: eventId(input.namespace, item.externalId, input.eventId),
89
+ entityType: input.entityType ?? 'content_document',
90
+ externalId: item.externalId,
91
+ operation: 'upsert',
92
+ sourceVersion: item.sourceVersion,
93
+ sourceSequence: item.sourceSequence,
94
+ sourceUpdatedAt: item.sourceUpdatedAt,
95
+ payload: contentPayload(item),
96
+ },
97
+ ],
98
+ });
99
+ }
100
+ delete(input) {
101
+ return this.connectors.events({
102
+ installationId: input.installationId,
103
+ namespace: input.namespace,
104
+ events: [
65
105
  {
106
+ eventId: input.eventId,
107
+ entityType: input.entityType ?? 'content_document',
66
108
  externalId: input.externalId,
67
- title: input.title,
68
- content: input.content,
69
- url: input.url,
109
+ operation: 'delete',
110
+ sourceSequence: input.sourceSequence,
111
+ sourceUpdatedAt: input.sourceUpdatedAt,
70
112
  },
71
113
  ],
72
114
  });
73
115
  }
74
116
  upsertMany(input) {
75
- const prefix = input.source.trim().replace(/:+$/u, '');
76
- return this.connectors.ingestKnowledge({
77
- widgetId: input.widgetId,
78
- knowledgeExternalIdPrefix: prefix ? `${prefix}:` : undefined,
79
- replace: input.replace,
80
- items: input.items.map((item) => ({
81
- externalId: withSourcePrefix(input.source, item.externalId),
82
- name: item.title,
83
- content: item.content,
84
- url: item.url,
117
+ return this.connectors.events({
118
+ installationId: input.installationId,
119
+ namespace: input.namespace,
120
+ events: input.documents.map((item) => ({
121
+ eventId: input.eventIdFor(item),
122
+ entityType: input.entityType ?? 'content_document',
123
+ externalId: item.externalId,
124
+ operation: 'upsert',
125
+ sourceVersion: item.sourceVersion,
126
+ sourceSequence: item.sourceSequence,
127
+ sourceUpdatedAt: item.sourceUpdatedAt,
128
+ payload: contentPayload(item),
85
129
  })),
86
130
  });
87
131
  }
132
+ replaceAll(input) {
133
+ const pageSize = Math.max(1, Math.min(input.pageSize ?? 500, 500));
134
+ const pages = [];
135
+ for (let offset = 0; offset < input.documents.length; offset += pageSize) {
136
+ pages.push({
137
+ items: input.documents.slice(offset, offset + pageSize).map((item) => ({
138
+ externalId: item.externalId,
139
+ sourceVersion: item.sourceVersion,
140
+ sourceSequence: item.sourceSequence,
141
+ sourceUpdatedAt: item.sourceUpdatedAt,
142
+ payload: contentPayload(item),
143
+ })),
144
+ });
145
+ }
146
+ return this.connectors.replaceSnapshot({
147
+ installationId: input.installationId,
148
+ namespace: input.namespace,
149
+ entityType: input.entityType ?? 'content_document',
150
+ pages,
151
+ });
152
+ }
88
153
  }
89
154
  exports.AnhoraContentApi = AnhoraContentApi;
package/dist/http.js CHANGED
@@ -22,7 +22,7 @@ class AnhoraHttp {
22
22
  }
23
23
  if (options.auth === 'ingest') {
24
24
  if (!this.ingestSecret) {
25
- throw new Error('Anhora ingestSecret is required for connectors.ingest / connectors.sync');
25
+ throw new Error('Anhora ingestSecret is required for connector events and snapshots');
26
26
  }
27
27
  headers['X-Anhora-Ingest-Secret'] = this.ingestSecret;
28
28
  }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export { createAnhora } from './client';
2
2
  export type { AnhoraClient } from './client';
3
3
  export { AnhoraApiError, normalizeApiBase } from './types';
4
- export type { AnhoraClientConfig, AnhoraFetch, RequestOptions, } from './types';
5
- export type { ConnectorSyncResponse, ContentUpsertBatchInput, ContentUpsertInput, } from './connectors';
4
+ export type { AnhoraClientConfig, AnhoraFetch, RequestOptions } from './types';
5
+ export type { BeginSnapshotResponse, CommitSnapshotResponse, ContentDocumentInput, SnapshotPageResponse, } from './connectors';
6
6
  export type { WidgetReleaseWithUrl } from './embed';
7
7
  /** @deprecated Use `createAnhora().widgets.getConfig` / `deployments.getRuntime`. */
8
8
  export { AnhoraClient as LegacyAnhoraClient } from './legacy';
package/dist/legacy.js CHANGED
@@ -14,6 +14,8 @@ class AnhoraClient {
14
14
  return this.anhora.widgets.getConfig(this.widgetId);
15
15
  }
16
16
  async sendMessage(_message, _sessionId) {
17
+ void _message;
18
+ void _sessionId;
17
19
  throw new Error('Legacy AnhoraClient.sendMessage is removed. Use the embed widget or POST /api/v1/chat/message with a deploymentKey.');
18
20
  }
19
21
  }
package/dist/types.d.ts CHANGED
@@ -11,7 +11,7 @@ export type AnhoraClientConfig = {
11
11
  */
12
12
  apiBase?: string;
13
13
  /**
14
- * Server-side ingest secret for `/connectors/ingest` and `/connectors/sync`.
14
+ * Server-side ingest secret for V2 integration events and snapshots.
15
15
  * Sent as `X-Anhora-Ingest-Secret` (Bearer also accepted by the API).
16
16
  */
17
17
  ingestSecret?: string;
package/dist/types.js CHANGED
@@ -14,12 +14,18 @@ class AnhoraApiError extends Error {
14
14
  }
15
15
  exports.AnhoraApiError = AnhoraApiError;
16
16
  function normalizeApiBase(apiBase) {
17
- let normalized = apiBase.trim().replace(/\/+$/u, '');
17
+ let normalized = apiBase.trim();
18
+ while (normalized.endsWith('/')) {
19
+ normalized = normalized.slice(0, -1);
20
+ }
18
21
  if (!normalized) {
19
22
  return 'https://api.anhora.net/api';
20
23
  }
21
24
  // Accept both `.../api` and `.../api/v1` from older snippets.
22
- normalized = normalized.replace(/\/v\d+$/u, '');
25
+ const versionSuffix = normalized.match(/\/v\d+$/u);
26
+ if (versionSuffix) {
27
+ normalized = normalized.slice(0, -versionSuffix[0].length);
28
+ }
23
29
  if (!normalized.endsWith('/api')) {
24
30
  normalized = `${normalized}/api`;
25
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anhora/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Framework-agnostic Anhora API client (connectors, content ingest, deployment runtime)",
5
5
  "license": "MIT",
6
6
  "author": "Anhora",
@@ -37,7 +37,7 @@
37
37
  "access": "public"
38
38
  },
39
39
  "dependencies": {
40
- "@anhora/shared-types": "0.1.0"
40
+ "@anhora/shared-types": "0.3.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "eslint": "^9.24.0",