@onlyworlds/sdk 4.27.0 → 4.27.2

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
@@ -5,6 +5,27 @@ earlier history lives in git log only.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [4.27.2] — 2026-10-16
9
+
10
+ ### Fixed
11
+ - **`bulk` mints an id for each element that has none, as `create` does.** It sent id-less elements
12
+ bare and keel minted them. With an `idempotencyKey`, each id is now derived from the key and the
13
+ element's position in `items` (the ids the Python package mints), so the same key and items send the
14
+ same body from either client, and a resend after keel's 24-hour replay window rewrites the same
15
+ elements instead of creating new ones. Without a key, a fresh UUIDv7 each. Ids you pass are kept, and
16
+ your items are not changed. Found by Quillon's parity audit of the Unity SDK (h37 #5794).
17
+
18
+ ## [4.27.1] — 2026-10-16
19
+
20
+ ### Fixed
21
+ - **A retried `create` with the same `idempotencyKey` now replays instead of failing.** `create` minted a
22
+ fresh id on every call, so a retry of an id-less element sent a different body and keel answered 409
23
+ `idempotency_error`. With a key and no id, the id is now derived from the key (a UUIDv5, the same id the
24
+ Python package mints), so the retry sends the same body and gets the first answer back. Without a key,
25
+ a fresh UUIDv7 as before; an id you pass is kept.
26
+ - `create`'s doc said a keyed retry "rewrites the same element"; keel's replay writes nothing. Found by
27
+ Quillon's parity audit of the Unity SDK (h37 #5764).
28
+
8
29
  ## [4.27.0] — 2026-10-16
9
30
 
10
31
  ### Added
package/dist/index.d.ts CHANGED
@@ -2556,9 +2556,12 @@ declare class OwV2Client {
2556
2556
  /** GET /{type}/{id}/ -- optional one-level stub expansion / sparse fields. */
2557
2557
  get(type: ElementType | string, id: string, opts?: Pick<ListParams, 'expand' | 'fields'>): Promise<OwElement>;
2558
2558
  /**
2559
- * POST /{type}/ -- create one element, minting a UUIDv7 id on the client when you leave id out.
2560
- * The client-side id makes a retry carrying the same Idempotency-Key safe: it rewrites
2561
- * the same element instead of creating a second one. You may still supply your own id.
2559
+ * POST /{type}/ -- create one element, minting an id on the client when you leave id out.
2560
+ * With an `idempotencyKey`, the minted id is derived from the key (a UUIDv5, the same one the
2561
+ * Python package mints), so a retry with the same key and element sends the same body and keel
2562
+ * replays its first answer: nothing is written twice, and the stored 201 comes back. Without a
2563
+ * key, a fresh UUIDv7. You may still supply your own id. A new create meant to write again
2564
+ * (an undo that re-creates) needs a new key, or none: within 24 h a reused key only replays.
2562
2565
  * `name` is the one required field, and required means the key is present: `''` and
2563
2566
  * `null` are accepted and stored as `''` (nameless Markers exist).
2564
2567
  */
@@ -2607,6 +2610,10 @@ declare class OwV2Client {
2607
2610
  * After an atomic:true request with errors:true NOTHING was written, yet the slots of
2608
2611
  * the items that would have succeeded still say 201 with the ids and timestamps they
2609
2612
  * would have had (keel's "counterfactual 201s"): do not record those ids as created.
2613
+ * An element without an id gets one, as `create` mints it, by its position in `items`: with an
2614
+ * `idempotencyKey`, derived from the key and the position (the ids the Python package mints),
2615
+ * so a retry with the same key and items sends the same body; without one, a
2616
+ * fresh UUIDv7. Ids you supply are kept.
2610
2617
  */
2611
2618
  bulk(items: OwBulkItem[], opts?: {
2612
2619
  atomic?: boolean;
package/dist/index.js CHANGED
@@ -1,3 +1,61 @@
1
+ // src/v2/keyed-id.ts
2
+ var KEYED_IDS = "0199b6a0-6f0e-7c2a-9d1e-3b5c7a8e4f21";
3
+ function sha1(msg) {
4
+ const ml = msg.length;
5
+ const total = (ml + 8 >> 6) + 1 << 6;
6
+ const buf = new Uint8Array(total);
7
+ buf.set(msg);
8
+ buf[ml] = 128;
9
+ const bits = ml * 8;
10
+ const view = new DataView(buf.buffer);
11
+ view.setUint32(total - 8, Math.floor(bits / 2 ** 32));
12
+ view.setUint32(total - 4, bits >>> 0);
13
+ let h0 = 1732584193, h1 = 4023233417, h2 = 2562383102, h3 = 271733878, h4 = 3285377520;
14
+ const w = new Uint32Array(80);
15
+ for (let off = 0; off < total; off += 64) {
16
+ for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4);
17
+ for (let i = 16; i < 80; i++) {
18
+ const x = w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16];
19
+ w[i] = x << 1 | x >>> 31;
20
+ }
21
+ let a = h0, b = h1, c = h2, d = h3, e = h4;
22
+ for (let i = 0; i < 80; i++) {
23
+ const [f, k] = i < 20 ? [b & c | ~b & d, 1518500249] : i < 40 ? [b ^ c ^ d, 1859775393] : i < 60 ? [b & c | b & d | c & d, 2400959708] : [b ^ c ^ d, 3395469782];
24
+ const t = (a << 5 | a >>> 27) + f + e + k + w[i] >>> 0;
25
+ e = d;
26
+ d = c;
27
+ c = b << 30 | b >>> 2;
28
+ b = a;
29
+ a = t;
30
+ }
31
+ h0 = h0 + a >>> 0;
32
+ h1 = h1 + b >>> 0;
33
+ h2 = h2 + c >>> 0;
34
+ h3 = h3 + d >>> 0;
35
+ h4 = h4 + e >>> 0;
36
+ }
37
+ const out = new Uint8Array(20);
38
+ const ov = new DataView(out.buffer);
39
+ [h0, h1, h2, h3, h4].forEach((h, i) => ov.setUint32(i * 4, h));
40
+ return out;
41
+ }
42
+ var hexBytes = (hex) => Uint8Array.from(hex.replace(/-/g, "").match(/../g).map((b) => parseInt(b, 16)));
43
+ function uuid5(ns, name) {
44
+ const nameBytes = new TextEncoder().encode(name);
45
+ const input = new Uint8Array(16 + nameBytes.length);
46
+ input.set(hexBytes(ns));
47
+ input.set(nameBytes, 16);
48
+ const b = sha1(input).slice(0, 16);
49
+ b[6] = b[6] & 15 | 80;
50
+ b[8] = b[8] & 63 | 128;
51
+ const h = [...b].map((x) => x.toString(16).padStart(2, "0")).join("");
52
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
53
+ }
54
+ function keyedId(idempotencyKey, index) {
55
+ return uuid5(KEYED_IDS, `${idempotencyKey}
56
+ ${index}`);
57
+ }
58
+
1
59
  // src/v2/errors.ts
2
60
  var OwApiError = class extends Error {
3
61
  constructor(status, code, message, docUrl, detail, type = null, param = null, retryAfter = null) {
@@ -274,16 +332,19 @@ var OwV2Client = class {
274
332
  return this.request("GET", `/${type}/${id}/`, { query });
275
333
  }
276
334
  /**
277
- * POST /{type}/ -- create one element, minting a UUIDv7 id on the client when you leave id out.
278
- * The client-side id makes a retry carrying the same Idempotency-Key safe: it rewrites
279
- * the same element instead of creating a second one. You may still supply your own id.
335
+ * POST /{type}/ -- create one element, minting an id on the client when you leave id out.
336
+ * With an `idempotencyKey`, the minted id is derived from the key (a UUIDv5, the same one the
337
+ * Python package mints), so a retry with the same key and element sends the same body and keel
338
+ * replays its first answer: nothing is written twice, and the stored 201 comes back. Without a
339
+ * key, a fresh UUIDv7. You may still supply your own id. A new create meant to write again
340
+ * (an undo that re-creates) needs a new key, or none: within 24 h a reused key only replays.
280
341
  * `name` is the one required field, and required means the key is present: `''` and
281
342
  * `null` are accepted and stored as `''` (nameless Markers exist).
282
343
  */
283
344
  async create(type, element, opts = {}) {
284
345
  const body = sanitizePayload(element);
285
346
  if (body.id === void 0 || body.id === null || body.id === "") {
286
- body.id = mintUuid();
347
+ body.id = opts.idempotencyKey ? keyedId(opts.idempotencyKey, 0) : mintUuid();
287
348
  }
288
349
  return this.request("POST", `/${type}/`, {
289
350
  write: true,
@@ -338,10 +399,20 @@ var OwV2Client = class {
338
399
  * After an atomic:true request with errors:true NOTHING was written, yet the slots of
339
400
  * the items that would have succeeded still say 201 with the ids and timestamps they
340
401
  * would have had (keel's "counterfactual 201s"): do not record those ids as created.
402
+ * An element without an id gets one, as `create` mints it, by its position in `items`: with an
403
+ * `idempotencyKey`, derived from the key and the position (the ids the Python package mints),
404
+ * so a retry with the same key and items sends the same body; without one, a
405
+ * fresh UUIDv7. Ids you supply are kept.
341
406
  */
342
407
  async bulk(items, opts = {}) {
343
408
  const body = {
344
- items: items.map((it) => ({ type: it.type, element: sanitizePayload(it.element) })),
409
+ items: items.map((it, index) => {
410
+ const element = sanitizePayload(it.element);
411
+ if (element.id === void 0 || element.id === null || element.id === "") {
412
+ element.id = opts.idempotencyKey ? keyedId(opts.idempotencyKey, index) : mintUuid();
413
+ }
414
+ return { type: it.type, element };
415
+ }),
345
416
  atomic: opts.atomic ?? false
346
417
  };
347
418
  return this.request("POST", "/bulk/", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlyworlds/sdk",
3
- "version": "4.27.0",
3
+ "version": "4.27.2",
4
4
  "description": "TypeScript SDK for the OnlyWorlds API - build world-building applications with type safety",
5
5
  "type": "module",
6
6
  "exports": {