@andesine/sdk 0.6.0 → 0.7.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,88 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0
4
+
5
+ - `auth.getIdentity` returns `client` (ID and registered name) for OAuth access tokens.
6
+ - Webhook events have an `actor`: the member, API key, extension, or app (OAuth client ID and
7
+ registered name, with the member) that made the change, or `null` for changes by Andesine.
8
+ - OAuth for MCP clients: the authorization server supports the authorization code flow with
9
+ PKCE and dynamic client registration, with the scopes `andesine:read` and `andesine:write`.
10
+ `auth.getIdentity` returns `scopes` and `workspaceID` for such tokens.
11
+ - `auth.getIdentity` returns the `permissions` of an API key, and `instance.get` returns
12
+ `appURL`, the web app URL (entries open at `{appURL}/{workspaceID}/{entryID}`).
13
+ - `versions.create` takes an optional `document`: ProseMirror JSON checked against the editor
14
+ and the entry's collection schema. The version has the new reason `import`, and the
15
+ current content does not change.
16
+ - `entries.get` returns `hash`, the hash of the current content, comparable to version hashes.
17
+ - Add `entries.move` and `collections.move` (API key permissions `entries` and
18
+ `collections`). Without `order`, a moved entry keeps its position in the same collection
19
+ and goes last in a new one.
20
+ - `entries.update` replaces the content with `document` and `expectedHash`, the hash of the
21
+ content it replaces. Only the changed blocks change, so live edits elsewhere are kept. A
22
+ different current hash returns the new error `CONTENT_HASH_MISMATCH` (409) with the current
23
+ `hash`. Current content without a version is first saved as an `auto` version, and `version`
24
+ also saves the new content as a `manual` version. Use `name` or `document`, not both. The
25
+ result is `{ hash, version }`; before, it was empty.
26
+ - `entries.create` takes an optional `document` in place of `name`; its title sets the name.
27
+ - MDX content: `entries.get`, `versions.get`, and `content.get` take `contentFormat: "mdx"`
28
+ and then also return the content as MDX in `mdx`: the title as H1, properties as
29
+ `<Property name value />` elements in their position, fragments as `<Fragment name>`
30
+ elements, and images as `asset:<assetID>` references (delivery URLs in published content).
31
+ `entries.create`, `entries.update`, and `versions.create` take `mdx` in place of `document`;
32
+ frontmatter properties are also read. Fields match by name or key. Node IDs and attributes
33
+ that MDX doesn't hold are kept from the current content. Invalid MDX returns the new error
34
+ `CONTENT_FORMAT_INVALID` (400) with the line and column.
35
+ - `Fragment` and `Property` are reserved element names; content writes with them return
36
+ `CONTENT_SCHEMA_INVALID`.
37
+ - `entries.update` takes `operations`, applied in order with all or none applied:
38
+ `replaceText` (`find` must occur once in one block's MDX), `replaceBlock`, `insertBlocks`
39
+ (`before`, `after`, or `position` with an optional `fragment`), `deleteBlocks`, and `setField`
40
+ (a property value, or MDX for a fragment). Anchors come from `entries.get` with
41
+ `contentFormat: "blocks"`, which returns the blocks with IDs, fragment keys, and MDX.
42
+ `expectedHash` is optional with operations; `name` can be combined with them. A missing or
43
+ ambiguous anchor returns the new error `CONTENT_OPERATION_INVALID` (409) with the operation
44
+ `index` and `reason`.
45
+ - Internal links: link marks can point to entries with `entry:<entryID>`, and an optional
46
+ `#anchor`. Content writes also accept `entry:<path>` (a path or slug path) and store the entry
47
+ ID; a link to a missing entry returns the new error `CONTENT_LINK_INVALID` (409).
48
+ `entries.get` and `versions.get` return `references`: the path, slug path, title, and
49
+ collection of each linked entry, or `available: false` for deleted and unreadable entries.
50
+ MDX and blocks show links by slug path. Published content resolves links to the entry's slug
51
+ path in the snapshot, and removes links to entries that aren't published (the text stays).
52
+ - Images follow content: content writes attach the `asset:<assetID>` images they newly
53
+ reference (the caller must be able to read each image and update the entry), so
54
+ `assets.attach` is no longer part of the public API. `entries.update` and `versions.create`
55
+ also import image URLs in MDX and wait until the images are ready; an image that fails or is
56
+ still processing returns the new error `CONTENT_IMAGE_UNAVAILABLE` (409). `entries.create`
57
+ accepts only `asset:` images. To remove an image, remove it from the content.
58
+ - `entries.create` takes `path`: a full name path. Missing parent collections are created with
59
+ exact names; a name already used at the path returns `CONTENT_NAME_CONFLICT`.
60
+ - Target collections by path: `entries.create` and `entries.move` take `collectionPath` or
61
+ `collectionSlugPath` in place of `collectionID`, `collections.create` takes `parentPath` or
62
+ `parentSlugPath`, `collections.move` takes `newParentPath` or `newParentSlugPath` (`/` is the
63
+ top level), and `publishing.getChannelContent` takes `collectionPath` or `collectionSlugPath`.
64
+ These select existing collections only.
65
+ - Preconditions: `expectedName` on `entries.update` and `collections.update`,
66
+ `expectedCollectionID` on `entries.move`, and `expectedParentID` on `collections.move`. A
67
+ different current value returns the new error `CONTENT_PRECONDITION_FAILED` (409) with the
68
+ current value.
69
+ - `Idempotency-Key` header on `entries.create`, `entries.update`, and `versions.create`: a
70
+ repeated request returns the first result for 24 hours (with `Idempotent-Replayed: true`).
71
+ The same key with a different body returns `IDEMPOTENCY_KEY_REUSED` (422), and a repeat while
72
+ the first request runs returns `IDEMPOTENCY_KEY_IN_PROGRESS` (409). The SDK sends a generated
73
+ key for these operations unless `headers` has one, and `retries` now also applies to them, including a retry that
74
+ arrives while the first attempt still runs (`IDEMPOTENCY_KEY_IN_PROGRESS`, with
75
+ `Retry-After: 1`).
76
+ - Add `entries.bulkUpdate`: the items of `entries.update` for many entries in one call (a name
77
+ without content is written as the title). All items are checked before any content changes,
78
+ and all or none apply; errors name the failing item. Supports `Idempotency-Key`.
79
+ - Add `entries.bulkMove`: the items of `entries.move` in one call, all or none. Moves that need a
80
+ schema migration aren't supported; use `entries.move` for them.
81
+ - Add `publishing.updateChannel`: renames a channel; its code doesn't change.
82
+ - `entries.create` takes `fields`: property values and fragment MDX by field name or key.
83
+ - `entries.get` returns the content of a document that is open in the editor, including edits
84
+ that aren't saved yet, so its `hash` matches what content writes compare against.
85
+
3
86
  ## 0.6.0
4
87
 
5
88
  - 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,
@@ -395,6 +419,16 @@ const createResources = (request) => ({
395
419
  binary: false,
396
420
  anonymous: false
397
421
  }),
422
+ move: operation(request, {
423
+ method: "post",
424
+ path: "/collections/{id}/move",
425
+ pathParams: ["id"],
426
+ queryParams: [],
427
+ body: true,
428
+ multipart: false,
429
+ binary: false,
430
+ anonymous: false
431
+ }),
398
432
  list: operation(request, {
399
433
  method: "get",
400
434
  path: "/collections/list",
@@ -494,7 +528,8 @@ const createResources = (request) => ({
494
528
  "slugPath",
495
529
  "expectedSchemaHash",
496
530
  "channel",
497
- "snapshotID"
531
+ "snapshotID",
532
+ "contentFormat"
498
533
  ],
499
534
  body: false,
500
535
  multipart: false,
@@ -1148,12 +1183,26 @@ const createResources = (request) => ({
1148
1183
  method: "get",
1149
1184
  path: "/publishing/channels/{channel}/content",
1150
1185
  pathParams: ["channel"],
1151
- queryParams: ["collectionID"],
1186
+ queryParams: [
1187
+ "collectionID",
1188
+ "collectionPath",
1189
+ "collectionSlugPath"
1190
+ ],
1152
1191
  body: false,
1153
1192
  multipart: false,
1154
1193
  binary: false,
1155
1194
  anonymous: false
1156
1195
  }),
1196
+ updateChannel: operation(request, {
1197
+ method: "put",
1198
+ path: "/publishing/channels/{code}",
1199
+ pathParams: ["code"],
1200
+ queryParams: [],
1201
+ body: true,
1202
+ multipart: false,
1203
+ binary: false,
1204
+ anonymous: false
1205
+ }),
1157
1206
  deleteChannel: operation(request, {
1158
1207
  method: "delete",
1159
1208
  path: "/publishing/channels/{code}",
@@ -1177,6 +1226,7 @@ const createResources = (request) => ({
1177
1226
  }) },
1178
1227
  versions: {
1179
1228
  create: operation(request, {
1229
+ idempotent: true,
1180
1230
  method: "post",
1181
1231
  path: "/entries/{entryID}/versions",
1182
1232
  pathParams: ["entryID"],
@@ -1200,7 +1250,7 @@ const createResources = (request) => ({
1200
1250
  method: "get",
1201
1251
  path: "/versions/{id}",
1202
1252
  pathParams: ["id"],
1203
- queryParams: ["expectedSchemaHash"],
1253
+ queryParams: ["expectedSchemaHash", "contentFormat"],
1204
1254
  body: false,
1205
1255
  multipart: false,
1206
1256
  binary: false,
@@ -1274,6 +1324,10 @@ const retryDelay = (attempt, response) => {
1274
1324
  if (Number.isFinite(date)) return Math.max(0, date - Date.now());
1275
1325
  return Math.min(1e3 * 2 ** attempt, 3e4) + Math.random() * 250;
1276
1326
  };
1327
+ const isInProgressWrite = async (request, response) => {
1328
+ if (response.status !== 409 || !request.headers.has("Idempotency-Key")) return false;
1329
+ return (await response.clone().json().catch(() => null))?.code === "IDEMPOTENCY_KEY_IN_PROGRESS";
1330
+ };
1277
1331
  const fetchWithRetries = async (fetcher, request, retries) => {
1278
1332
  const signal = request.signal;
1279
1333
  for (let attempt = 0;; attempt += 1) {
@@ -1286,13 +1340,14 @@ const fetchWithRetries = async (fetcher, request, retries) => {
1286
1340
  await wait(retryDelay(attempt), signal);
1287
1341
  continue;
1288
1342
  }
1289
- if (attempt >= retries || ![
1343
+ const retryable = [
1290
1344
  429,
1291
1345
  500,
1292
1346
  502,
1293
1347
  503,
1294
1348
  504
1295
- ].includes(response.status)) return response;
1349
+ ].includes(response.status) || await isInProgressWrite(request, response);
1350
+ if (attempt >= retries || !retryable) return response;
1296
1351
  const delay = retryDelay(attempt, response);
1297
1352
  await response.body?.cancel();
1298
1353
  await wait(delay, signal);
@@ -1343,6 +1398,7 @@ const createClient = (config = {}) => {
1343
1398
  new Headers(options.headers).forEach((value, key) => headers.set(key, value));
1344
1399
  if (definition.streaming && !headers.has("Accept")) headers.set("Accept", "text/event-stream");
1345
1400
  if (credential && !definition.anonymous) headers.set("Authorization", `Bearer ${credential}`);
1401
+ if (definition.idempotent && !headers.has("Idempotency-Key")) headers.set("Idempotency-Key", crypto.randomUUID());
1346
1402
  const timer = timeout > 0 ? setTimeout(() => controller.abort(new DOMException("Request timed out", "TimeoutError")), timeout) : void 0;
1347
1403
  let streamOwnsRequest = false;
1348
1404
  try {
@@ -1361,7 +1417,7 @@ const createClient = (config = {}) => {
1361
1417
  else if (item !== void 0) form.append(key, typeof item === "string" ? item : JSON.stringify(item));
1362
1418
  return form;
1363
1419
  } : void 0,
1364
- fetch: (request) => fetchWithRetries(fetcher, request, definition.method === "get" ? retries : 0)
1420
+ fetch: (request) => fetchWithRetries(fetcher, request, definition.method === "get" || definition.idempotent ? retries : 0)
1365
1421
  });
1366
1422
  const response = result.response;
1367
1423
  if (response.status === 304) {
@@ -1421,4 +1477,4 @@ const createClient = (config = {}) => {
1421
1477
  //#endregion
1422
1478
  export { paginate as n, paginatePages as r, createClient as t };
1423
1479
 
1424
- //# sourceMappingURL=client-DioqVOxl.js.map
1480
+ //# sourceMappingURL=client-D7hqNIEY.js.map