@andesine/sdk 0.1.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0
4
+
5
+ - Document the limits: per-minute rate limits, the monthly API call and AI credit
6
+ meters, the Pro spending limit, the `X-RateLimit-*`, `X-API-Usage*`, and
7
+ `X-AI-Credits*` headers, and the new `limit` field of `TOO_MANY_REQUESTS` error
8
+ data (`rate`, `api-calls`, `ai-credits`, or `spending`). `X-RateLimit-*` now
9
+ describes the rate limit, not the monthly usage.
10
+
11
+ - Document publishable keys (`adn_pk_...`) for browser use: published content
12
+ reads, search, and answers from the key's collections and origins, and server
13
+ reads without an `Origin`, e.g. site builds.
14
+
15
+ ## 0.3.0
16
+
17
+ - Add generated webhook methods for management (including bulk enable/disable and
18
+ delete), delivery history, signed tests, and single or bulk replay, plus public
19
+ webhook event types.
20
+ - Add the isolated `@andesine/sdk/webhooks` verifier with raw-body authentication,
21
+ timestamp and schema checks, plus receiver and durable deduplication examples.
22
+
23
+ ## 0.2.0
24
+
25
+ - Add derived `slugPath` selectors and responses, slug-based sibling uniqueness,
26
+ slug-aware workspace maps, and slug URLs by default in `toContentURL`.
27
+
28
+ - Add type-only workspace maps through `createClient<Workspace>()`, with selector-aware
29
+ content types, revision unions, optional entry/tree bindings, and typed content helpers.
30
+ - Add bulk current/published type metadata with effective schema revisions,
31
+ optional entries/tree, consistent source reads, and stable fingerprints.
32
+ - Explicit OAuth access tokens without API-key environment fallback, plus credential identity and user workspace discovery.
33
+
3
34
  ## 0.1.0
4
35
 
5
36
  - Initial public TypeScript client generated from the Andesine OpenAPI document.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  MIT-licensed TypeScript client for the public Andesine HTTP API. ESM only. Requires
4
4
  Node.js 22 or later, or a browser/worker runtime with Fetch, Blob, FormData, and
5
- AbortSignal.any. Keep private API keys in server code.
5
+ AbortSignal.any. Keep secret API keys in server code; browsers use publishable keys.
6
6
 
7
7
  ## Use
8
8
 
@@ -25,13 +25,51 @@ const published = await client.content.get({
25
25
  });
26
26
  ```
27
27
 
28
- `baseURL` is required. A trailing slash is optional; path prefixes are preserved.
28
+ `baseURL` defaults to `https://api.andesine.app`. A trailing slash is optional; path prefixes are preserved.
29
29
  Each client has its own configuration. You can supply `fetch` and default
30
30
  `headers` for proxies, instrumentation, or other authentication arrangements.
31
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.
34
+
35
+ ```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
46
+
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.
55
+
56
+ ```ts
57
+ const client = createClient({ apiKey: "adn_pk_..." });
58
+ const results = await client.search.published({ query: "install", channel: "published" });
59
+ ```
60
+
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
+
32
70
  Resource methods are generated from the checked-in OpenAPI document. They cover
33
71
  assets, entries, collections, content delivery, roles, search, schemas, schema
34
- migrations, schema versions, memberships, publishing, entry versions, and instance information. Internal
72
+ migrations, schema versions, memberships, publishing, entry versions, webhooks, and instance information. Internal
35
73
  session-only operations are excluded. Published asset delivery does not require
36
74
  an API key.
37
75
 
@@ -68,7 +106,7 @@ try {
68
106
  }
69
107
 
70
108
  if (error.is("TOO_MANY_REQUESTS")) {
71
- console.log(error.data?.retryAfterSeconds);
109
+ console.log(error.data?.limit, error.data?.retryAfterSeconds);
72
110
  }
73
111
  }
74
112
  }
@@ -89,6 +127,48 @@ Use full-response mode for conditional requests. A 304 response has
89
127
  `notModified: true` and no data. A 304 in data mode causes a clear error because
90
128
  the client does not store a response cache.
91
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; Pro
145
+ includes 500,000 and bills the rest.
146
+ - **AI credits:** an AI answer uses 3 credits, and a semantic search uses 1. Requests
147
+ from the app use credits too. Free includes 5,000; Pro includes 50,000 and bills
148
+ the rest.
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
+
92
172
  ## Cancellation, timeouts, and retries
93
173
 
94
174
  The default timeout is 30 seconds. Set `timeout: 0` to disable it. Timeouts include
@@ -361,9 +441,8 @@ public package imports the private editor or backend at runtime.
361
441
  From the repository root:
362
442
 
363
443
  ```sh
364
- pnpm --filter @andesine/backend openapi:export
365
- pnpm --filter @andesine/sdk generate
366
- pnpm --filter @andesine/sdk build
444
+ pnpm api:generate
445
+ pnpm build --filter=@andesine/sdk
367
446
  ```
368
447
 
369
448
  The export calls oRPC's OpenAPI generator on the same contracts implemented by
@@ -403,7 +482,8 @@ keeps runtime dependencies small.
403
482
  ## Content names
404
483
 
405
484
  Sibling entries and collections share names. Names are trimmed, normalized to
406
- Unicode NFC, and compared case-sensitively. Names cannot contain `/` or equal
485
+ Unicode NFC, and must have unique derived URL slugs at each level. Exact names
486
+ also remain unique, including symbol-only names. Names cannot contain `/` or equal
407
487
  `.` or `..`. The limit is 300 JavaScript string units. The collection name `~`
408
488
  is reserved for the internal workspace root. Root entries and root collections
409
489
  share the same level.
@@ -413,20 +493,20 @@ Use the returned name rather than assuming that your requested name was saved.
413
493
  Rename, move, and restore operations reject conflicts. Deleted items release
414
494
  names; a schema move temporarily reserves its source name for rollback.
415
495
 
416
- `CONTENT_NAME_CONFLICT` (409) includes `name`, `parentID`, and optional `hints`.
496
+ `CONTENT_NAME_CONFLICT` (409) includes `name`, `slug`, `parentID`, and optional `hints`.
417
497
  Use `error.is("CONTENT_NAME_CONFLICT")` to access typed data. Renaming never
418
498
  chooses a different name for you.
419
499
 
420
500
  ## Content paths
421
501
 
422
- Read an entry by ID or by path. Use exactly one selector:
502
+ Read an entry by ID, `path`, or `slugPath`. Use exactly one selector:
423
503
 
424
504
  ```ts
425
505
  const current = await client.entries.get({ path: "/Docs/Getting started" });
426
- const tree = await client.content.getTree({ collectionPath: "/Docs" });
506
+ const tree = await client.content.getTree({ collectionSlugPath: "/docs" });
427
507
  const content = client.atSnapshot(tree.snapshotID);
428
- const page = await content.get({ path: "/Docs/Getting started" });
429
- const definition = await content.getSchema({ path: page.path });
508
+ const page = await content.get({ slugPath: "/docs/getting-started" });
509
+ const definition = await content.getSchema({ slugPath: page.slugPath });
430
510
  const activeSchema = await client.schemas.get({ collectionPath: "/Docs" });
431
511
  ```
432
512
 
@@ -441,23 +521,56 @@ selected current tree or snapshot. A name that looks like an ID is still a name
441
521
  when it occurs in an absolute path. A bare collection ID is also a valid
442
522
  `collectionPath`. `/` selects the root. Neither selects an entry on its own.
443
523
 
444
- Current entry and collection responses include their canonical absolute `path`.
524
+ Current entry and collection responses include their canonical absolute `path` and `slugPath`.
445
525
  Published content, trees, and list results use snapshot collection names and the
446
526
  titles of assigned versions. Draft renames, moves, and deletions do not change
447
527
  those snapshot paths. Full published content also includes `id` and `collectionID`.
448
528
  Saved versions remain ID-based because a version alone has no snapshot location.
449
529
 
450
530
  `content.getTree({ collectionPath: "/" })` returns a virtual root collection with
451
- `id: null`, `name: ""`, and `path: "/"`. It contains top-level entries and collections.
531
+ `id: null`, `name: ""`, `path: "/"`, and `slugPath: "/"`. It contains top-level entries and collections.
452
532
 
453
533
  `entries.list`, `collections.list`, `content.listEntries`, and
454
- `content.listCollections` accept either `collectionID` or `collectionPath` to
534
+ `content.listCollections` accept one of `collectionID`, `collectionPath`, or `collectionSlugPath` to
455
535
  select direct children. Keep the same scope while paging. With no scope,
456
536
  `collections.list` selects root children; the other lists select all accessible
457
537
  items. `collections.list` now uses these fields instead of `ancestorID`.
458
538
  Search accepts the same collection selectors and includes descendants. Its `/`
459
539
  scope covers the whole accessible tree. Search results include a canonical entry
460
- `path` and an optional heading `anchor`.
540
+ `path`, `slugPath`, and an optional heading `anchor`.
541
+
542
+ Slugs use Unicode NFKC normalization, lowercase, Unicode letters/marks/numbers,
543
+ and single hyphens between other character groups. Accents and non-Latin letters
544
+ are retained. A symbol-only name uses its public ID in lowercase with `-` instead
545
+ of `_`. Slugs are derived, never edited separately. Renaming or moving content
546
+ changes its path; redirects are not stored. `Getting Started`, `getting-started`,
547
+ and `Getting Started!` conflict at the same level. Existing conflicting content
548
+ must be renamed before publishing it again.
549
+
550
+ `slugPath` and `collectionSlugPath` accept absolute paths and collection-ID
551
+ anchors, with the same snapshot and scope rules as name paths. Pass decoded
552
+ segments, for example `coll_ID/getting-started`. The URL route should be decoded
553
+ once by your framework before passing it to the SDK.
554
+
555
+ ```ts
556
+ import { toContentURL } from "@andesine/sdk";
557
+
558
+ // Prefer the API object: its slugPath also covers symbol-only ancestor names.
559
+ const url = toContentURL(page, { rootPath: "/Docs", baseURL: "/docs" });
560
+ // /docs/getting-started
561
+ const originalNames = toContentURL(page, { format: "name" });
562
+ // /Docs/Getting%20started
563
+ ```
564
+
565
+ `toContentURL` defaults to slug URLs. A string input derives slugs from the path;
566
+ API objects use their returned `slugPath`. `format: "name"` preserves original
567
+ names. `rootPath` accepts a name or slug prefix in slug mode. Destination prefixes
568
+ and heading anchors are not slugified. `toContentSlug(name, id?)` exposes the
569
+ shared segment rule; symbol-only names require a public content ID. It is also
570
+ available from `@andesine/sdk/slug`, which loads only the slug helper.
571
+
572
+ Search indexes must be rebuilt after this upgrade to populate `slugPath` on
573
+ existing results and AI sources. No database schema migration is needed.
461
574
 
462
575
  HTTP reads use query selectors on `GET /entries/get`, `GET /content/entries/get`,
463
576
  `GET /content/tree`, and `GET /schemas/collection`. The published schema route
@@ -473,7 +586,7 @@ publication leaves the channel and its snapshot unchanged.
473
586
  ## Search and source links
474
587
 
475
588
  Published search results include required `channel`, `snapshotID`, `versionID`,
476
- and canonical entry `path` fields. `entryID` remains the stable identity.
589
+ and canonical entry `path` and `slugPath` fields. `entryID` remains the stable identity.
477
590
  An optional `anchor` identifies a heading in that version; a result before any
478
591
  heading has no anchor. Current results have their indexed working path. Search
479
592
  index updates are asynchronous, so current paths can briefly lag draft changes.
@@ -488,9 +601,8 @@ for (const result of matches.results) {
488
601
  const page = await client.atSnapshot(result.snapshotID).get({
489
602
  entryID: result.entryID
490
603
  });
491
- const pathname = result.path.split("/").map(encodeURIComponent).join("/");
492
- const link = pathname + (result.anchor ? `#${encodeURIComponent(result.anchor)}` : "");
493
- // Map pathname to your docs site's route and render page with matching heading IDs.
604
+ const link = toContentURL(result, { rootPath: "/Docs", baseURL: "/docs" });
605
+ // Render page headings with matching IDs.
494
606
  }
495
607
  ```
496
608
 
@@ -547,9 +659,9 @@ Both complete and streamed answers use the same inputs and source-selection rule
547
659
 
548
660
  The limits are 1,000 characters per question, 10 history messages of up to 4,000
549
661
  characters each, and 20 property filters. History is supplied per request; the SDK
550
- does not store it. Trim history before sending a later question. The existing Ask
551
- AI rate limit is 10 requests per 60 seconds per credential. API usage metering
552
- is unchanged: a successful complete response counts once when billing is enabled.
662
+ does not store it. Trim history before sending a later question. Each answer uses 3
663
+ AI credits and has its own rate limit; see [Limits and usage](#limits-and-usage). A
664
+ complete response is counted when it succeeds.
553
665
  A rate-limit error includes `Retry-After` and `data.retryAfterSeconds`. Provider
554
666
  failures return `SERVICE_UNAVAILABLE` with hints. Answer POST requests are never
555
667
  automatically retried, even when read retries are enabled.
@@ -590,6 +702,121 @@ on publication, with issue paths and available entry, version, and revision IDs.
590
702
  Use `error.is(code)` to read the generated error data types. Schema definitions
591
703
  remain readable when their associated document is invalid.
592
704
 
705
+ ## Type-generation metadata
706
+
707
+ Read collection/schema metadata in bulk without fetching content bodies:
708
+
709
+ ```ts
710
+ const published = await client.typeMetadata.getPublished({
711
+ collections: ["/Tutorials"],
712
+ channel: "published",
713
+ includeEntries: true
714
+ });
715
+ const current = await client.typeMetadata.getCurrent({
716
+ collections: ["/Tutorials"],
717
+ includeTree: true
718
+ });
719
+ ```
720
+
721
+ Selectors accept IDs, decoded name paths, or `{ collectionSlugPath: "/docs" }`
722
+ objects and include descendant collections. An
723
+ empty list selects all accessible collections. Each response is one consistent
724
+ database snapshot; published metadata also returns its publication snapshot ID
725
+ for related reads. Published paths and schemas come from the assigned versions,
726
+ including mixed revisions and schema-free entries. A collection without recorded
727
+ schemas uses a general type without a warning. Current metadata uses active
728
+ effective schemas and rejects selected collections with an active schema migration.
729
+
730
+ `revisions` contains the referenced effective definitions, including inherited
731
+ fields. Each collection lists its `schemaRevisionIDs`: `null` means schema-free
732
+ content, while an empty array means there is no recorded schema evidence.
733
+ `includeEntries` adds entry identifiers, names, paths, and revision associations;
734
+ `includeTree` also includes entries and adds child IDs in display order. Current
735
+ entry/tree metadata requires `read:entries` in addition to `read:collections`.
736
+ Published metadata follows the existing `read:publishing` access rules.
737
+
738
+ The `fingerprint` changes with the returned type metadata. It excludes content
739
+ bodies, snapshot IDs, expiry, and entry/tree data that was not requested. These
740
+ endpoints provide metadata only; `andesine types generate` creates project declarations.
741
+
742
+ ## Workspace content types
743
+
744
+ `createClient<Workspace>()` accepts a type-only workspace map. Generate it with
745
+ `andesine types generate`. A small map can also be defined directly:
746
+
747
+ ```ts
748
+ import {
749
+ createClient,
750
+ toStructuredContent,
751
+ type EntryFragment,
752
+ type WorkspaceCollection,
753
+ type WorkspaceTypeMap
754
+ } from "@andesine/sdk";
755
+
756
+ interface Tutorials extends WorkspaceCollection {
757
+ id: "coll_tutorials";
758
+ path: "/Tutorials";
759
+ slugPath: "/tutorials";
760
+ schemaRevisionIDs: readonly ["schr_tutorial"];
761
+ descendants: readonly [];
762
+ }
763
+
764
+ interface Workspace extends WorkspaceTypeMap {
765
+ source: { kind: "published"; channel: "published" };
766
+ collections: { "coll_tutorials": Tutorials; "/Tutorials": Tutorials; "/tutorials": Tutorials };
767
+ schemas: {
768
+ schr_tutorial: {
769
+ hash: "RECORDED_SCHEMA_HASH";
770
+ properties: {
771
+ title: { name: "Title"; type: "text"; value: string };
772
+ duration?: { name: "Duration"; type: "number"; value: number | null };
773
+ };
774
+ fragments: { body: EntryFragment };
775
+ };
776
+ };
777
+ }
778
+
779
+ const client = createClient<Workspace>();
780
+
781
+ for await (const entry of client.content.paginateEntries({
782
+ collectionPath: "/Tutorials",
783
+ channel: "published",
784
+ includeContent: true
785
+ })) {
786
+ const tutorial = toStructuredContent(entry);
787
+ const title: string = tutorial.properties.title;
788
+ const duration: number | null | undefined = tutorial.properties.duration;
789
+ }
790
+ ```
791
+
792
+ Replace the example IDs, hash, and fields with the recorded metadata. Collections
793
+ can have several revision IDs; results then preserve each revision's fields as a
794
+ union. `null` represents schema-free content. An empty revision tuple uses general
795
+ content types. `descendants` contains all descendant collection keys; recursive
796
+ lists include their shapes. If the hierarchy is incomplete, recursive results
797
+ also retain the general content type.
798
+
799
+ Exact entry reads narrow only when the map has an `entries` binding for the given
800
+ ID, path, or slug path. Each binding supplies `collection` and `schemaRevisionID`. Optional
801
+ `tree` bindings supply published tree shapes by collection selector. Unknown keys
802
+ and dynamic strings keep general SDK types. Current maps apply to `entries.get`;
803
+ published maps apply to content delivery. Summary lists do not gain content fields.
804
+ Snapshots, full responses, pagination, and `toStructuredContent` preserve the map.
805
+
806
+ `SchemaContent<Workspace, "schr_tutorial">` exposes one revision's content fields.
807
+ `OperationOutput<"content.get", Workspace, { path: "/Tutorials/First" }>` exposes
808
+ a full result when that entry path is bound. For mixed revisions, TypeScript does
809
+ not narrow the parent entry after a nested `entry.schema.revisionID` check. Use a
810
+ type predicate with `Extract<Result, { schema: { revisionID: "schr_tutorial" } }>`
811
+ to narrow the entry. Collection lists can also narrow on their top-level
812
+ `collectionID` when the collections have distinct IDs.
813
+
814
+ The map makes no requests and changes no request options. Supply the workspace,
815
+ channel, and snapshot through the usual runtime options. The types assume those
816
+ options and the server content match the map; they do not validate responses or
817
+ detect stale generated types. Different clients can use different maps without
818
+ module augmentation. Plain `createClient()` keeps the general SDK types.
819
+
593
820
  ## Operation documentation and error hints
594
821
 
595
822
  All public methods include generated summaries, descriptions, API key permissions,
@@ -670,11 +897,10 @@ Missing completion or an interrupted body uses `INCOMPLETE_STREAM`. These are
670
897
  local protocol errors, not API errors with an invented HTTP status. Abort and
671
898
  timeout reasons are preserved.
672
899
 
673
- A stream counts as one API call when generation starts, including generation that
674
- later fails or is cancelled. Source preparation alone is not charged. The existing
675
- Ask AI rate limit and quota checks run before opening the stream. Usage headers
676
- show the allowance when the stream opens; chunks do not update headers or create
677
- additional usage records.
900
+ A stream is counted when generation starts, including generation that later fails
901
+ or is cancelled. Source preparation alone is not charged. The rate limit, quota, and
902
+ spending limit checks run before the stream opens. Usage headers show the allowance
903
+ when the stream opens; chunks do not update headers or create more usage records.
678
904
 
679
905
  ### Forward a stream from your server
680
906
 
@@ -726,3 +952,11 @@ JSON; `event: error` carries the error envelope. Completion is an application
726
952
  `completed` event. Comments are ignored, and a transport `done` event cannot
727
953
  replace application completion. Event IDs and retry fields never trigger
728
954
  reconnection.
955
+
956
+ ## Webhooks
957
+
958
+ Manage endpoints and delivery history through `client.webhooks`. Verify incoming
959
+ requests in server code with `verifyWebhook` or `verifyWebhookRequest` from
960
+ `@andesine/sdk/webhooks`. Both helpers return a validated, typed event after
961
+ authenticating the unchanged body and checking the five-minute timestamp window.
962
+ See [webhook setup, receivers, rotation, and replay](WEBHOOKS.md).