@interop/was-client 0.60.0 → 0.62.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.
Files changed (81) hide show
  1. package/README.md +124 -48
  2. package/dist/Collection.d.ts +135 -94
  3. package/dist/Collection.d.ts.map +1 -1
  4. package/dist/Collection.js +612 -194
  5. package/dist/Collection.js.map +1 -1
  6. package/dist/Resource.d.ts +1 -1
  7. package/dist/Resource.d.ts.map +1 -1
  8. package/dist/Resource.js +0 -3
  9. package/dist/Resource.js.map +1 -1
  10. package/dist/Space.d.ts +17 -16
  11. package/dist/Space.d.ts.map +1 -1
  12. package/dist/Space.js +41 -24
  13. package/dist/Space.js.map +1 -1
  14. package/dist/WasClient.d.ts +51 -6
  15. package/dist/WasClient.d.ts.map +1 -1
  16. package/dist/WasClient.js +84 -14
  17. package/dist/WasClient.js.map +1 -1
  18. package/dist/codec.d.ts +3 -3
  19. package/dist/edv/EdvCodec.js +1 -1
  20. package/dist/edv/descriptorStore.d.ts +16 -14
  21. package/dist/edv/descriptorStore.d.ts.map +1 -1
  22. package/dist/edv/descriptorStore.js +17 -14
  23. package/dist/edv/descriptorStore.js.map +1 -1
  24. package/dist/edv/index.d.ts +1 -1
  25. package/dist/edv/index.js +1 -1
  26. package/dist/edv/logGovernedDescriptorStore.d.ts +2 -2
  27. package/dist/edv/logGovernedDescriptorStore.d.ts.map +1 -1
  28. package/dist/edv/logGovernedDescriptorStore.js +10 -9
  29. package/dist/edv/logGovernedDescriptorStore.js.map +1 -1
  30. package/dist/edv/recipients.d.ts +1 -1
  31. package/dist/edv/recipients.js +2 -2
  32. package/dist/edv/recipients.js.map +1 -1
  33. package/dist/edv/refresh.d.ts +1 -1
  34. package/dist/edv/refresh.js +1 -1
  35. package/dist/errors.d.ts +14 -1
  36. package/dist/errors.d.ts.map +1 -1
  37. package/dist/errors.js +16 -3
  38. package/dist/errors.js.map +1 -1
  39. package/dist/index.d.ts +2 -2
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +1 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/internal/codec.d.ts +22 -1
  44. package/dist/internal/codec.d.ts.map +1 -1
  45. package/dist/internal/codec.js +117 -46
  46. package/dist/internal/codec.js.map +1 -1
  47. package/dist/internal/describe.d.ts +98 -24
  48. package/dist/internal/describe.d.ts.map +1 -1
  49. package/dist/internal/describe.js +125 -18
  50. package/dist/internal/describe.js.map +1 -1
  51. package/dist/internal/meta.d.ts +32 -17
  52. package/dist/internal/meta.d.ts.map +1 -1
  53. package/dist/internal/meta.js +64 -35
  54. package/dist/internal/meta.js.map +1 -1
  55. package/dist/internal/paths.d.ts +33 -21
  56. package/dist/internal/paths.d.ts.map +1 -1
  57. package/dist/internal/paths.js +93 -46
  58. package/dist/internal/paths.js.map +1 -1
  59. package/dist/internal/request.d.ts +22 -6
  60. package/dist/internal/request.d.ts.map +1 -1
  61. package/dist/internal/request.js +31 -13
  62. package/dist/internal/request.js.map +1 -1
  63. package/dist/internal/reserved.d.ts +14 -0
  64. package/dist/internal/reserved.d.ts.map +1 -1
  65. package/dist/internal/reserved.js +23 -4
  66. package/dist/internal/reserved.js.map +1 -1
  67. package/dist/internal/service.d.ts +61 -0
  68. package/dist/internal/service.d.ts.map +1 -0
  69. package/dist/internal/service.js +259 -0
  70. package/dist/internal/service.js.map +1 -0
  71. package/dist/paths.d.ts +8 -5
  72. package/dist/paths.d.ts.map +1 -1
  73. package/dist/paths.js +8 -5
  74. package/dist/paths.js.map +1 -1
  75. package/dist/sync/provisioning.d.ts +5 -5
  76. package/dist/sync/provisioning.d.ts.map +1 -1
  77. package/dist/sync/provisioning.js +2 -2
  78. package/dist/sync/provisioning.js.map +1 -1
  79. package/dist/types.d.ts +43 -10
  80. package/dist/types.d.ts.map +1 -1
  81. package/package.json +2 -2
package/README.md CHANGED
@@ -13,6 +13,7 @@
13
13
  - [Usage](#usage)
14
14
  - [Creating a client (signer + zcapClient)](#creating-a-client-signer--zcapclient)
15
15
  - [Deriving a client from a secret or seed (`./identity`)](#deriving-a-client-from-a-secret-or-seed-identity)
16
+ - [Service discovery](#service-discovery)
16
17
  - [The handle model](#the-handle-model)
17
18
  - [Spaces](#spaces)
18
19
  - [Collections](#collections)
@@ -187,6 +188,50 @@ accepted. A server that verifies both it and the older `Ed25519Signature2020`
187
188
  accepts chains whose links mix the two, which is what a fleet upgrading at its
188
189
  own pace produces.
189
190
 
191
+ ### Service discovery
192
+
193
+ Before its first signed request, the client reads the server's service
194
+ description. It sends an unsigned `HEAD` to `serverUrl` and follows the
195
+ `Link: <...>; rel="service"` header on the response, whatever its status. Then
196
+ it reads the linked document with an unsigned `GET`. The spec fixes no path for
197
+ the document, so the client assumes none.
198
+
199
+ From the document the client selects the WAS specification version it speaks.
200
+ This client speaks only v0.5. It ignores `specs` keys other than
201
+ `https://w3id.org/pws` and entries without a `version`. If no entry names v0.5,
202
+ or the document is malformed, every signed request rejects with
203
+ `IncompatibleServerError` before anything is signed. A server whose responses
204
+ carry no `service` link predates v0.5 and is refused the same way. The client
205
+ does not fall back to the v0.4 layout.
206
+
207
+ Discovery runs once per client and is shared by every handle. A failure is not
208
+ memoized, so the next request retries. Unsigned public reads (`publicRead`,
209
+ `publicListCollection`, ...) take absolute URLs that may point at another
210
+ server, and do not wait on discovery.
211
+
212
+ `was.service()` returns what discovery found:
213
+
214
+ ```ts
215
+ const info = await was.service()
216
+ info.version // '0.5'
217
+ info.entry // the chosen `PwsVersionEntry`
218
+ info.description // the whole `ServiceDescription`
219
+ info.spacesUrl // the Spaces Repository URL, or undefined
220
+ info.features // e.g. ['listing', 'collection-management', ...]
221
+ info.hasFeature('quotas') // false when the token is absent
222
+
223
+ // Discover again, e.g. before relying on an affordance a host may have dropped.
224
+ await was.service({ refresh: true })
225
+ ```
226
+
227
+ `features` is an open token list. A token this client does not know is carried
228
+ along and otherwise ignored, and an absent token means unsupported. The client
229
+ gates no behavior on the document's `instance` member.
230
+
231
+ A caller that already holds the document (from a cache, say) can pass it as the
232
+ `serviceDescription` constructor option. The client then selects a version from
233
+ that copy without fetching, and `service({ refresh: true })` discovers afresh.
234
+
190
235
  ### The handle model
191
236
 
192
237
  The client exposes the WAS containment model
@@ -220,22 +265,25 @@ one item" footgun. The next sections cover each level in turn.
220
265
 
221
266
  A Space is the top-level container, created from the spaces repository. The
222
267
  server requires a `name`; `controller` defaults to the client's own DID, and the
223
- server generates the id unless you pass one.
268
+ server generates the id unless you pass one. `createSpace()` and `listSpaces()`
269
+ address the Spaces Repository URL the service description names (see
270
+ [Service discovery](#service-discovery)). Against a server whose description has
271
+ no `spaces` URL, they throw `NotSupportedError` without sending anything.
224
272
 
225
273
  ```ts
226
- const space = await was.createSpace({ name: 'Home' }) // POST /spaces/
274
+ const space = await was.createSpace({ name: 'Home' }) // POST {spacesUrl}
227
275
 
228
276
  // Lazy handle to an existing space by id -- no I/O until a verb runs.
229
277
  const same = was.space(space.id)
230
278
 
231
- // Read the Space Description (null if missing or not visible to you).
279
+ // Read the Space Metadata object (null if missing or not visible to you).
232
280
  const desc = await space.describe() // { id, type: ['Space'], name, controller } | null
233
281
 
234
- // Upsert: merges the given fields over the current description.
282
+ // Upsert: merges the given fields over the current Metadata object.
235
283
  await space.configure({ name: 'Home (renamed)' })
236
284
 
237
- // Lost-update-safe writes: read the description with its ETag, then write
238
- // it under `ifMatch` (412 `PreconditionFailedError` if it changed), or
285
+ // Lost-update-safe writes: read the Metadata object with its ETag, then
286
+ // write it under `ifMatch` (412 `PreconditionFailedError` if it changed), or
239
287
  // create-if-absent under `ifNoneMatch` (412 if the Space already exists).
240
288
  // Nothing is read or merged on the client, so `controller` is required.
241
289
  const read = await space.describeWithEtag() // { description, etag? } | null
@@ -248,7 +296,7 @@ await was
248
296
  .replaceDescription(
249
297
  { name: 'Fresh', controller: 'did:key:z6Mk...' },
250
298
  { ifNoneMatch: true }
251
- ) // { description, etag? }: a create echoes the description
299
+ ) // { description, etag? }: a create echoes the Metadata object
252
300
 
253
301
  await space.delete() // idempotent
254
302
 
@@ -286,10 +334,10 @@ const collection = await space.createCollection({
286
334
  // Lazy handle to an existing collection by id.
287
335
  const same = space.collection(collection.id)
288
336
 
289
- // Read the Collection Description (null if missing or not visible).
337
+ // Read the Collection Metadata object (null if missing or not visible).
290
338
  const desc = await collection.describe() // { id, type: ['Collection'], name } | null
291
339
 
292
- // Update (upsert; merges over the current description).
340
+ // Update (upsert; merges over the current Metadata object).
293
341
  await collection.configure({ name: 'Credentials' })
294
342
 
295
343
  // List the collections in a space.
@@ -314,17 +362,16 @@ await collection.delete() // deletes the whole collection; idempotent
314
362
  To delete a single resource instead of the whole collection, use
315
363
  `collection.resource(id).delete()`.
316
364
 
317
- The Description's `name` is server-visible plaintext. An encrypted collection
318
- leaves it unpopulated by convention and carries its name and tags on the
319
- [Collection metadata](#collection-metadata) surface instead, where they are
320
- encrypted.
365
+ The Collection Metadata object's top-level `name` is server-visible plaintext.
366
+ An encrypted collection leaves it unpopulated by convention and carries its name
367
+ and tags on the same object's [`custom`](#collection-metadata) member instead,
368
+ where they are encrypted.
321
369
 
322
370
  An application that provisions a collection can record who it was provisioned
323
371
  for, with the optional `generator` (the application's DID) and `generatorOrigin`
324
- (the Web origin that DID was bound to) description fields. Both are accepted at
325
- create time and stay writable afterwards, so an existing collection can be
326
- backfilled; they are controller assertions the server persists but does not
327
- verify.
372
+ (the Web origin that DID was bound to) fields. Both are accepted at create time
373
+ and stay writable afterwards, so an existing collection can be backfilled; they
374
+ are controller assertions the server persists but does not verify.
328
375
 
329
376
  ```ts
330
377
  await space.createCollection({
@@ -544,10 +591,14 @@ collection listings; updating one updates the other.
544
591
 
545
592
  ### Collection metadata
546
593
 
547
- A Collection has the same metadata surface at its own reserved `/meta` path:
548
- server-managed properties (`createdAt` / `updatedAt` / `createdBy`) plus a
549
- user-writable `custom` object (`name` and `tags`). A server without the endpoint
550
- surfaces its 501 as `NotImplementedError`.
594
+ A Collection's configuration (`name`, `backend`, `encryption`, `generator`, read
595
+ by [`describe()`](#collections)) and its annotations live in one object at the
596
+ Collection's reserved `/meta` path: server-managed properties (`createdAt` /
597
+ `updatedAt` / `createdBy`) plus a user-writable `custom` object (`name` and
598
+ `tags`), under one `metaVersion` validator. `meta()` is that same object with
599
+ `custom` decoded through the codec; `describe()` does not resolve the codec at
600
+ all, so on an encrypted collection it reports `custom` as the opaque envelope. A
601
+ server without the endpoint surfaces its 501 as `NotImplementedError`.
551
602
 
552
603
  ```ts
553
604
  const meta = await collection.meta() // CollectionMetadata | null (null on a miss)
@@ -561,19 +612,31 @@ await collection.setTags({ app: 'wallet' }) // keeps existing name
561
612
 
562
613
  // Conditional metadata write, against a `conditional-writes` backend.
563
614
  await collection.setMeta({ custom: { name: 'Vault' } }, { ifMatch: meta?.etag })
564
- // ...or write only if no metadata is set yet:
615
+ // ...or create the Collection only if it does not exist yet:
565
616
  await collection.setMeta({ custom: { name: 'Vault' } }, { ifNoneMatch: true })
566
617
  ```
567
618
 
568
619
  A failed precondition throws `PreconditionFailedError` (412). This `/meta` ETag
569
- (`metaVersion`) is versioned independently of the Collection Description's ETag
570
- and of every Resource's versions: writing one never bumps the other.
620
+ (`metaVersion`) is independent of every Resource's versions, but not of the
621
+ configuration members: one validator covers the whole Collection Metadata
622
+ object, so `configure()` and `setMeta()` advance the same counter.
623
+
624
+ The write is a full replacement of that one object, so an annotation write
625
+ re-sends the configuration members (and any member this client does not model)
626
+ as read, and `configure()` merges over the version it pins to. Two consequences
627
+ worth knowing. `ifNoneMatch: true` is the only annotation write that may create:
628
+ it means "create the Collection only if it does not exist", creates it with no
629
+ configuration, and encodes `custom` with the plaintext codec, since the body it
630
+ sends declares no encryption descriptor (pass a per-handle `encryption` override
631
+ to state one). Every other annotation write requires a readable current object
632
+ and throws `NotFoundError` when there is none -- a masked 404 cannot turn a
633
+ rename into a create.
571
634
 
572
635
  On an encrypted collection `custom` is encrypted into an envelope before it is
573
636
  sent, so `name` / `tags` are never stored as server-visible plaintext, and
574
637
  `meta()` decrypts them back for a keyed reader. This is the encrypted
575
- collection's name/tags surface: by convention the plaintext Description `name`
576
- is left unpopulated there.
638
+ collection's name/tags surface: by convention the plaintext top-level `name` is
639
+ left unpopulated there.
577
640
 
578
641
  #### The governing history log
579
642
 
@@ -594,11 +657,12 @@ await collection.putHistoryLog(log.body + nextLine, { ifMatch: log.etag })
594
657
 
595
658
  From the create on, `describe()` serves `encryption` as the log head's `state`
596
659
  with `history: { method, resource }` stamped on, and a direct `encryption` write
597
- on the Description throws `ConflictError`. The create is refused the same way on
598
- a Collection whose Description already carries a client-written descriptor. A
599
- lost race throws `PreconditionFailedError` (412). These are the raw transport
600
- methods; the `/log` subpath's `resourceLogStore({ collection })` (below) drives
601
- them as the resource-log store port.
660
+ on the Collection Metadata object throws `ConflictError`. The create is refused
661
+ the same way on a Collection whose Metadata object already carries a
662
+ client-written descriptor. A lost race throws `PreconditionFailedError` (412).
663
+ These are the raw transport methods; the `/log` subpath's
664
+ `resourceLogStore({ collection })` (below) drives them as the resource-log store
665
+ port.
602
666
 
603
667
  A reader that holds the descriptor but not the convention follows its `history`
604
668
  pointer through the `/edv` subpath's `logGovernedCollectionDescriptorStore`, the
@@ -759,10 +823,9 @@ stores faithfully). Two things drive it, kept separate:
759
823
 
760
824
  - **Policy** (is this collection encrypted?) is declared on the collection
761
825
  itself: `createCollection({ encryption: { scheme: 'edv' } })` writes a
762
- non-secret `encryption` descriptor to the Collection Description. Any
826
+ non-secret `encryption` descriptor to the Collection Metadata object. Any
763
827
  authorized reader -- including a delegated consumer that did **not** create
764
- the collection -- discovers it by reading the Description, so it knows to
765
- decrypt.
828
+ the collection -- discovers it by reading that object, so it knows to decrypt.
766
829
  - **Keys** come from an `encryption` provider you pass to `WasClient` (built
767
830
  from the opt-in `@interop/was-client/edv` subpath, so plaintext consumers
768
831
  never pull the crypto graph). It is a pure **keystore**: `resolveKeys` returns
@@ -794,21 +857,23 @@ const { id } = await vault.add({ secret: 'hello' }) // encrypted; id is an EDV i
794
857
  const back = await vault.get(id) // { secret: 'hello' } -- decrypted
795
858
 
796
859
  // A consumer that did not create it discovers the descriptor and decrypts with its
797
- // own keys -- no override needed; one cached read of the Description:
860
+ // own keys -- no override needed; one cached read of the Collection Metadata
861
+ // object:
798
862
  const same = was.space(spaceId).collection('vault')
799
863
  await same.get(id) // reads the descriptor, then decrypts
800
864
  ```
801
865
 
802
866
  The switch is the **descriptor**: a handle encrypts a collection when its
803
- Description declares `encryption` (resolution reads the Description once, then
804
- caches -- no round-trip for plaintext-only clients or when an override is set).
805
- Keys are then **required**: if the collection is declared encrypted but your
806
- keystore returns no keys, reads/writes throw `EncryptionError` (fail-closed) --
807
- they never silently fall back to plaintext.
867
+ Collection Metadata object declares `encryption` (resolution reads that object
868
+ once, then caches -- no round-trip for plaintext-only clients or when an
869
+ override is set). Keys are then **required**: if the collection is declared
870
+ encrypted but your keystore returns no keys, reads/writes throw
871
+ `EncryptionError` (fail-closed) instead of silently falling back to plaintext.
808
872
 
809
873
  **Per-handle override (escape hatch).** Pass `encryption` in the handle options
810
- to force the decision and skip the Description read -- `{ scheme: 'edv' }` (keys
811
- from the keystore), `{ scheme: 'edv', keys }` (keys inline), or `'plaintext'`:
874
+ to force the decision and skip the Collection Metadata read --
875
+ `{ scheme: 'edv' }` (keys from the keystore), `{ scheme: 'edv', keys }` (keys
876
+ inline), or `'plaintext'`:
812
877
 
813
878
  ```ts
814
879
  const vault = was.space(spaceId).collection('vault', {
@@ -820,8 +885,12 @@ const vault = was.space(spaceId).collection('vault', {
820
885
  keys-only): re-declare it once with
821
886
  `collection.configure({ encryption: { scheme: 'edv' } })` (the descriptor is
822
887
  set-once: declaring it on a collection that lacks one is allowed, changing or
823
- clearing an existing one is rejected). Until then, a per-handle override reads
824
- it correctly.
888
+ clearing an existing one is rejected). The same write re-seals the stored
889
+ `custom` under the newly declared descriptor, since the server validates the
890
+ envelope against the incoming one; a client that cannot build that codec (no
891
+ keystore, or no keys for the descriptor) sends no `custom` at all rather than
892
+ plaintext beside the descriptor, which loses the stored name and tags. Until the
893
+ collection is re-declared, a per-handle override reads it correctly.
825
894
 
826
895
  Encrypted collections are a **stricter contract**, not a drop-in (documents-only
827
896
  scope for now):
@@ -842,9 +911,9 @@ scope for now):
842
911
  endpoint has its own ETag (`metaVersion`), independent of the content ETag.
843
912
  The same pair at Collection level (`collection.setName()` / `setTags()` /
844
913
  `setMeta()` / `meta()`) is where an encrypted collection carries its own name
845
- and tags, since the Description's plaintext `name` is left unpopulated; the
846
- collection-level envelope binds no resource id, and a resource-bound envelope
847
- served into that slot is refused.
914
+ and tags, since the Collection Metadata object's plaintext `name` is left
915
+ unpopulated; the collection-level envelope binds no resource id, and a
916
+ resource-bound envelope served into that slot is refused.
848
917
  - **Binary.** A `Blob`/`Uint8Array` up to `maxBlobBytes` (512 KiB by default) is
849
918
  encrypted as a single document. A larger one is routed automatically by
850
919
  `add()` to the chunked-stream path: one document plus its chunk resources,
@@ -1113,7 +1182,8 @@ large Blobs to disk.)
1113
1182
  `was.request(...)` mirrors ezcap's generic `request()` for hand-built calls. As
1114
1183
  a deliberate escape hatch it returns the raw `HttpResponse` and throws raw
1115
1184
  ezcap/ky errors -- it does not apply the null-on-404 or typed-error
1116
- conveniences.
1185
+ conveniences. It still waits on service discovery, so against an incompatible
1186
+ server it rejects with `IncompatibleServerError`.
1117
1187
 
1118
1188
  ```ts
1119
1189
  const response = await was.request({ path: `/space/${spaceId}`, method: 'GET' })
@@ -1150,6 +1220,12 @@ instead, as `{ outcome: 'not-found' }`, rather than swallowing it.
1150
1220
  Spec endpoints a given server has not yet implemented surface as
1151
1221
  `NotImplementedError` (the server's 501).
1152
1222
 
1223
+ `IncompatibleServerError` comes from service discovery, not from a response
1224
+ status. Any signed method, read methods included, throws it when the server
1225
+ speaks no WAS version this client understands (see
1226
+ [Service discovery](#service-discovery)). It carries no `status`, so a read
1227
+ method does not mistake it for a 404.
1228
+
1153
1229
  ## Contribute
1154
1230
 
1155
1231
  PRs accepted. See [CONTRIBUTING.md](CONTRIBUTING.md) for editor setup (Prettier,
@@ -2,7 +2,7 @@ import type { ClientContext } from './internal/request.js';
2
2
  import type { IndexDeclaration, IndexSchema } from './codec.js';
3
3
  import { Resource } from './Resource.js';
4
4
  import type { ChangeDocument, ChangesCheckpoint, ChangesPage } from '@interop/storage-core';
5
- import type { AddResult, BackendDescriptor, BackendUsage, CollectionDescription, CollectionMetadata, CollectionWritableFields, EncryptionOverride, FindPage, GrantOptions, HandleOptions, IDelegatedZcap, IZcap, Json, ResourceData, LinkSet, PolicyDocument, CollectionResourcesList, ResourceMetadataCustomInput, ResourceSummary } from './types.js';
5
+ import type { AddResult, BackendDescriptor, BackendUsage, CollectionMetadata, CollectionWritableFields, EncryptionOverride, FindPage, GrantOptions, HandleOptions, IDelegatedZcap, IZcap, Json, ResourceData, LinkSet, PolicyDocument, CollectionResourcesList, ResourceMetadataCustomInput, ResourceSummary } from './types.js';
6
6
  export declare class Collection {
7
7
  #private;
8
8
  readonly spaceId: string;
@@ -26,93 +26,121 @@ export declare class Collection {
26
26
  encryption?: EncryptionOverride;
27
27
  });
28
28
  /**
29
- * Reads the Collection Description. Returns `null` if the collection is
30
- * missing or not visible to you (WAS returns 404 for both not-found and
31
- * unauthorized).
29
+ * Reads the Collection Metadata object -- the Collection's configuration
30
+ * (`name`, `backend`, `encryption`, `generator`) beside the server-managed
31
+ * timestamps and the user-writable `custom`, as one object under one
32
+ * validator. Returns `null` if the collection is missing or not visible to
33
+ * you (WAS returns 404 for both not-found and unauthorized).
32
34
  *
33
- * @returns {Promise<CollectionDescription | null>}
35
+ * This read never resolves the codec, so it is the configuration read: on an
36
+ * encrypted Collection the served `custom` is the opaque envelope, exactly as
37
+ * stored. {@link meta} is the same object with `custom` decoded.
38
+ *
39
+ * @returns {Promise<(CollectionMetadata & { etag?: string }) | null>}
34
40
  */
35
- describe(): Promise<CollectionDescription | null>;
41
+ describe(): Promise<(CollectionMetadata & {
42
+ etag?: string;
43
+ }) | null>;
36
44
  /**
37
- * Creates or updates the collection by id (upsert). Merges the given fields
38
- * over the current description.
45
+ * {@link describe}, with the object and its `ETag` in separate members. The
46
+ * `ETag` is the `metaVersion` validator to pass to
47
+ * {@link replaceDescription}'s `ifMatch` for a lost-update-safe
48
+ * (compare-and-swap) write. One validator covers the whole object, so it is
49
+ * also what {@link setMeta} pins against. Returns `null` if the collection is
50
+ * missing or not visible to you (404 conflation caveat); `etag` is absent
51
+ * against a backend that does not version the object.
39
52
  *
40
- * The merge needs a readable current description to be lost-update-safe, and
41
- * `describe()` cannot distinguish "absent" from "unreadable" (WAS masks
42
- * unauthorized reads as 404). When it returns `null` and either `backend`
43
- * or `encryption` is left out, this fails closed rather than sending a PUT
44
- * body that would silently drop an existing collection's `backend` (a
45
- * data-placement change) or trip `encryption-immutable` by clearing its
46
- * descriptor on a replace-semantics server. Supplying only one of the two
47
- * does not cover the other: with nothing readable to merge from, the
48
- * omitted field is dropped either way. Pass `force: true` to proceed
49
- * anyway
50
- * -- e.g. when creating a new collection through a handle (or use
51
- * `space.createCollection()`, which does not merge).
53
+ * @returns {Promise<{ description: CollectionMetadata; etag?: string } | null>}
54
+ */
55
+ describeWithEtag(): Promise<{
56
+ description: CollectionMetadata;
57
+ etag?: string;
58
+ } | null>;
59
+ /**
60
+ * Creates or updates the collection by id (upsert). Merges the given
61
+ * configuration members over the current ones; the annotations (`custom` and
62
+ * its `epoch` stamp) are carried forward untouched, unless this write
63
+ * changes the encryption scheme, in which case `custom` is re-sealed under
64
+ * the incoming descriptor (see {@link replaceDescription}).
65
+ *
66
+ * The merge runs against the same read the write is pinned to, and rebases
67
+ * with it: a rival configuration change landing in between is re-read and
68
+ * merged over rather than re-applied stale.
69
+ *
70
+ * The merge needs a readable current object to be lost-update-safe, and the
71
+ * read cannot distinguish "absent" from "unreadable" (WAS masks unauthorized
72
+ * reads as 404). When it finds nothing and either `backend` or `encryption`
73
+ * is left out, this fails closed rather than sending a PUT body that would
74
+ * silently drop an existing collection's `backend` (a data-placement change)
75
+ * or trip `encryption-immutable` by clearing its descriptor. Supplying only
76
+ * one of the two does not cover the other: with nothing readable to merge
77
+ * from, the omitted field is dropped either way. Pass `force: true` to
78
+ * proceed anyway -- e.g. when creating a new collection through a handle (or
79
+ * use `space.createCollection()`, which does not merge).
52
80
  *
53
81
  * @param desc {CollectionWritableFields} the fields to merge; `encryption`
54
82
  * declares the client-side encryption descriptor, which is set-once on the
55
83
  * server (it may be added to a Collection that lacks one, but
56
84
  * changing/clearing an existing descriptor is rejected -- `ConflictError`,
57
85
  * `encryption-immutable`)
58
- * @param [desc.force] {boolean} proceed even when the current description
86
+ * @param [desc.force] {boolean} proceed even when the current object
59
87
  * is unreadable and `backend`/`encryption` are omitted (see above)
60
- * @param [desc.current] {CollectionDescription | null} the current
61
- * description, when the caller has already read it -- the merge and the
62
- * fail-closed check then run against this instead of a second `describe()`
63
- * round trip. `null` means the caller read it and found the collection
64
- * absent or unreadable, which is a supplied answer; omitting the member
65
- * entirely is what asks for the read. Supplying a description this
66
- * handle's own writes have since superseded would merge stale fields
67
- * forward -- dropping a `backend` or tripping `encryption-immutable` --
68
- * so pass only a read the caller itself made and has not written over
69
- * @returns {Promise<CollectionDescription>}
88
+ * @param [desc.current] {CollectionMetadata | null} the current Collection
89
+ * Metadata object, when the caller has already read it -- the merge's first
90
+ * attempt then runs against this instead of a second read. It is used only
91
+ * while it carries the `etag` the write pins against, so a copy another
92
+ * client has since overwritten loses the compare-and-swap and is re-read
93
+ * rather than merged forward. `null` means the caller read it and found
94
+ * the collection absent or unreadable; omitting the member entirely is
95
+ * what asks for the read
96
+ * @returns {Promise<CollectionMetadata>}
70
97
  */
71
98
  configure(desc: CollectionWritableFields & {
72
99
  force?: boolean;
73
- current?: CollectionDescription | null;
74
- }): Promise<CollectionDescription>;
100
+ current?: (CollectionMetadata & {
101
+ etag?: string;
102
+ }) | null;
103
+ }): Promise<CollectionMetadata>;
75
104
  /**
76
- * Reads the Collection Description together with its `ETag` validator (the
77
- * server's `conditional-writes` / description-version support). The `ETag` is
78
- * the opaque validator to pass to {@link replaceDescription}'s `ifMatch` for a
79
- * lost-update-safe (compare-and-swap) description write. Returns `null` if the
80
- * collection is missing or not visible to you (404 conflation caveat); `etag`
81
- * is absent against a server that does not version the description.
82
- *
83
- * @returns {Promise<{ description: CollectionDescription; etag?: string } | null>}
84
- */
85
- describeWithEtag(): Promise<{
86
- description: CollectionDescription;
87
- etag?: string;
88
- } | null>;
89
- /**
90
- * Writes (replaces) the Collection Description, optionally under a
91
- * precondition: `ifMatch` (the `ETag` from {@link describeWithEtag}) makes
105
+ * Writes (replaces) the Collection's configuration members, optionally under
106
+ * a precondition: `ifMatch` (the `ETag` from {@link describeWithEtag}) makes
92
107
  * it a compare-and-swap so a concurrent writer cannot be silently clobbered,
93
108
  * and `ifNoneMatch: true` makes it a guarded create that proceeds only while
94
109
  * no Collection exists under this id. A failed precondition surfaces as
95
- * `PreconditionFailedError` (412). Sends the writable fields as the full
96
- * body; omit a field to drop it (replace semantics), so callers doing CAS
97
- * pass every field forward. Returns the new `ETag` and the fields written.
98
- *
99
- * This is the generic description-CAS primitive the key-epoch recipient
110
+ * `PreconditionFailedError` (412). Sends the writable configuration as the
111
+ * full body; omit a member to drop it (replace semantics), so callers doing
112
+ * CAS pass every member forward. Members this client does not model are
113
+ * carried forward from the stored object rather than cleared.
114
+ *
115
+ * The annotations (`custom` and its `epoch` stamp) are carried forward
116
+ * verbatim -- `custom` exactly as served, so an encrypted Collection's
117
+ * envelope is never re-sealed by an ordinary configuration write. The one
118
+ * exception is a write that changes the encryption scheme: the server
119
+ * validates `custom` against the INCOMING descriptor, so a stored plaintext
120
+ * `custom` cannot travel beside a newly declared `encryption` descriptor.
121
+ * Such a write re-seals `custom` under the incoming descriptor's codec, and
122
+ * drops `custom` (and its stamp) when this client cannot build that codec.
123
+ * A Collection with no stored `custom` sends none, which clears an
124
+ * already-empty value on an encrypted Collection just as on a plaintext one.
125
+ * Returns the new `ETag`, and the object the server answers a create with.
126
+ *
127
+ * This is the generic compare-and-swap primitive the key-epoch recipient
100
128
  * operations build on (add/remove a reader is a CAS of the `encryption`
101
129
  * descriptor); it is not epoch-specific.
102
130
  *
103
131
  * @param description {CollectionWritableFields}
104
132
  * @param options {object}
105
133
  * @param [options.ifMatch] {string} the prior `ETag`; the write applies only
106
- * if the description is unchanged
134
+ * if the object is unchanged
107
135
  * @param [options.ifNoneMatch] {boolean} write only if the Collection does
108
136
  * not exist yet
109
- * @returns {Promise<{ description: CollectionDescription; etag?: string }>}
137
+ * @returns {Promise<{ description: CollectionMetadata; etag?: string }>}
110
138
  */
111
139
  replaceDescription(description: CollectionWritableFields, options?: {
112
140
  ifMatch?: string;
113
141
  ifNoneMatch?: boolean;
114
142
  }): Promise<{
115
- description: CollectionDescription;
143
+ description: CollectionMetadata;
116
144
  etag?: string;
117
145
  }>;
118
146
  /**
@@ -123,23 +151,25 @@ export declare class Collection {
123
151
  */
124
152
  delete(): Promise<void>;
125
153
  /**
126
- * Reads the Collection's metadata object (server-managed timestamps,
127
- * `createdBy` and the encrypted-`custom` key `epoch`, plus the user-writable
128
- * `custom` object). Returns `null` if the collection is missing or not
129
- * visible to you (404 conflation caveat). A server without Collection
130
- * metadata support surfaces its 501 as `NotImplementedError`.
154
+ * Reads the Collection Metadata object with its user-writable `custom`
155
+ * decoded -- {@link describe} plus the codec. Returns `null` if the
156
+ * collection is missing or not visible to you (404 conflation caveat). A
157
+ * server without Collection metadata support surfaces its 501 as
158
+ * `NotImplementedError`.
131
159
  *
132
160
  * On an encrypted collection the stored `custom` is an opaque envelope; this
133
161
  * decodes it (decrypts, via the codec) so a caller always sees plaintext
134
162
  * `{ name, tags }`. A collection with no user metadata reports `custom` as
135
- * `{}`.
163
+ * `{}`. Resolving the codec can fail closed on a collection this client holds
164
+ * no keys for, which is why {@link describe} -- the configuration read -- goes
165
+ * without it.
136
166
  *
137
167
  * Against a backend with the `conditional-writes` feature the result also
138
- * carries the metadata's current `etag` (the `/meta` `metaVersion`
139
- * validator) -- pass it as `setMeta(meta, { ifMatch })` for a
140
- * lost-update-safe metadata update. That validator is independent of the
141
- * Collection Description's ETag ({@link describeWithEtag}) and of every
142
- * Resource's versions: writing one never bumps the other.
168
+ * carries the object's current `etag` (its `metaVersion` validator) -- pass it
169
+ * as `setMeta(meta, { ifMatch })` for a lost-update-safe update. One
170
+ * validator covers the whole object: a configuration write and an annotation
171
+ * write advance the same counter, so a client holds one ETag for the
172
+ * Collection rather than one per surface.
143
173
  *
144
174
  * @returns {Promise<(CollectionMetadata & { etag?: string }) | null>}
145
175
  */
@@ -147,24 +177,33 @@ export declare class Collection {
147
177
  etag?: string;
148
178
  }) | null>;
149
179
  /**
150
- * Replaces the Collection's user-writable metadata (`custom`). This is a full
151
- * replacement: any property omitted from `custom` is cleared, and an omitted
152
- * `custom` clears them all. Does not create the collection -- a `PUT` to the
153
- * metadata of a nonexistent collection throws `NotFoundError`. Servers
154
- * without Collection metadata support surface their 501 as
155
- * `NotImplementedError`.
180
+ * Replaces the Collection's user-writable annotations (`custom`). This is a
181
+ * full replacement of `custom`: any property omitted from it is cleared, and
182
+ * an omitted `custom` clears them all. The Collection's configuration
183
+ * members are carried forward -- the same `PUT` replaces the whole
184
+ * Collection Metadata object, so they are read and re-sent rather than
185
+ * dropped.
156
186
  *
157
187
  * On an encrypted collection `custom` is encrypted into an opaque envelope by
158
188
  * the codec before it is sent, so `name` / `tags` are never stored as
159
189
  * server-visible plaintext -- transparently, the same call works on plaintext
160
- * and encrypted collections alike.
161
- *
162
- * Conditional metadata writes (the backend's `conditional-writes` feature):
163
- * pass `ifMatch` (the `etag` from a prior `meta()`) for an
164
- * update-if-unchanged, or `ifNoneMatch: true` for a
165
- * write-only-if-no-metadata. A failed precondition throws
166
- * `PreconditionFailedError` (412). The `/meta` ETag (`metaVersion`) is
167
- * independent of the Collection Description's ETag. Returns the new `etag`.
190
+ * and encrypted collections alike. The key epoch the codec stamps the
191
+ * envelope with travels as a top-level `epoch` member of the same body.
192
+ *
193
+ * Conditional writes (the backend's `conditional-writes` feature): pass
194
+ * `ifMatch` (the `etag` from a prior {@link meta} or {@link describe}) for an
195
+ * update-if-unchanged, and a failed precondition throws
196
+ * `PreconditionFailedError` (412). Without one the write is pinned to the
197
+ * version it composed the configuration from and rebases on a lost race.
198
+ * `ifNoneMatch: true` is the guarded create: one validator covers the whole
199
+ * object, which exists exactly as long as its Collection does, so it means
200
+ * "create the Collection only if it does not exist" -- and creates it with no
201
+ * configuration, the plaintext codec encoding its `custom` (nothing is
202
+ * stored yet to declare encryption, and a per-handle override is the only
203
+ * way to state it). Every other annotation write requires the Collection to
204
+ * be there: one that cannot read the current object throws `NotFoundError`
205
+ * rather than upserting a configuration-less Collection over one this
206
+ * capability cannot see. Returns the new `etag`.
168
207
  *
169
208
  * @param meta {object}
170
209
  * @param [meta.custom] {ResourceMetadataCustomInput} the user-writable
@@ -172,9 +211,10 @@ export declare class Collection {
172
211
  * Collection-level `custom` also carries the persisted `indexSchema`)
173
212
  * while `name` / `tags` themselves stay checked at their stored types
174
213
  * @param options {object}
175
- * @param [options.ifMatch] {string} update only if the `/meta` ETag matches
176
- * @param [options.ifNoneMatch] {boolean} write only if no metadata is set
177
- * @returns {Promise<{ etag?: string }>} the metadata's new ETag
214
+ * @param [options.ifMatch] {string} update only if the `ETag` matches
215
+ * @param [options.ifNoneMatch] {boolean} create only if the Collection does
216
+ * not exist
217
+ * @returns {Promise<{ etag?: string }>} the object's new ETag
178
218
  */
179
219
  setMeta(meta?: {
180
220
  custom?: ResourceMetadataCustomInput;
@@ -185,19 +225,20 @@ export declare class Collection {
185
225
  etag?: string;
186
226
  }>;
187
227
  /**
188
- * Sets the Collection's metadata-level human-readable `name`, preserving any
189
- * existing `tags`. Convenience over `setMeta()`. The write is pinned to the
190
- * `etag` the `meta()` read returned (when the backend supports
191
- * `conditional-writes`), so a concurrent metadata write surfaces as
192
- * `PreconditionFailedError` instead of being silently erased by this
228
+ * Sets the Collection's annotation-level human-readable `name`, preserving
229
+ * any existing `tags`. Convenience over `setMeta()`. The write is pinned to
230
+ * the `etag` the `meta()` read returned (when the backend supports
231
+ * `conditional-writes`) and rebases on a lost race, so a concurrent write --
232
+ * an annotation write or a configuration change, which share the one
233
+ * validator -- is re-read and re-applied rather than silently erased by this
193
234
  * full-replacement write.
194
235
  *
195
236
  * On an encrypted collection this is the collection's client-encrypted name
196
237
  * surface: the codec seals it into the `custom` envelope, and by convention
197
- * the plaintext Description `name` is left unpopulated. On a plaintext
238
+ * the plaintext top-level `name` is left unpopulated. On a plaintext
198
239
  * collection the two are separate labels -- space-level listings surface the
199
- * Description's `name` (set via `configure({ name })`), while this one is
200
- * metadata-level.
240
+ * top-level `name` (set via `configure({ name })`), while this one is inside
241
+ * `custom`.
201
242
  *
202
243
  * @param name {string}
203
244
  * @returns {Promise<void>}
@@ -213,7 +254,7 @@ export declare class Collection {
213
254
  */
214
255
  setTags(tags: Record<string, string>): Promise<void>;
215
256
  /**
216
- * The absolute URL of the Collection's governing history log, the `/meta/log`
257
+ * The absolute URL of the Collection's governing history log, the `meta/log`
217
258
  * sub-resource {@link getHistoryLog} reads. A log-governed descriptor's
218
259
  * `history.resource` names this URL; a verifying reader compares the two
219
260
  * before opening the log.