@andesine/sdk 0.6.0 → 0.8.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,111 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0
4
+
5
+ - `collections.get` returns one collection by `collectionID`, `collectionPath`, or
6
+ `collectionSlugPath`, with its name, path, and ancestors (`read:collections`).
7
+ - `extensions.listElements` lists the custom elements of the workspace's active extensions (e.g.
8
+ Tabs): their props, whether they have content, the blocks their content accepts, and the
9
+ elements that belong inside them (`read:entries`).
10
+ - `extensions.getSelf` and `extensions.verifySession` return `workspacePlan`, the workspace's
11
+ effective plan (`pro` on instances without billing), so extensions can offer plan features.
12
+ - Extension-managed publishable keys: `extensions.listSelfPublishableKeys`,
13
+ `createSelfPublishableKey`, `updateSelfPublishableKey`, and `deleteSelfPublishableKey`, for
14
+ extensions with the `publishable-keys` permission. The keys read published content of their
15
+ collections (and ask for AI answers with `answers` and the extension's `ai-answers` permission),
16
+ in their origins. Members can only delete them. They stop while the extension is disabled or
17
+ revoked; losing `publishable-keys` or `read:publishing`, or uninstalling, deletes them, and
18
+ losing `ai-answers` removes it.
19
+ - Keys have `extensionID`: the extension that manages the key, or `null`.
20
+ - `extensions.verifySession` takes optional `checks` (`{ collectionID, actions }`, up to 10) and
21
+ returns `checks` with `allowed` and the `denied` actions. An action is allowed when the
22
+ member can take it and the extension can read the collection, restricted collections included.
23
+ - `createExtensionBackend().verifySession(headers, { checks })` passes the checks; the results
24
+ are in `session.checks`.
25
+
26
+ ## 0.7.0
27
+
28
+ - `auth.getIdentity` returns `client` (ID and registered name) for OAuth access tokens.
29
+ - Webhook events have an `actor`: the member, API key, extension, or app (OAuth client ID and
30
+ registered name, with the member) that made the change, or `null` for changes by Andesine.
31
+ - OAuth for MCP clients: the authorization server supports the authorization code flow with
32
+ PKCE and dynamic client registration, with the scopes `andesine:read` and `andesine:write`.
33
+ `auth.getIdentity` returns `scopes` and `workspaceID` for such tokens.
34
+ - `auth.getIdentity` returns the `permissions` of an API key, and `instance.get` returns
35
+ `appURL`, the web app URL (entries open at `{appURL}/{workspaceID}/{entryID}`).
36
+ - `versions.create` takes an optional `document`: ProseMirror JSON checked against the editor
37
+ and the entry's collection schema. The version has the new reason `import`, and the
38
+ current content does not change.
39
+ - `entries.get` returns `hash`, the hash of the current content, comparable to version hashes.
40
+ - Add `entries.move` and `collections.move` (API key permissions `entries` and
41
+ `collections`). Without `order`, a moved entry keeps its position in the same collection
42
+ and goes last in a new one.
43
+ - `entries.update` replaces the content with `document` and `expectedHash`, the hash of the
44
+ content it replaces. Only the changed blocks change, so live edits elsewhere are kept. A
45
+ different current hash returns the new error `CONTENT_HASH_MISMATCH` (409) with the current
46
+ `hash`. Current content without a version is first saved as an `auto` version, and `version`
47
+ also saves the new content as a `manual` version. Use `name` or `document`, not both. The
48
+ result is `{ hash, version }`; before, it was empty.
49
+ - `entries.create` takes an optional `document` in place of `name`; its title sets the name.
50
+ - MDX content: `entries.get`, `versions.get`, and `content.get` take `contentFormat: "mdx"`
51
+ and then also return the content as MDX in `mdx`: the title as H1, properties as
52
+ `<Property name value />` elements in their position, fragments as `<Fragment name>`
53
+ elements, and images as `asset:<assetID>` references (delivery URLs in published content).
54
+ `entries.create`, `entries.update`, and `versions.create` take `mdx` in place of `document`;
55
+ frontmatter properties are also read. Fields match by name or key. Node IDs and attributes
56
+ that MDX doesn't hold are kept from the current content. Invalid MDX returns the new error
57
+ `CONTENT_FORMAT_INVALID` (400) with the line and column.
58
+ - `Fragment` and `Property` are reserved element names; content writes with them return
59
+ `CONTENT_SCHEMA_INVALID`.
60
+ - `entries.update` takes `operations`, applied in order with all or none applied:
61
+ `replaceText` (`find` must occur once in one block's MDX), `replaceBlock`, `insertBlocks`
62
+ (`before`, `after`, or `position` with an optional `fragment`), `deleteBlocks`, and `setField`
63
+ (a property value, or MDX for a fragment). Anchors come from `entries.get` with
64
+ `contentFormat: "blocks"`, which returns the blocks with IDs, fragment keys, and MDX.
65
+ `expectedHash` is optional with operations; `name` can be combined with them. A missing or
66
+ ambiguous anchor returns the new error `CONTENT_OPERATION_INVALID` (409) with the operation
67
+ `index` and `reason`.
68
+ - Internal links: link marks can point to entries with `entry:<entryID>`, and an optional
69
+ `#anchor`. Content writes also accept `entry:<path>` (a path or slug path) and store the entry
70
+ ID; a link to a missing entry returns the new error `CONTENT_LINK_INVALID` (409).
71
+ `entries.get` and `versions.get` return `references`: the path, slug path, title, and
72
+ collection of each linked entry, or `available: false` for deleted and unreadable entries.
73
+ MDX and blocks show links by slug path. Published content resolves links to the entry's slug
74
+ path in the snapshot, and removes links to entries that aren't published (the text stays).
75
+ - Images follow content: content writes attach the `asset:<assetID>` images they newly
76
+ reference (the caller must be able to read each image and update the entry), so
77
+ `assets.attach` is no longer part of the public API. `entries.update` and `versions.create`
78
+ also import image URLs in MDX and wait until the images are ready; an image that fails or is
79
+ still processing returns the new error `CONTENT_IMAGE_UNAVAILABLE` (409). `entries.create`
80
+ accepts only `asset:` images. To remove an image, remove it from the content.
81
+ - `entries.create` takes `path`: a full name path. Missing parent collections are created with
82
+ exact names; a name already used at the path returns `CONTENT_NAME_CONFLICT`.
83
+ - Target collections by path: `entries.create` and `entries.move` take `collectionPath` or
84
+ `collectionSlugPath` in place of `collectionID`, `collections.create` takes `parentPath` or
85
+ `parentSlugPath`, `collections.move` takes `newParentPath` or `newParentSlugPath` (`/` is the
86
+ top level), and `publishing.getChannelContent` takes `collectionPath` or `collectionSlugPath`.
87
+ These select existing collections only.
88
+ - Preconditions: `expectedName` on `entries.update` and `collections.update`,
89
+ `expectedCollectionID` on `entries.move`, and `expectedParentID` on `collections.move`. A
90
+ different current value returns the new error `CONTENT_PRECONDITION_FAILED` (409) with the
91
+ current value.
92
+ - `Idempotency-Key` header on `entries.create`, `entries.update`, and `versions.create`: a
93
+ repeated request returns the first result for 24 hours (with `Idempotent-Replayed: true`).
94
+ The same key with a different body returns `IDEMPOTENCY_KEY_REUSED` (422), and a repeat while
95
+ the first request runs returns `IDEMPOTENCY_KEY_IN_PROGRESS` (409). The SDK sends a generated
96
+ key for these operations unless `headers` has one, and `retries` now also applies to them, including a retry that
97
+ arrives while the first attempt still runs (`IDEMPOTENCY_KEY_IN_PROGRESS`, with
98
+ `Retry-After: 1`).
99
+ - Add `entries.bulkUpdate`: the items of `entries.update` for many entries in one call (a name
100
+ without content is written as the title). All items are checked before any content changes,
101
+ and all or none apply; errors name the failing item. Supports `Idempotency-Key`.
102
+ - Add `entries.bulkMove`: the items of `entries.move` in one call, all or none. Moves that need a
103
+ schema migration aren't supported; use `entries.move` for them.
104
+ - Add `publishing.updateChannel`: renames a channel; its code doesn't change.
105
+ - `entries.create` takes `fields`: property values and fragment MDX by field name or key.
106
+ - `entries.get` returns the content of a document that is open in the editor, including edits
107
+ that aren't saved yet, so its `hash` matches what content writes compare against.
108
+
3
109
  ## 0.6.0
4
110
 
5
111
  - No changes; versioned with the other public Andesine packages.
package/README.md CHANGED
@@ -176,9 +176,17 @@ request execution, response reading, and retry delays. Supply `signal` per call
176
176
  for cancellation.
177
177
 
178
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. Reads retry network `TypeError` failures
180
- and HTTP 429, 500, 502, 503, and 504. They respect `Retry-After`, otherwise use
181
- increasing delays with jitter. Writes are never retried automatically.
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.
182
190
 
183
191
  ```ts
184
192
  await client.entries.get(
package/WEBHOOKS.md CHANGED
@@ -275,6 +275,13 @@ or restricted resources can become unavailable. Publication snapshot references
275
275
  can expire under the workspace policy; webhooks do not pin snapshots. Handle
276
276
  missing references explicitly.
277
277
 
278
+ Each event has an `actor`: who made the change. It is a member (`memberID`), an
279
+ API key (`keyID`), an extension (`extensionID`), or an app such as an MCP client
280
+ (`clientID`, the `name` that the app registered, and the authorizing `memberID`).
281
+ It is `null` for changes by Andesine itself, and for editor saves that combine
282
+ several people's edits. App names are chosen by the app; don't use them for
283
+ access decisions. An extension's own webhooks don't receive its changes.
284
+
278
285
  ## Rotation, replay, and recovery
279
286
 
280
287
  Normal rotation returns a new secret once and signs with both old and new keys
@@ -124,16 +124,6 @@ const createResources = (request) => ({
124
124
  binary: false,
125
125
  anonymous: false
126
126
  }),
127
- attach: operation(request, {
128
- method: "post",
129
- path: "/assets/{assetID}/attachments",
130
- pathParams: ["assetID"],
131
- queryParams: [],
132
- body: true,
133
- multipart: false,
134
- binary: false,
135
- anonymous: false
136
- }),
137
127
  register: operation(request, {
138
128
  method: "post",
139
129
  path: "/assets/uploads",
@@ -187,6 +177,7 @@ const createResources = (request) => ({
187
177
  }) },
188
178
  entries: {
189
179
  create: operation(request, {
180
+ idempotent: true,
190
181
  method: "post",
191
182
  path: "/entries",
192
183
  pathParams: [],
@@ -217,6 +208,7 @@ const createResources = (request) => ({
217
208
  anonymous: false
218
209
  }),
219
210
  update: operation(request, {
211
+ idempotent: true,
220
212
  method: "put",
221
213
  path: "/entries/{id}",
222
214
  pathParams: ["id"],
@@ -226,6 +218,37 @@ const createResources = (request) => ({
226
218
  binary: false,
227
219
  anonymous: false
228
220
  }),
221
+ bulkUpdate: operation(request, {
222
+ idempotent: true,
223
+ method: "post",
224
+ path: "/entries/bulk/update",
225
+ pathParams: [],
226
+ queryParams: [],
227
+ body: true,
228
+ multipart: false,
229
+ binary: false,
230
+ anonymous: false
231
+ }),
232
+ move: operation(request, {
233
+ method: "post",
234
+ path: "/entries/{id}/move",
235
+ pathParams: ["id"],
236
+ queryParams: [],
237
+ body: true,
238
+ multipart: false,
239
+ binary: false,
240
+ anonymous: false
241
+ }),
242
+ bulkMove: operation(request, {
243
+ method: "post",
244
+ path: "/entries/bulk/move",
245
+ pathParams: [],
246
+ queryParams: [],
247
+ body: true,
248
+ multipart: false,
249
+ binary: false,
250
+ anonymous: false
251
+ }),
229
252
  get: operation(request, {
230
253
  method: "get",
231
254
  path: "/entries/get",
@@ -234,7 +257,8 @@ const createResources = (request) => ({
234
257
  "id",
235
258
  "path",
236
259
  "slugPath",
237
- "expectedSchemaHash"
260
+ "expectedSchemaHash",
261
+ "contentFormat"
238
262
  ],
239
263
  body: false,
240
264
  multipart: false,
@@ -309,6 +333,46 @@ const createResources = (request) => ({
309
333
  binary: false,
310
334
  anonymous: false
311
335
  }),
336
+ listSelfPublishableKeys: operation(request, {
337
+ method: "get",
338
+ path: "/extensions/self/publishable-keys",
339
+ pathParams: [],
340
+ queryParams: [],
341
+ body: false,
342
+ multipart: false,
343
+ binary: false,
344
+ anonymous: false
345
+ }),
346
+ createSelfPublishableKey: operation(request, {
347
+ method: "post",
348
+ path: "/extensions/self/publishable-keys",
349
+ pathParams: [],
350
+ queryParams: [],
351
+ body: true,
352
+ multipart: false,
353
+ binary: false,
354
+ anonymous: false
355
+ }),
356
+ updateSelfPublishableKey: operation(request, {
357
+ method: "patch",
358
+ path: "/extensions/self/publishable-keys/{keyID}",
359
+ pathParams: ["keyID"],
360
+ queryParams: [],
361
+ body: true,
362
+ multipart: false,
363
+ binary: false,
364
+ anonymous: false
365
+ }),
366
+ deleteSelfPublishableKey: operation(request, {
367
+ method: "delete",
368
+ path: "/extensions/self/publishable-keys/{keyID}",
369
+ pathParams: ["keyID"],
370
+ queryParams: [],
371
+ body: false,
372
+ multipart: false,
373
+ binary: false,
374
+ anonymous: false
375
+ }),
312
376
  getSelfStorageEntry: operation(request, {
313
377
  method: "get",
314
378
  path: "/extensions/self/storage/entry",
@@ -352,6 +416,16 @@ const createResources = (request) => ({
352
416
  multipart: false,
353
417
  binary: false,
354
418
  anonymous: false
419
+ }),
420
+ listElements: operation(request, {
421
+ method: "get",
422
+ path: "/extensions/elements",
423
+ pathParams: [],
424
+ queryParams: [],
425
+ body: false,
426
+ multipart: false,
427
+ binary: false,
428
+ anonymous: false
355
429
  })
356
430
  },
357
431
  collections: {
@@ -395,6 +469,30 @@ const createResources = (request) => ({
395
469
  binary: false,
396
470
  anonymous: false
397
471
  }),
472
+ move: operation(request, {
473
+ method: "post",
474
+ path: "/collections/{id}/move",
475
+ pathParams: ["id"],
476
+ queryParams: [],
477
+ body: true,
478
+ multipart: false,
479
+ binary: false,
480
+ anonymous: false
481
+ }),
482
+ get: operation(request, {
483
+ method: "get",
484
+ path: "/collections/get",
485
+ pathParams: [],
486
+ queryParams: [
487
+ "collectionID",
488
+ "collectionPath",
489
+ "collectionSlugPath"
490
+ ],
491
+ body: false,
492
+ multipart: false,
493
+ binary: false,
494
+ anonymous: false
495
+ }),
398
496
  list: operation(request, {
399
497
  method: "get",
400
498
  path: "/collections/list",
@@ -494,7 +592,8 @@ const createResources = (request) => ({
494
592
  "slugPath",
495
593
  "expectedSchemaHash",
496
594
  "channel",
497
- "snapshotID"
595
+ "snapshotID",
596
+ "contentFormat"
498
597
  ],
499
598
  body: false,
500
599
  multipart: false,
@@ -1148,12 +1247,26 @@ const createResources = (request) => ({
1148
1247
  method: "get",
1149
1248
  path: "/publishing/channels/{channel}/content",
1150
1249
  pathParams: ["channel"],
1151
- queryParams: ["collectionID"],
1250
+ queryParams: [
1251
+ "collectionID",
1252
+ "collectionPath",
1253
+ "collectionSlugPath"
1254
+ ],
1152
1255
  body: false,
1153
1256
  multipart: false,
1154
1257
  binary: false,
1155
1258
  anonymous: false
1156
1259
  }),
1260
+ updateChannel: operation(request, {
1261
+ method: "put",
1262
+ path: "/publishing/channels/{code}",
1263
+ pathParams: ["code"],
1264
+ queryParams: [],
1265
+ body: true,
1266
+ multipart: false,
1267
+ binary: false,
1268
+ anonymous: false
1269
+ }),
1157
1270
  deleteChannel: operation(request, {
1158
1271
  method: "delete",
1159
1272
  path: "/publishing/channels/{code}",
@@ -1177,6 +1290,7 @@ const createResources = (request) => ({
1177
1290
  }) },
1178
1291
  versions: {
1179
1292
  create: operation(request, {
1293
+ idempotent: true,
1180
1294
  method: "post",
1181
1295
  path: "/entries/{entryID}/versions",
1182
1296
  pathParams: ["entryID"],
@@ -1200,7 +1314,7 @@ const createResources = (request) => ({
1200
1314
  method: "get",
1201
1315
  path: "/versions/{id}",
1202
1316
  pathParams: ["id"],
1203
- queryParams: ["expectedSchemaHash"],
1317
+ queryParams: ["expectedSchemaHash", "contentFormat"],
1204
1318
  body: false,
1205
1319
  multipart: false,
1206
1320
  binary: false,
@@ -1274,6 +1388,10 @@ const retryDelay = (attempt, response) => {
1274
1388
  if (Number.isFinite(date)) return Math.max(0, date - Date.now());
1275
1389
  return Math.min(1e3 * 2 ** attempt, 3e4) + Math.random() * 250;
1276
1390
  };
1391
+ const isInProgressWrite = async (request, response) => {
1392
+ if (response.status !== 409 || !request.headers.has("Idempotency-Key")) return false;
1393
+ return (await response.clone().json().catch(() => null))?.code === "IDEMPOTENCY_KEY_IN_PROGRESS";
1394
+ };
1277
1395
  const fetchWithRetries = async (fetcher, request, retries) => {
1278
1396
  const signal = request.signal;
1279
1397
  for (let attempt = 0;; attempt += 1) {
@@ -1286,13 +1404,14 @@ const fetchWithRetries = async (fetcher, request, retries) => {
1286
1404
  await wait(retryDelay(attempt), signal);
1287
1405
  continue;
1288
1406
  }
1289
- if (attempt >= retries || ![
1407
+ const retryable = [
1290
1408
  429,
1291
1409
  500,
1292
1410
  502,
1293
1411
  503,
1294
1412
  504
1295
- ].includes(response.status)) return response;
1413
+ ].includes(response.status) || await isInProgressWrite(request, response);
1414
+ if (attempt >= retries || !retryable) return response;
1296
1415
  const delay = retryDelay(attempt, response);
1297
1416
  await response.body?.cancel();
1298
1417
  await wait(delay, signal);
@@ -1343,6 +1462,7 @@ const createClient = (config = {}) => {
1343
1462
  new Headers(options.headers).forEach((value, key) => headers.set(key, value));
1344
1463
  if (definition.streaming && !headers.has("Accept")) headers.set("Accept", "text/event-stream");
1345
1464
  if (credential && !definition.anonymous) headers.set("Authorization", `Bearer ${credential}`);
1465
+ if (definition.idempotent && !headers.has("Idempotency-Key")) headers.set("Idempotency-Key", crypto.randomUUID());
1346
1466
  const timer = timeout > 0 ? setTimeout(() => controller.abort(new DOMException("Request timed out", "TimeoutError")), timeout) : void 0;
1347
1467
  let streamOwnsRequest = false;
1348
1468
  try {
@@ -1361,7 +1481,7 @@ const createClient = (config = {}) => {
1361
1481
  else if (item !== void 0) form.append(key, typeof item === "string" ? item : JSON.stringify(item));
1362
1482
  return form;
1363
1483
  } : void 0,
1364
- fetch: (request) => fetchWithRetries(fetcher, request, definition.method === "get" ? retries : 0)
1484
+ fetch: (request) => fetchWithRetries(fetcher, request, definition.method === "get" || definition.idempotent ? retries : 0)
1365
1485
  });
1366
1486
  const response = result.response;
1367
1487
  if (response.status === 304) {
@@ -1421,4 +1541,4 @@ const createClient = (config = {}) => {
1421
1541
  //#endregion
1422
1542
  export { paginate as n, paginatePages as r, createClient as t };
1423
1543
 
1424
- //# sourceMappingURL=client-DioqVOxl.js.map
1544
+ //# sourceMappingURL=client-CyhWWH37.js.map