@andesine/sdk 0.9.0 → 0.11.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,970 +1,36 @@
1
1
  # @andesine/sdk
2
2
 
3
- MIT-licensed TypeScript client for the public Andesine HTTP API. ESM only. Requires
4
- Node.js 22 or later, or a browser/worker runtime with Fetch, Blob, FormData, and
5
- AbortSignal.any. Keep secret API keys in server code; browsers use publishable keys.
3
+ The TypeScript client for the [Andesine](https://andesine.app) API. It works in Node.js 22 or
4
+ later, and in browsers and workers.
6
5
 
7
- ## Use
6
+ ## Install
8
7
 
9
8
  ```sh
10
9
  npm install @andesine/sdk
11
10
  ```
12
11
 
13
- ```ts
14
- import { createClient } from "@andesine/sdk";
15
-
16
- const client = createClient({
17
- baseURL: "https://api.andesine.app",
18
- apiKey: process.env.ANDESINE_API_KEY
19
- });
20
-
21
- const entry = await client.entries.get({ id: "ent_123" });
22
- const published = await client.content.get({
23
- entryID: entry.id,
24
- channel: "published"
25
- });
26
- ```
27
-
28
- `baseURL` defaults to `https://api.andesine.app`. A trailing slash is optional; path prefixes are preserved.
29
- Each client has its own configuration. You can supply `fetch` and default
30
- `headers` for proxies, instrumentation, or other authentication arrangements.
31
-
32
- For OAuth credentials, use `accessToken` instead of `apiKey`. This disables the
33
- `ANDESINE_API_KEY` fallback. The SDK does not log in or refresh tokens.
12
+ ## Example
34
13
 
35
14
  ```ts
36
- const client = createClient({ accessToken });
37
- const identity = await client.auth.getIdentity();
38
- const workspaces = await client.workspaces.list();
39
- const workspaceClient = createClient({
40
- accessToken,
41
- headers: { "x-workspace-id": selectedWorkspaceID }
42
- });
43
- ```
44
-
45
- ### Publishable keys
15
+ import { createClient } from "@andesine/sdk";
46
16
 
47
- Publishable keys (`adn_pk_...`) are safe in browsers. Create one in the Andesine
48
- app, with its collections and the origins of the sites that use it. A publishable
49
- key can read the published content of its collections (`content.getTree`,
50
- `content.get`, `content.listEntries`), search it (`search.published`), and, when
51
- the key allows it, answer questions about it (`search.askPublished` and
52
- `search.askPublishedStream`). Browser requests need an allowed `Origin`, so the key
53
- works only on its sites; requests without `Origin`, e.g. from a site build on a
54
- server, are allowed. Other operations are refused.
17
+ const client = createClient({ apiKey: process.env.ANDESINE_API_KEY });
55
18
 
56
- ```ts
57
- const client = createClient({ apiKey: "adn_pk_..." });
19
+ const page = await client.content.get({ path: "/Docs/Quickstart", channel: "published" });
58
20
  const results = await client.search.published({ query: "install", channel: "published" });
59
21
  ```
60
22
 
61
- Pass the key as `apiKey`. This also disables the `ANDESINE_API_KEY` fallback, so a
62
- bundler that defines `process.env` cannot add a secret key to browser code. The key
63
- has the same rate limits and usage quota as other API access, and all visitors of a
64
- site share them.
65
-
66
- Select a workspace explicitly for OAuth workspace operations. The server checks
67
- current user permissions on each request. Browser-only operations remain excluded.
68
- API keys keep their workspace binding and cannot list a user's workspaces.
69
-
70
- Resource methods are generated from the checked-in OpenAPI document. They cover
71
- assets, entries, collections, content delivery, roles, search, schemas, schema
72
- migrations, schema versions, memberships, publishing, entry versions, webhooks, and instance information. Internal
73
- session-only operations are excluded. Published asset delivery does not require
74
- an API key.
75
-
76
- Methods accept one object containing path, query, and body fields. Operations
77
- without required fields can be called without arguments. Date/time values remain
78
- JSON strings. File inputs and binary outputs use `Blob`.
79
-
80
- ```ts
81
- await client.assets.upload({ assetID: "ast_123", file: new Blob([bytes]) });
82
- const image = await client.assets.get({ assetID: "ast_123", entryID: entry.id });
83
- ```
84
-
85
- The client does not add a content-save endpoint. Use converters to produce JSON;
86
- storage remains a separate responsibility.
87
-
88
- ## Errors and responses
89
-
90
- Methods return data directly. HTTP failures throw `AndesineAPIError`, with
91
- `status`, `code`, `message`, `data`, `defined`, `hints`, and the Fetch `response`. Network failures and
92
- cancellation retain their native errors.
93
-
94
- ```ts
95
- import { AndesineAPIError } from "@andesine/sdk";
96
-
97
- try {
98
- await client.entries.update({ id: "ent_123", name: "Installation" });
99
- } catch (error) {
100
- if (error instanceof AndesineAPIError) {
101
- console.error(error.status, error.code, error.message, error.hints);
102
-
103
- if (error.is("SCHEMA_MIGRATION_IN_PROGRESS")) {
104
- // data is typed for this code.
105
- console.log(error.data.migrationID);
106
- }
107
-
108
- if (error.is("TOO_MANY_REQUESTS")) {
109
- console.log(error.data?.limit, error.data?.retryAfterSeconds);
110
- }
111
- }
112
- }
113
-
114
- const result = await client.content.get(
115
- { entryID: "ent_123" },
116
- { response: "full", headers: { "If-None-Match": cachedETag } }
117
- );
118
-
119
- if (result.notModified) {
120
- // Reuse the cached content.
121
- } else {
122
- console.log(result.data, result.headers.get("ETag"));
123
- }
124
- ```
125
-
126
- Use full-response mode for conditional requests. A 304 response has
127
- `notModified: true` and no data. A 304 in data mode causes a clear error because
128
- the client does not store a response cache.
129
-
130
- ## Limits and usage
131
-
132
- Rate limits stop abuse; they count requests for each credential (API key, OAuth
133
- grant, or app session) in one-minute windows.
134
-
135
- | Requests a minute | Free | Pro |
136
- | --------------------------------- | ---- | ----- |
137
- | All requests | 600 | 6,000 |
138
- | AI answers | 60 | 600 |
139
- | Semantic search (with `semantic`) | 300 | 3,000 |
140
-
141
- Each workspace also has two monthly meters, which reset at 00:00 UTC on the first
142
- day of each month:
143
-
144
- - **API calls:** each request with an API key or OAuth. Free includes 50,000; on Pro,
145
- each seat adds 150,000 to a pool shared across the workspace, and the rest is billed.
146
- - **AI credits:** an AI answer uses 10 credits, and a semantic search uses 1. Requests
147
- from the app use credits too. Free includes 5,000; on Pro, each seat adds 15,000 to
148
- the shared pool, and the rest is billed.
149
-
150
- On Free, the meters are hard limits. On Pro, a workspace admin can set a monthly
151
- spending limit for the billed usage; when it is reached, metered requests stop until
152
- the next month.
153
-
154
- Every limit returns `TOO_MANY_REQUESTS` with `data.limit` (`rate`, `api-calls`,
155
- `ai-credits`, or `spending`) and `data.retryAfterSeconds`, also sent as
156
- `Retry-After`. A `rate` limit passes within a minute; the others last until the
157
- monthly reset, so do not retry them automatically.
158
-
159
- Responses describe the limits in headers:
160
-
161
- - `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`: the rate
162
- limit that applies to the request; AI requests show their AI limit.
163
- - `X-API-Usage`, `X-API-Usage-Limit`, and `X-API-Usage-Reset`: the month's API calls,
164
- on metered requests.
165
- - `X-AI-Credits`, `X-AI-Credits-Limit`, and `X-AI-Credits-Reset`: the month's AI
166
- credits, on AI requests.
167
-
168
- Limits are the hard limit on Free, or the included amount on Pro. Reset values are
169
- Unix times in seconds. Self-hosted instances without billing have the Pro rate limits
170
- and no meters.
171
-
172
- ## Cancellation, timeouts, and retries
173
-
174
- The default timeout is 30 seconds. Set `timeout: 0` to disable it. Timeouts include
175
- request execution, response reading, and retry delays. Supply `signal` per call
176
- for cancellation.
177
-
178
- Retries are **disabled by default**. Set `retries: 2` on the client or a call to
179
- allow up to two retries of GET requests and idempotent writes. They retry network
180
- `TypeError` failures and HTTP 429, 500, 502, 503, and 504. They respect
181
- `Retry-After`, otherwise use increasing delays with jitter. Other writes are never
182
- retried automatically.
183
-
184
- Idempotent writes (`entries.create`, `entries.update`, and `versions.create`) send
185
- an `Idempotency-Key` header, generated for each call unless you supply one in
186
- `headers`. The server applies a key once and returns the first result for 24 hours,
187
- so retries can't apply a write twice. A retry that arrives while the first attempt
188
- still runs gets `IDEMPOTENCY_KEY_IN_PROGRESS` and is retried after `Retry-After`. Supply your own key to make a write safe
189
- across separate calls, for example after a process restart.
190
-
191
- ```ts
192
- await client.entries.get(
193
- { id: "ent_123" },
194
- { signal: controller.signal, timeout: 10_000, retries: 2 }
195
- );
196
- ```
197
-
198
- ## Snapshots and pagination
199
-
200
- Pin related content reads to the snapshot returned by the first read:
201
-
202
- ```ts
203
- const tree = await client.content.getTree({ collectionID: "col_123" });
204
- const content = client.atSnapshot(tree.snapshotID);
205
- const entry = await content.get({ entryID: "ent_123" });
206
- ```
207
-
208
- `atSnapshot()` returns content resource methods. It rejects a conflicting channel
209
- or snapshot selector. It does not pin unrelated management operations. Snapshot
210
- image URLs can expire; copying images for archival export is the caller's task.
211
-
212
- ```ts
213
- import { paginate } from "@andesine/sdk";
214
-
215
- for await (const entry of paginate((cursor) => client.entries.list({ cursor }))) {
216
- console.log(entry.name);
217
- }
218
- ```
219
-
220
- Cursor lists default to 50 items and accept at most 100. `memberships.list()` and
221
- `memberships.listInvites()` return `{ data, pagination }`. Migration status no longer
222
- includes `contentLossEntries`; use `schemaMigrations.listContentLossEntries()` for
223
- that paginated list. Mutable management lists are not snapshots: concurrent edits
224
- can change their contents between requests.
225
-
226
- Use `paginatePages()` to keep page metadata and save a cursor after processing a
227
- whole page. Both iterators accept `{ cursor }` to resume. Save the same filters with
228
- your cursor. A missing or repeated continuation cursor causes an error.
229
-
230
- Published listings also require the snapshot ID on later pages. The first page
231
- selects a channel; later pages select that exact snapshot:
232
-
233
- ```ts
234
- import { paginatePages } from "@andesine/sdk";
235
-
236
- let snapshotID: string | undefined; // Restore this together with a saved cursor.
237
- const savedCursor: string | undefined = undefined;
238
-
239
- for await (const page of paginatePages(
240
- (cursor) =>
241
- client.content.listEntries({
242
- ...(snapshotID ? { snapshotID } : { channel: "published" }),
243
- cursor,
244
- limit: 100
245
- }),
246
- { cursor: savedCursor }
247
- )) {
248
- snapshotID = page.snapshotID;
249
- // Process page.data, then save snapshotID and page.pagination.nextCursor.
250
- // A null nextCursor means the listing is complete.
251
- }
252
- ```
253
-
254
- `content.listCollections()` uses the same format. Both return flat records ordered
255
- by ID, with parent/collection IDs for links. Entry summaries include a version ID and
256
- hash. Use `includeContent: true` for full entry records, or fetch one entry with
257
- `client.atSnapshot(snapshotID).get()`. Snapshot expiry
258
- still applies. If a saved snapshot is unavailable, start a new listing from its
259
- channel; do not combine the old cursor with a new snapshot.
260
-
261
- ### Filtered full-content lists
262
-
263
- ```ts
264
- const page = await client.content.listEntries({
265
- collectionPath: "/Tutorials",
266
- descendants: true,
267
- includeContent: true,
268
- filters: [
269
- { kind: "text", key: "audience", values: ["beginner"] },
270
- { kind: "boolean", key: "enabled", value: true }
271
- ],
272
- limit: 50
273
- });
274
-
275
- for (const entry of page.data) {
276
- console.log(entry.path, entry.content, entry.properties, entry.schema);
277
- }
278
-
279
- if (page.pagination.nextCursor) {
280
- const next = await client.atSnapshot(page.snapshotID).listEntries({
281
- collectionPath: "/Tutorials",
282
- descendants: true,
283
- includeContent: true,
284
- filters: [
285
- { kind: "text", key: "audience", values: ["beginner"] },
286
- { kind: "boolean", key: "enabled", value: true }
287
- ],
288
- cursor: page.pagination.nextCursor,
289
- limit: 50
290
- });
291
- }
292
- ```
293
-
294
- `includeContent` defaults to false. A literal true gives full items; false or an
295
- omitted option gives summaries. A runtime boolean gives their union. This also
296
- works with full-response mode, `atSnapshot()`, and `paginatePages()`. Full items
297
- add `content`, `properties`, `fragments`, `assets`, `schema`, and full version
298
- metadata. Channel and snapshot metadata stay in the page envelope. The generated
299
- `PublishedEntryContent` and `PublishedEntrySummary` types are public.
300
-
301
- With a collection selector, the list returns direct entries by default.
302
- `descendants: true` includes nested collections and requires a collection selector.
303
- Use `collectionPath: "/"` with descendants for the complete snapshot tree. Without
304
- a scope, the list includes all snapshot entries.
305
-
306
- Filters use the same contract as search: text `any`/`all`/`none`, number and date
307
- comparisons, and boolean equality. All filters must match. Text comparison ignores
308
- case; missing or incompatible properties do not match negative operators. Filters
309
- use the assigned version's property records, including fields between content
310
- blocks. They do not use the current draft or the search index. The SDK encodes
311
- nested filters automatically.
312
-
313
- Keep the same scope and filters across pages. Limits remain 1–100 entries and up
314
- to 20 filters, with the existing per-filter bounds. Assets are URLs and metadata.
315
- The server validates each returned full document against its recorded schema. An
316
- invalid document fails the page with `CONTENT_SCHEMA_INVALID`; it is not skipped.
317
- Summary reads do not load or validate full documents.
318
-
319
- ### Structured content
320
-
321
- Use `toStructuredContent()` to remove property and fragment wrappers:
322
-
323
- ```ts
324
- import { toStructuredContent } from "@andesine/sdk";
325
-
326
- const page = await client.content.get({ path: "/Tutorials/Welcome" });
327
- const tutorial = toStructuredContent(page);
328
-
329
- console.log(tutorial.properties); // Each property's value, keyed by derived key.
330
- console.log(tutorial.fragments); // Each fragment's content, keyed by derived key.
331
- console.log(tutorial.schema, tutorial.path); // Other response fields are preserved.
332
- ```
333
-
334
- The helper also accepts current full entries and full published list items:
335
- `page.data.map(toStructuredContent)`. Property and fragment maps stay separate.
336
- Values such as `null`, false, zero, empty strings, and empty arrays are preserved.
337
- The helper is synchronous. It creates a new object and maps, does not change its
338
- input, and retains references to values and other fields. It does not validate,
339
- coerce, fetch, or generate collection-specific types. Types follow the supplied
340
- input; API property values retain their generated union types. `StructuredContent<T>`
341
- is available for explicit annotations. Collection type generation remains deferred.
342
-
343
- ## Publishing preconditions
344
-
345
- Pass `expectedSnapshotID` to publish, unpublish, or delete a channel only if its
346
- publication state still matches a prior read:
347
-
348
- ```ts
349
- const reviewed = await client.content.listEntries({ channel: "published", limit: 1 });
350
-
351
- await client.publishing.publishEntry({
352
- entryID: "ent_123",
353
- channel: "published",
354
- expectedSnapshotID: reviewed.snapshotID
355
- });
356
- ```
357
-
358
- The server checks the channel inside the mutation transaction. A mismatch returns
359
- HTTP 409 with code `PUBLISHING_SNAPSHOT_CHANGED`, the expected and current snapshot
360
- IDs, and a hint. Read the current state and review it before another write. Omitting
361
- the precondition makes no comparison with your prior read. Draft edits are not
362
- covered by this check.
363
-
364
- `setCollection()` and `bulkSetCollections()` can affect multiple channels. Their
365
- optional `expectedSnapshots` map checks each supplied channel before the change,
366
- for example `{ published: "snp_123", preview: "snp_456" }`. Omitted channels are not
367
- checked. `revertChanges()` continues to require its reviewed `snapshotID`.
368
-
369
- Bulk inputs accept at most 100 selected items. Revert selections share that limit
370
- across entries and collections. A selected collection can contain more descendants;
371
- this is an input limit, not a limit on the number of affected records. Separate bulk
372
- requests are separate transactions.
373
-
374
- ## Instance information
375
-
376
- ```ts
377
- const instance = await client.instance.get();
378
- console.log(instance.features.imageStorage, instance.features.semanticSearch);
379
- console.log(instance.features.aiAnswers, instance.features.aiAnswerStreaming);
380
- console.log(instance.limits.maxUploadBytes, instance.limits.assetStorageBytes);
381
- console.log(instance.limits.maxPageSize, instance.limits.maxBulkItems);
382
- ```
383
-
384
- This authenticated call returns the API contract version, configured features,
385
- and effective workspace limits, including the search result limit. Features do
386
- not grant permissions or report live service health. Storage limits are capacity,
387
- not remaining space. `aiAnswers` requires search, embedding, and answer-model
388
- configuration. `aiAnswerStreaming` reports the configured streaming implementation;
389
- the AI provider must support Chat Completions SSE. These flags do not report
390
- remaining quota. The SDK makes no discovery request automatically.
391
-
392
- ## Types
393
-
394
- Named types are generated from the public OpenAPI components. Import them directly
395
- or through the type-only `Andesine` namespace:
396
-
397
- ```ts
398
- import type { Entry, PropertyFilter, Andesine } from "@andesine/sdk";
399
-
400
- const renderEntry = (entry: Entry): string => entry.name;
401
- const renderAsset = (asset: Andesine.Asset): string => asset.filename;
402
- const filter: PropertyFilter = {
403
- kind: "text",
404
- key: "category",
405
- values: ["guides"]
406
- };
407
- ```
408
-
409
- The exports cover entries and properties; assets, files, and analysis; collections;
410
- entry and schema versions; schema fields, inheritance, and migrations; memberships,
411
- invites, roles, and permissions; published content and trees; publishing channels;
412
- search results and filters; and pagination.
413
-
414
- `Entry` includes content, fragments, and properties. `EntrySummary` is the metadata
415
- returned by entry lists and creation. `SchemaDefinition` describes a collection's
416
- content schema; `CollectionSchema` contains its local and effective schema details.
417
- Types describe the HTTP data: dates are strings and binary values are `Blob`.
418
- Search filter types describe inputs, so fields with server defaults can be omitted.
419
-
420
- Every public operation also has generated input, output, and error aliases. Their names
421
- combine the resource and method with `Input`, `Output`, or `Error`:
422
-
423
- ```ts
424
- import type { EntriesCreateInput, EntriesListOutput, Andesine } from "@andesine/sdk";
425
-
426
- const input: EntriesCreateInput = { name: "Getting started" };
427
- type EntryPage = EntriesListOutput;
428
- type SearchRequest = Andesine.SearchCurrentInput;
429
- ```
430
-
431
- Operation inputs match the SDK's single argument object, including path, query,
432
- and body fields. Outputs match the returned data, excluding the full-response
433
- wrapper. All these exports, including `Andesine`, are types only and add no runtime
434
- code. The generic helpers and raw `components`, `operations`, and `paths` types
435
- remain available:
436
-
437
- ```ts
438
- import type { OperationInput, OperationOutput } from "@andesine/sdk";
439
-
440
- type CreateEntry = OperationInput<"entries.create">;
441
- type EntryPage = OperationOutput<"entries.list">;
442
- ```
443
-
444
- Content types are structurally compatible with `@andesine/converters`. Neither
445
- public package imports the private editor or backend at runtime.
446
-
447
- ## Maintainer workflow
448
-
449
- From the repository root:
450
-
451
- ```sh
452
- pnpm api:generate
453
- pnpm build --filter=@andesine/sdk
454
- ```
455
-
456
- The export calls oRPC's OpenAPI generator on the same contracts implemented by
457
- the server. It imports no request handlers or service initialization and needs no
458
- backend environment variables, database, queue, or running server. The export
459
- uses the default upload-size limit; self-hosted instances can set a different
460
- limit, which their own OpenAPI document reflects. The checked-in document's
461
- server URL is a placeholder; consumers must supply their instance URL.
462
-
463
- Commit `openapi.json` and `src/generated/` together. Do not edit generated files.
464
- Public schema names are defined in the backend's
465
- `src/contracts/schemas/public.ts` registry. Adding a component there generates its
466
- named SDK export and its `Andesine` member; no SDK alias list needs to be updated.
467
- The generator also creates input/output/error aliases for each public operation and
468
- rejects invalid or duplicate public type names.
469
- Generation is a maintainer operation, never an install-time operation. `prepack`
470
- checks types, bundles ESM and declarations with Rolldown, and checks
471
- package metadata with publint. These 0.x packages are pre-release; review
472
- generated changes together with API changes.
473
-
474
- See the [release guide](https://github.com/vriteio/andesine/blob/andesine/packages/public/README.md)
475
- for archive checks, versioning, and npm publishing. This package uses the
476
- [MIT license](./LICENSE).
477
-
478
- ### Generator choice
479
-
480
- Both Hey API 0.99.0 and openapi-typescript 7.13.0 generated output from the Andesine
481
- spec. This package uses openapi-typescript and openapi-fetch. A small generated
482
- resource layer supplies the flat argument objects and instance methods; a shared
483
- transport supplies errors, retries, multipart serialization, and full responses.
484
-
485
- Hey API generated operation functions and a larger client layer, but still needed
486
- adaptation for the selected public interface and multipart operation defaults.
487
- The selected approach keeps these behaviors in one maintained transport and
488
- keeps runtime dependencies small.
489
-
490
- ## Content names
491
-
492
- Sibling entries and collections share names. Names are trimmed, normalized to
493
- Unicode NFC, and must have unique derived URL slugs at each level. Exact names
494
- also remain unique, including symbol-only names. Names cannot contain `/` or equal
495
- `.` or `..`. The limit is 300 JavaScript string units. The collection name `~`
496
- is reserved for the internal workspace root. Root entries and root collections
497
- share the same level.
498
-
499
- Creation chooses an available name, adding suffixes such as ` (2)` when needed.
500
- Use the returned name rather than assuming that your requested name was saved.
501
- Rename, move, and restore operations reject conflicts. Deleted items release
502
- names; a schema move temporarily reserves its source name for rollback.
503
-
504
- `CONTENT_NAME_CONFLICT` (409) includes `name`, `slug`, `parentID`, and optional `hints`.
505
- Use `error.is("CONTENT_NAME_CONFLICT")` to access typed data. Renaming never
506
- chooses a different name for you.
507
-
508
- ## Content paths
509
-
510
- Read an entry by ID, `path`, or `slugPath`. Use exactly one selector:
511
-
512
- ```ts
513
- const current = await client.entries.get({ path: "/Docs/Getting started" });
514
- const tree = await client.content.getTree({ collectionSlugPath: "/docs" });
515
- const content = client.atSnapshot(tree.snapshotID);
516
- const page = await content.get({ slugPath: "/docs/getting-started" });
517
- const definition = await content.getSchema({ slugPath: page.slugPath });
518
- const activeSchema = await client.schemas.get({ collectionPath: "/Docs" });
519
- ```
520
-
521
- Paths use names from the tree, with `/` between levels. Names are case-sensitive;
522
- each segment is trimmed and NFC-normalized. Pass decoded names to the SDK. It
523
- encodes query parameters once, so a name containing the literal text `%2F` keeps
524
- that text. Empty segments, trailing slashes, `.` and `..` are invalid.
525
-
526
- An absolute path starts with `/`. An anchored path starts with a collection ID,
527
- such as `coll_67bqzSOb9kct2DaVkTwT53/Getting started`. The anchor must exist in the
528
- selected current tree or snapshot. A name that looks like an ID is still a name
529
- when it occurs in an absolute path. A bare collection ID is also a valid
530
- `collectionPath`. `/` selects the root. Neither selects an entry on its own.
531
-
532
- Current entry and collection responses include their canonical absolute `path` and `slugPath`.
533
- Published content, trees, and list results use snapshot collection names and the
534
- titles of assigned versions. Draft renames, moves, and deletions do not change
535
- those snapshot paths. Full published content also includes `id` and `collectionID`.
536
- Saved versions remain ID-based because a version alone has no snapshot location.
537
-
538
- `content.getTree({ collectionPath: "/" })` returns a virtual root collection with
539
- `id: null`, `name: ""`, `path: "/"`, and `slugPath: "/"`. It contains top-level entries and collections.
540
-
541
- `entries.list`, `collections.list`, `content.listEntries`, and
542
- `content.listCollections` accept one of `collectionID`, `collectionPath`, or `collectionSlugPath` to
543
- select direct children. Keep the same scope while paging. With no scope,
544
- `collections.list` selects root children; the other lists select all accessible
545
- items. `collections.list` now uses these fields instead of `ancestorID`.
546
- Search accepts the same collection selectors and includes descendants. Its `/`
547
- scope covers the whole accessible tree. Search results include a canonical entry
548
- `path`, `slugPath`, and an optional heading `anchor`.
549
-
550
- Slugs use Unicode NFKC normalization, lowercase, Unicode letters/marks/numbers,
551
- and single hyphens between other character groups. Accents and non-Latin letters
552
- are retained. A symbol-only name uses its public ID in lowercase with `-` instead
553
- of `_`. Slugs are derived, never edited separately. Renaming or moving content
554
- changes its path; redirects are not stored. `Getting Started`, `getting-started`,
555
- and `Getting Started!` conflict at the same level. Existing conflicting content
556
- must be renamed before publishing it again.
557
-
558
- `slugPath` and `collectionSlugPath` accept absolute paths and collection-ID
559
- anchors, with the same snapshot and scope rules as name paths. Pass decoded
560
- segments, for example `coll_ID/getting-started`. The URL route should be decoded
561
- once by your framework before passing it to the SDK.
562
-
563
- ```ts
564
- import { toContentURL } from "@andesine/sdk";
565
-
566
- // Prefer the API object: its slugPath also covers symbol-only ancestor names.
567
- const url = toContentURL(page, { rootPath: "/Docs", baseURL: "/docs" });
568
- // /docs/getting-started
569
- const originalNames = toContentURL(page, { format: "name" });
570
- // /Docs/Getting%20started
571
- ```
572
-
573
- `toContentURL` defaults to slug URLs. A string input derives slugs from the path;
574
- API objects use their returned `slugPath`. `format: "name"` preserves original
575
- names. `rootPath` accepts a name or slug prefix in slug mode. Destination prefixes
576
- and heading anchors are not slugified. `toContentSlug(name, id?)` exposes the
577
- shared segment rule; symbol-only names require a public content ID. It is also
578
- available from `@andesine/sdk/slug`, which loads only the slug helper.
579
-
580
- Search indexes must be rebuilt after this upgrade to populate `slugPath` on
581
- existing results and AI sources. No database schema migration is needed.
582
-
583
- HTTP reads use query selectors on `GET /entries/get`, `GET /content/entries/get`,
584
- `GET /content/tree`, and `GET /schemas/collection`. The published schema route
585
- remains `GET /content/entries/schema`. These replace the former ID-in-path GET
586
- routes; mutation routes are unchanged. SDK method names are unchanged.
587
-
588
- Publication checks names across the complete next snapshot, including retained
589
- versions. `PUBLISHING_NAME_CONFLICT` (409) includes `name`, `parentID`, and `hints`.
590
- Publish related replacements together or unpublish the conflict first. A draft
591
- rename alone does not remove a name from an existing publication. A rejected
592
- publication leaves the channel and its snapshot unchanged.
593
-
594
- ## Search and source links
595
-
596
- Published search results include required `channel`, `snapshotID`, `versionID`,
597
- and canonical entry `path` and `slugPath` fields. `entryID` remains the stable identity.
598
- An optional `anchor` identifies a heading in that version; a result before any
599
- heading has no anchor. Current results have their indexed working path. Search
600
- index updates are asynchronous, so current paths can briefly lag draft changes.
601
-
602
- ```ts
603
- const matches = await client.search.published({
604
- channel: "published",
605
- query: "installation"
606
- });
607
-
608
- for (const result of matches.results) {
609
- const page = await client.atSnapshot(result.snapshotID).get({
610
- entryID: result.entryID
611
- });
612
- const link = toContentURL(result, { rootPath: "/Docs", baseURL: "/docs" });
613
- // Render page headings with matching IDs.
614
- }
615
- ```
616
-
617
- Paths and anchors are decoded data, not application URLs. Encode each path segment
618
- when building a URL. Reading by `entryID` at the source snapshot preserves the
619
- content version even if the channel advances. A retained snapshot can expire;
620
- handle an unavailable snapshot with a new search. Search still uses the current
621
- channel and does not support historical snapshot queries.
622
-
623
- The converters' `getHeadingAnchors()` helper derives matching targets. HTML
624
- conversion applies those IDs; other renderers must preserve AST IDs or apply the
625
- helper themselves. Duplicate headings are numbered across all fragments. Existing
626
- `headingPath` is retained for editor navigation. Answer sources use the same path,
627
- snapshot, and anchor metadata, plus a numbered citation `id` and `relevance`.
628
-
629
- ## AI answers
630
-
631
- Call these methods from your server with an API key. For published documentation,
632
- grant `ai-answers` and `read:publishing`. For current workspace content, grant
633
- `ai-answers`, `read:entries`, and `read:collections`. Write permissions also supply
634
- the corresponding read access, but never imply `ai-answers`. The key editor has a
635
- separate AI answers option.
636
-
637
- ```ts
638
- import type { AnswerHistoryMessage, PublishedAnswerSource } from "@andesine/sdk";
639
-
640
- const history: AnswerHistoryMessage[] = [];
641
- const result = await client.search.askPublished({
642
- channel: "published",
643
- collectionPath: "/Docs",
644
- question: "How do I install Andesine?",
645
- history
646
- });
647
- const sources: PublishedAnswerSource[] = result.sources;
648
-
649
- // [1] in result.answer refers to the source with id === 1.
650
- // Use each source's path/anchor for links and snapshotID/entryID for content reads.
651
- history.push(
652
- { role: "user", content: "How do I install Andesine?" },
653
- { role: "assistant", content: result.answer }
654
- );
655
- ```
656
-
657
- `client.search.askCurrent({ question, collectionPath, history })` reads current
658
- content with the caller's collection access rules. Both methods accept optional
659
- property `filters` and either `collectionID` or `collectionPath`. Published scopes
660
- resolve within the channel's current snapshot. Omit the collection selector to
661
- search all content available to the caller.
662
-
663
- Responses contain the complete `answer` and `sources`. The generated `Answer`,
664
- `PublishedAnswer`, `AnswerSource`, `PublishedAnswerSource`, and
665
- `AnswerHistoryMessage` types are also available through `Andesine`.
666
- Both complete and streamed answers use the same inputs and source-selection rules.
667
-
668
- The limits are 1,000 characters per question, 10 history messages of up to 4,000
669
- characters each, and 20 property filters. History is supplied per request; the SDK
670
- does not store it. Trim history before sending a later question. Each answer uses 3
671
- AI credits and has its own rate limit; see [Limits and usage](#limits-and-usage). A
672
- complete response is counted when it succeeds.
673
- A rate-limit error includes `Retry-After` and `data.retryAfterSeconds`. Provider
674
- failures return `SERVICE_UNAVAILABLE` with hints. Answer POST requests are never
675
- automatically retried, even when read retries are enabled.
676
-
677
- ## Recorded content schemas
678
-
679
- Full entry, version, and published-content responses include `schema`, containing
680
- `revisionID` and `hash`, or `null` for content without a recorded schema. Reads and
681
- publication validate the document against that recorded revision, including
682
- inherited fields. They do not repair content or use the latest schema instead.
683
-
684
- ```ts
685
- const page = await client.content.get({ entryID: "ent_123" });
686
- const definition = await client.atSnapshot(page.snapshotID).getSchema({
687
- entryID: page.id
688
- });
689
-
690
- // Use a hash saved by your application when it requires a specific definition.
691
- const checked = await client.content.get({
692
- entryID: page.id,
693
- snapshotID: page.snapshotID,
694
- expectedSchemaHash: "YOUR_SAVED_64_CHARACTER_SHA256_HASH"
695
- });
696
- ```
697
-
698
- `content.getSchema()` uses published-content access. It returns the selected
699
- entry's recorded definition or `null`. `schemas.getRevision({ revisionID })`
700
- requires collection-read access and returns an exact revision, including inactive
701
- revisions. Both return `{ revisionID, hash, definition }`. The definition combines
702
- inherited fields without internal source metadata and matches the recorded hash.
703
-
704
- `expectedSchemaHash` is also available on `entries.get()`, `versions.get()`, and
705
- `publishing.getEntryVersion()`. A mismatch, including content without a schema,
706
- returns `CONTENT_SCHEMA_MISMATCH` (409) with the expected hash and actual metadata.
707
- Invalid stored content returns `CONTENT_SCHEMA_INVALID` (500) on delivery or (409)
708
- on publication, with issue paths and available entry, version, and revision IDs.
709
- `SCHEMA_FIELD_KEY_CONFLICT` (409) identifies conflicting derived field keys.
710
- Use `error.is(code)` to read the generated error data types. Schema definitions
711
- remain readable when their associated document is invalid.
712
-
713
- ## Type-generation metadata
714
-
715
- Read collection/schema metadata in bulk without fetching content bodies:
716
-
717
- ```ts
718
- const published = await client.typeMetadata.getPublished({
719
- collections: ["/Tutorials"],
720
- channel: "published",
721
- includeEntries: true
722
- });
723
- const current = await client.typeMetadata.getCurrent({
724
- collections: ["/Tutorials"],
725
- includeTree: true
726
- });
727
- ```
728
-
729
- Selectors accept IDs, decoded name paths, or `{ collectionSlugPath: "/docs" }`
730
- objects and include descendant collections. An
731
- empty list selects all accessible collections. Each response is one consistent
732
- database snapshot; published metadata also returns its publication snapshot ID
733
- for related reads. Published paths and schemas come from the assigned versions,
734
- including mixed revisions and schema-free entries. A collection without recorded
735
- schemas uses a general type without a warning. Current metadata uses active
736
- effective schemas and rejects selected collections with an active schema migration.
737
-
738
- `revisions` contains the referenced effective definitions, including inherited
739
- fields. Each collection lists its `schemaRevisionIDs`: `null` means schema-free
740
- content, while an empty array means there is no recorded schema evidence.
741
- `includeEntries` adds entry identifiers, names, paths, and revision associations;
742
- `includeTree` also includes entries and adds child IDs in display order. Current
743
- entry/tree metadata requires `read:entries` in addition to `read:collections`.
744
- Published metadata follows the existing `read:publishing` access rules.
745
-
746
- The `fingerprint` changes with the returned type metadata. It excludes content
747
- bodies, snapshot IDs, expiry, and entry/tree data that was not requested. These
748
- endpoints provide metadata only; `andesine types generate` creates project declarations.
749
-
750
- ## Workspace content types
751
-
752
- `createClient<Workspace>()` accepts a type-only workspace map. Generate it with
753
- `andesine types generate`. A small map can also be defined directly:
754
-
755
- ```ts
756
- import {
757
- createClient,
758
- toStructuredContent,
759
- type EntryFragment,
760
- type WorkspaceCollection,
761
- type WorkspaceTypeMap
762
- } from "@andesine/sdk";
763
-
764
- interface Tutorials extends WorkspaceCollection {
765
- id: "coll_tutorials";
766
- path: "/Tutorials";
767
- slugPath: "/tutorials";
768
- schemaRevisionIDs: readonly ["schr_tutorial"];
769
- descendants: readonly [];
770
- }
771
-
772
- interface Workspace extends WorkspaceTypeMap {
773
- source: { kind: "published"; channel: "published" };
774
- collections: { "coll_tutorials": Tutorials; "/Tutorials": Tutorials; "/tutorials": Tutorials };
775
- schemas: {
776
- schr_tutorial: {
777
- hash: "RECORDED_SCHEMA_HASH";
778
- properties: {
779
- title: { name: "Title"; type: "text"; value: string };
780
- duration?: { name: "Duration"; type: "number"; value: number | null };
781
- };
782
- fragments: { body: EntryFragment };
783
- };
784
- };
785
- }
786
-
787
- const client = createClient<Workspace>();
788
-
789
- for await (const entry of client.content.paginateEntries({
790
- collectionPath: "/Tutorials",
791
- channel: "published",
792
- includeContent: true
793
- })) {
794
- const tutorial = toStructuredContent(entry);
795
- const title: string = tutorial.properties.title;
796
- const duration: number | null | undefined = tutorial.properties.duration;
797
- }
798
- ```
799
-
800
- Replace the example IDs, hash, and fields with the recorded metadata. Collections
801
- can have several revision IDs; results then preserve each revision's fields as a
802
- union. `null` represents schema-free content. An empty revision tuple uses general
803
- content types. `descendants` contains all descendant collection keys; recursive
804
- lists include their shapes. If the hierarchy is incomplete, recursive results
805
- also retain the general content type.
806
-
807
- Exact entry reads narrow only when the map has an `entries` binding for the given
808
- ID, path, or slug path. Each binding supplies `collection` and `schemaRevisionID`. Optional
809
- `tree` bindings supply published tree shapes by collection selector. Unknown keys
810
- and dynamic strings keep general SDK types. Current maps apply to `entries.get`;
811
- published maps apply to content delivery. Summary lists do not gain content fields.
812
- Snapshots, full responses, pagination, and `toStructuredContent` preserve the map.
813
-
814
- `SchemaContent<Workspace, "schr_tutorial">` exposes one revision's content fields.
815
- `OperationOutput<"content.get", Workspace, { path: "/Tutorials/First" }>` exposes
816
- a full result when that entry path is bound. For mixed revisions, TypeScript does
817
- not narrow the parent entry after a nested `entry.schema.revisionID` check. Use a
818
- type predicate with `Extract<Result, { schema: { revisionID: "schr_tutorial" } }>`
819
- to narrow the entry. Collection lists can also narrow on their top-level
820
- `collectionID` when the collections have distinct IDs.
821
-
822
- The map makes no requests and changes no request options. Supply the workspace,
823
- channel, and snapshot through the usual runtime options. The types assume those
824
- options and the server content match the map; they do not validate responses or
825
- detect stale generated types. Different clients can use different maps without
826
- module augmentation. Plain `createClient()` keeps the general SDK types.
827
-
828
- ## Operation documentation and error hints
829
-
830
- All public methods include generated summaries, descriptions, API key permissions,
831
- and input examples in editor tooltips. The same examples appear in OpenAPI parameters
832
- and request bodies. IDs and file contents in examples are placeholders; replace them
833
- with values from your instance. Role permissions differ from API key permissions.
23
+ Keep secret API keys in server code. In browsers, use a publishable key (`adn_pk_...`), which can
24
+ only read and search published content.
834
25
 
835
- Error declarations come from the backend contracts. `error.is(code)` narrows `data`
836
- only when the server marks the error as defined by its contract. An undeclared error,
837
- proxy response, or network failure does not become a typed domain error. Handle the
838
- remaining error cases as well. `data` stays `unknown` until narrowed.
839
-
840
- `APIErrorCode`, `APIErrorBody`, `APIErrorData<Code>`, and
841
- `OperationError<"entries.create">` are available for application types. Operation
842
- aliases such as `EntriesCreateError` are also available as
843
- `Andesine.EntriesCreateError`. These types describe declared errors, not every
844
- possible failure. TypeScript does not enforce the types of thrown errors.
845
-
846
- `data.hints` is optional advice. `error.hints` returns an empty array when no hints
847
- are present. Use stable codes and structured data for program logic; never parse
848
- hint text. Hints do not trigger retries. Automatic retries remain disabled by default.
849
-
850
- Common errors can include validation `issues`, `missingPermissions`, `requiredPlan`,
851
- or `retryAfterSeconds`. Domain errors include `SCHEMA_MIGRATION_IN_PROGRESS`,
852
- `PUBLISHING_SNAPSHOT_CHANGED`, `ROLE_NAME_DUPLICATE`, `ROLE_NAME_INVALID`,
853
- `MEMBERSHIP_ALREADY_EXISTS`, and `INVITE_ALREADY_PENDING`. Migration and snapshot
854
- conflicts now use their specific codes instead of the generic `CONFLICT` code.
855
-
856
- Asset operation errors can also include the current `assetStatus`, expected and
857
- actual file sizes and checksums, or storage `limitBytes`, `usedBytes`, and
858
- `requiredBytes`. Storage reservations include processed image variants.
859
- These fields are declared on the applicable operation error types, such as
860
- `AssetsUploadError` and `AssetsRegisterError`; not every error with the same code
861
- contains asset data. With `AndesineAPIError`, check for the field before using it.
862
- Known rate-limit delays appear in both `data.retryAfterSeconds` and `Retry-After`.
863
- Concurrency limits have no fixed delay and only supply a hint.
864
-
865
- ## Streaming AI answers
866
-
867
- `search.askCurrentStream()` and `search.askPublishedStream()` return a promise of
868
- an async iterator. They require the same permissions and inputs as their complete
869
- answer methods. Published source types require `snapshotID`, `versionID`, and
870
- `channel`. `AnswerEvent` and `PublishedAnswerEvent` are available directly and
871
- through `Andesine`.
872
-
873
- ```ts
874
- const controller = new AbortController();
875
- const events = await client.search.askPublishedStream(
876
- { channel: "published", collectionPath: "/Docs", question: "How do I install Andesine?" },
877
- { signal: controller.signal, timeout: 120_000 }
878
- );
879
-
880
- for await (const event of events) {
881
- if (event.type === "sources") console.log(event.sources);
882
- if (event.type === "textDelta") process.stdout.write(event.text);
883
- if (event.type === "completed") console.log(event.answer);
884
- }
885
- ```
886
-
887
- The order is one `sources` event, non-empty `textDelta` events, then one
888
- `completed` event. A no-source answer may complete without deltas. The completed
889
- answer contains the final text and sources. Deltas contain plain text, including
890
- any Markdown syntax; choose how to render it in your application.
891
-
892
- Use `controller.abort()` to cancel, or `break` from the loop to stop early. If you
893
- open a stream but do not iterate it, call `await events.return?.()` to close it.
894
- The request timeout covers both opening and consuming the stream; `timeout: 0`
895
- disables it. The SDK never reconnects or retries generation, even if read retries
896
- are enabled. `response: "full"` returns the iterator in `data`, with the actual
897
- response status and headers.
898
-
899
- Errors before SSE headers are normal `AndesineAPIError` failures. A declared
900
- error after headers throws `AndesineStreamError`, which extends `AndesineAPIError`
901
- and supports `.is()`, `code`, `data`, and `hints`. Its `status` is the error status
902
- from the frame; `response.status` remains the actual HTTP status, usually 200.
903
- Malformed streams throw `AndesineStreamProtocolError` with `INVALID_STREAM`.
904
- Missing completion or an interrupted body uses `INCOMPLETE_STREAM`. These are
905
- local protocol errors, not API errors with an invented HTTP status. Abort and
906
- timeout reasons are preserved.
907
-
908
- A stream is counted when generation starts, including generation that later fails
909
- or is cancelled. Source preparation alone is not charged. The rate limit, quota, and
910
- spending limit checks run before the stream opens. Usage headers show the allowance
911
- when the stream opens; chunks do not update headers or create more usage records.
912
-
913
- ### Forward a stream from your server
914
-
915
- The optional `@andesine/sdk/streaming` entry has no framework dependency. It uses
916
- standard Fetch types, preserves backpressure and cancellation, and forwards no
917
- upstream headers or credentials.
918
-
919
- ```ts
920
- import { toAnswerResponse } from "@andesine/sdk/streaming";
921
-
922
- async function answerRequest(request: Request): Promise<Response> {
923
- const { question } = await request.json();
924
- const events = await client.search.askPublishedStream(
925
- { channel: "published", collectionPath: "/Docs", question },
926
- { signal: request.signal, timeout: 120_000 }
927
- );
928
-
929
- return toAnswerResponse(events);
930
- }
931
- ```
932
-
933
- Keep the configured `client` and API key on your server. Handle errors before
934
- `toAnswerResponse()` with your application's normal HTTP error handler. Once
935
- streaming starts, the helper preserves typed API error frames and uses a generic
936
- error frame for other failures. Its response disables caching and requests that
937
- proxies do not buffer the stream. Your hosting platform must support streamed
938
- responses.
939
-
940
- A browser can read the forwarded response without an Andesine API key:
941
-
942
- ```ts
943
- import { readAnswerStream } from "@andesine/sdk/streaming";
944
-
945
- const controller = new AbortController();
946
- const response = await fetch("/api/ask", {
947
- method: "POST",
948
- headers: { "Content-Type": "application/json" },
949
- body: JSON.stringify({ question: "How do I install Andesine?" }),
950
- signal: controller.signal
951
- });
952
-
953
- for await (const event of await readAnswerStream(response, { signal: controller.signal })) {
954
- console.log(event);
955
- }
956
- ```
26
+ ## Documentation
957
27
 
958
- Both helpers use oRPC's SSE framing: `event: message` carries an answer event as
959
- JSON; `event: error` carries the error envelope. Completion is an application
960
- `completed` event. Comments are ignored, and a transport `done` event cannot
961
- replace application completion. Event IDs and retry fields never trigger
962
- reconnection.
28
+ - [SDK](https://andesine.app/docs/developers/sdk/)
29
+ - [API overview](https://andesine.app/docs/developers/api-overview/)
30
+ - [API reference](https://andesine.app/docs/api/)
31
+ - [Webhooks](https://andesine.app/docs/developers/webhooks/), and the
32
+ [webhook receivers guide](WEBHOOKS.md) in this package
963
33
 
964
- ## Webhooks
34
+ ## License
965
35
 
966
- Manage endpoints and delivery history through `client.webhooks`. Verify incoming
967
- requests in server code with `verifyWebhook` or `verifyWebhookRequest` from
968
- `@andesine/sdk/webhooks`. Both helpers return a validated, typed event after
969
- authenticating the unchanged body and checking the five-minute timestamp window.
970
- See [webhook setup, receivers, rotation, and replay](WEBHOOKS.md).
36
+ MIT