was-teaching-server 0.9.1
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 +1432 -0
- package/LICENSE +661 -0
- package/README.md +305 -0
- package/common/css/materialize.1.0.0.min.css +13 -0
- package/dist/auth-header-hooks.d.ts +84 -0
- package/dist/auth-header-hooks.d.ts.map +1 -0
- package/dist/auth-header-hooks.js +136 -0
- package/dist/auth-header-hooks.js.map +1 -0
- package/dist/authorize.d.ts +45 -0
- package/dist/authorize.d.ts.map +1 -0
- package/dist/authorize.js +76 -0
- package/dist/authorize.js.map +1 -0
- package/dist/backends/filesystem.d.ts +1179 -0
- package/dist/backends/filesystem.d.ts.map +1 -0
- package/dist/backends/filesystem.js +2727 -0
- package/dist/backends/filesystem.js.map +1 -0
- package/dist/backends/postgres.d.ts +833 -0
- package/dist/backends/postgres.d.ts.map +1 -0
- package/dist/backends/postgres.js +2164 -0
- package/dist/backends/postgres.js.map +1 -0
- package/dist/backends/postgresSchema.d.ts +36 -0
- package/dist/backends/postgresSchema.d.ts.map +1 -0
- package/dist/backends/postgresSchema.js +203 -0
- package/dist/backends/postgresSchema.js.map +1 -0
- package/dist/config.default.d.ts +360 -0
- package/dist/config.default.d.ts.map +1 -0
- package/dist/config.default.js +493 -0
- package/dist/config.default.js.map +1 -0
- package/dist/corsProxy.d.ts +45 -0
- package/dist/corsProxy.d.ts.map +1 -0
- package/dist/corsProxy.js +278 -0
- package/dist/corsProxy.js.map +1 -0
- package/dist/digest.d.ts +45 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +174 -0
- package/dist/digest.js.map +1 -0
- package/dist/errors.d.ts +654 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +960 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/atomicFile.d.ts +61 -0
- package/dist/lib/atomicFile.d.ts.map +1 -0
- package/dist/lib/atomicFile.js +150 -0
- package/dist/lib/atomicFile.js.map +1 -0
- package/dist/lib/backendRegistry.d.ts +59 -0
- package/dist/lib/backendRegistry.d.ts.map +1 -0
- package/dist/lib/backendRegistry.js +92 -0
- package/dist/lib/backendRegistry.js.map +1 -0
- package/dist/lib/backendUsage.d.ts +28 -0
- package/dist/lib/backendUsage.d.ts.map +1 -0
- package/dist/lib/backendUsage.js +55 -0
- package/dist/lib/backendUsage.js.map +1 -0
- package/dist/lib/backends.d.ts +142 -0
- package/dist/lib/backends.d.ts.map +1 -0
- package/dist/lib/backends.js +272 -0
- package/dist/lib/backends.js.map +1 -0
- package/dist/lib/blindedIndex.d.ts +156 -0
- package/dist/lib/blindedIndex.d.ts.map +1 -0
- package/dist/lib/blindedIndex.js +338 -0
- package/dist/lib/blindedIndex.js.map +1 -0
- package/dist/lib/collectionDescription.d.ts +33 -0
- package/dist/lib/collectionDescription.d.ts.map +1 -0
- package/dist/lib/collectionDescription.js +21 -0
- package/dist/lib/collectionDescription.js.map +1 -0
- package/dist/lib/cursor.d.ts +18 -0
- package/dist/lib/cursor.d.ts.map +1 -0
- package/dist/lib/cursor.js +50 -0
- package/dist/lib/cursor.js.map +1 -0
- package/dist/lib/edvEnvelope.d.ts +50 -0
- package/dist/lib/edvEnvelope.d.ts.map +1 -0
- package/dist/lib/edvEnvelope.js +106 -0
- package/dist/lib/edvEnvelope.js.map +1 -0
- package/dist/lib/encryption.d.ts +192 -0
- package/dist/lib/encryption.d.ts.map +1 -0
- package/dist/lib/encryption.js +383 -0
- package/dist/lib/encryption.js.map +1 -0
- package/dist/lib/etag.d.ts +33 -0
- package/dist/lib/etag.d.ts.map +1 -0
- package/dist/lib/etag.js +36 -0
- package/dist/lib/etag.js.map +1 -0
- package/dist/lib/exportManifest.d.ts +29 -0
- package/dist/lib/exportManifest.d.ts.map +1 -0
- package/dist/lib/exportManifest.js +77 -0
- package/dist/lib/exportManifest.js.map +1 -0
- package/dist/lib/importTar.d.ts +81 -0
- package/dist/lib/importTar.d.ts.map +1 -0
- package/dist/lib/importTar.js +301 -0
- package/dist/lib/importTar.js.map +1 -0
- package/dist/lib/isJson.d.ts +16 -0
- package/dist/lib/isJson.d.ts.map +1 -0
- package/dist/lib/isJson.js +17 -0
- package/dist/lib/isJson.js.map +1 -0
- package/dist/lib/keyEpoch.d.ts +38 -0
- package/dist/lib/keyEpoch.d.ts.map +1 -0
- package/dist/lib/keyEpoch.js +75 -0
- package/dist/lib/keyEpoch.js.map +1 -0
- package/dist/lib/keyedMutex.d.ts +28 -0
- package/dist/lib/keyedMutex.d.ts.map +1 -0
- package/dist/lib/keyedMutex.js +45 -0
- package/dist/lib/keyedMutex.js.map +1 -0
- package/dist/lib/kmsModule.d.ts +62 -0
- package/dist/lib/kmsModule.d.ts.map +1 -0
- package/dist/lib/kmsModule.js +327 -0
- package/dist/lib/kmsModule.js.map +1 -0
- package/dist/lib/kmsRecordCipher.d.ts +82 -0
- package/dist/lib/kmsRecordCipher.d.ts.map +1 -0
- package/dist/lib/kmsRecordCipher.js +226 -0
- package/dist/lib/kmsRecordCipher.js.map +1 -0
- package/dist/lib/pagination.d.ts +19 -0
- package/dist/lib/pagination.d.ts.map +1 -0
- package/dist/lib/pagination.js +21 -0
- package/dist/lib/pagination.js.map +1 -0
- package/dist/lib/paths.d.ts +273 -0
- package/dist/lib/paths.d.ts.map +1 -0
- package/dist/lib/paths.js +270 -0
- package/dist/lib/paths.js.map +1 -0
- package/dist/lib/preconditions.d.ts +65 -0
- package/dist/lib/preconditions.d.ts.map +1 -0
- package/dist/lib/preconditions.js +112 -0
- package/dist/lib/preconditions.js.map +1 -0
- package/dist/lib/resourceFileName.d.ts +41 -0
- package/dist/lib/resourceFileName.d.ts.map +1 -0
- package/dist/lib/resourceFileName.js +58 -0
- package/dist/lib/resourceFileName.js.map +1 -0
- package/dist/lib/revocations.d.ts +49 -0
- package/dist/lib/revocations.d.ts.map +1 -0
- package/dist/lib/revocations.js +86 -0
- package/dist/lib/revocations.js.map +1 -0
- package/dist/lib/validateDid.d.ts +19 -0
- package/dist/lib/validateDid.d.ts.map +1 -0
- package/dist/lib/validateDid.js +35 -0
- package/dist/lib/validateDid.js.map +1 -0
- package/dist/lib/validateId.d.ts +66 -0
- package/dist/lib/validateId.d.ts.map +1 -0
- package/dist/lib/validateId.js +126 -0
- package/dist/lib/validateId.js.map +1 -0
- package/dist/plugin.d.ts +115 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +134 -0
- package/dist/plugin.js.map +1 -0
- package/dist/policy.d.ts +78 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +115 -0
- package/dist/policy.js.map +1 -0
- package/dist/provisioning.d.ts +38 -0
- package/dist/provisioning.d.ts.map +1 -0
- package/dist/provisioning.js +72 -0
- package/dist/provisioning.js.map +1 -0
- package/dist/requests/BackendRequest.d.ts +61 -0
- package/dist/requests/BackendRequest.d.ts.map +1 -0
- package/dist/requests/BackendRequest.js +152 -0
- package/dist/requests/BackendRequest.js.map +1 -0
- package/dist/requests/CollectionRequest.d.ts +234 -0
- package/dist/requests/CollectionRequest.d.ts.map +1 -0
- package/dist/requests/CollectionRequest.js +705 -0
- package/dist/requests/CollectionRequest.js.map +1 -0
- package/dist/requests/KeyRequest.d.ts +115 -0
- package/dist/requests/KeyRequest.d.ts.map +1 -0
- package/dist/requests/KeyRequest.js +468 -0
- package/dist/requests/KeyRequest.js.map +1 -0
- package/dist/requests/KeystoreRequest.d.ts +99 -0
- package/dist/requests/KeystoreRequest.d.ts.map +1 -0
- package/dist/requests/KeystoreRequest.js +247 -0
- package/dist/requests/KeystoreRequest.js.map +1 -0
- package/dist/requests/PolicyRequest.d.ts +53 -0
- package/dist/requests/PolicyRequest.d.ts.map +1 -0
- package/dist/requests/PolicyRequest.js +124 -0
- package/dist/requests/PolicyRequest.js.map +1 -0
- package/dist/requests/ResourceRequest.d.ts +134 -0
- package/dist/requests/ResourceRequest.d.ts.map +1 -0
- package/dist/requests/ResourceRequest.js +548 -0
- package/dist/requests/ResourceRequest.js.map +1 -0
- package/dist/requests/RevocationRequest.d.ts +67 -0
- package/dist/requests/RevocationRequest.d.ts.map +1 -0
- package/dist/requests/RevocationRequest.js +187 -0
- package/dist/requests/RevocationRequest.js.map +1 -0
- package/dist/requests/SpaceRequest.d.ts +204 -0
- package/dist/requests/SpaceRequest.d.ts.map +1 -0
- package/dist/requests/SpaceRequest.js +520 -0
- package/dist/requests/SpaceRequest.js.map +1 -0
- package/dist/requests/SpacesRepositoryRequest.d.ts +50 -0
- package/dist/requests/SpacesRepositoryRequest.d.ts.map +1 -0
- package/dist/requests/SpacesRepositoryRequest.js +159 -0
- package/dist/requests/SpacesRepositoryRequest.js.map +1 -0
- package/dist/requests/collectionContext.d.ts +21 -0
- package/dist/requests/collectionContext.d.ts.map +1 -0
- package/dist/requests/collectionContext.js +30 -0
- package/dist/requests/collectionContext.js.map +1 -0
- package/dist/requests/controllerConsent.d.ts +58 -0
- package/dist/requests/controllerConsent.d.ts.map +1 -0
- package/dist/requests/controllerConsent.js +73 -0
- package/dist/requests/controllerConsent.js.map +1 -0
- package/dist/requests/keystoreContext.d.ts +68 -0
- package/dist/requests/keystoreContext.d.ts.map +1 -0
- package/dist/requests/keystoreContext.js +87 -0
- package/dist/requests/keystoreContext.js.map +1 -0
- package/dist/requests/resourceInput.d.ts +19 -0
- package/dist/requests/resourceInput.d.ts.map +1 -0
- package/dist/requests/resourceInput.js +102 -0
- package/dist/requests/resourceInput.js.map +1 -0
- package/dist/requests/spaceContext.d.ts +127 -0
- package/dist/requests/spaceContext.d.ts.map +1 -0
- package/dist/requests/spaceContext.js +201 -0
- package/dist/requests/spaceContext.js.map +1 -0
- package/dist/routes.d.ts +55 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +306 -0
- package/dist/routes.js.map +1 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +60 -0
- package/dist/server.js.map +1 -0
- package/dist/start.d.ts +7 -0
- package/dist/start.d.ts.map +1 -0
- package/dist/start.js +61 -0
- package/dist/start.js.map +1 -0
- package/dist/storage.d.ts +29 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +47 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +915 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/views/home.hbs +9 -0
- package/dist/views/templates/main.hbs +12 -0
- package/dist/zcap.d.ts +267 -0
- package/dist/zcap.d.ts.map +1 -0
- package/dist/zcap.js +478 -0
- package/dist/zcap.js.map +1 -0
- package/package.json +109 -0
- package/src/auth-header-hooks.ts +163 -0
- package/src/authorize.ts +117 -0
- package/src/backends/filesystem.ts +3626 -0
- package/src/backends/postgres.ts +3073 -0
- package/src/backends/postgresSchema.ts +231 -0
- package/src/config.default.ts +657 -0
- package/src/corsProxy.ts +326 -0
- package/src/declarations.d.ts +28 -0
- package/src/digest.ts +223 -0
- package/src/errors.ts +1122 -0
- package/src/index.ts +21 -0
- package/src/lib/atomicFile.ts +176 -0
- package/src/lib/backendRegistry.ts +139 -0
- package/src/lib/backendUsage.ts +79 -0
- package/src/lib/backends.ts +351 -0
- package/src/lib/blindedIndex.ts +466 -0
- package/src/lib/collectionDescription.ts +43 -0
- package/src/lib/cursor.ts +52 -0
- package/src/lib/edvEnvelope.ts +120 -0
- package/src/lib/encryption.ts +472 -0
- package/src/lib/etag.ts +40 -0
- package/src/lib/exportManifest.ts +115 -0
- package/src/lib/importTar.ts +381 -0
- package/src/lib/isJson.ts +18 -0
- package/src/lib/keyEpoch.ts +89 -0
- package/src/lib/keyedMutex.ts +45 -0
- package/src/lib/kmsModule.ts +451 -0
- package/src/lib/kmsRecordCipher.ts +292 -0
- package/src/lib/pagination.ts +22 -0
- package/src/lib/paths.ts +382 -0
- package/src/lib/preconditions.ts +145 -0
- package/src/lib/resourceFileName.ts +69 -0
- package/src/lib/revocations.ts +116 -0
- package/src/lib/validateDid.ts +41 -0
- package/src/lib/validateId.ts +148 -0
- package/src/plugin.ts +288 -0
- package/src/policy.ts +176 -0
- package/src/provisioning.ts +98 -0
- package/src/requests/BackendRequest.ts +219 -0
- package/src/requests/CollectionRequest.ts +899 -0
- package/src/requests/KeyRequest.ts +617 -0
- package/src/requests/KeystoreRequest.ts +325 -0
- package/src/requests/PolicyRequest.ts +169 -0
- package/src/requests/ResourceRequest.ts +649 -0
- package/src/requests/RevocationRequest.ts +267 -0
- package/src/requests/SpaceRequest.ts +686 -0
- package/src/requests/SpacesRepositoryRequest.ts +194 -0
- package/src/requests/collectionContext.ts +41 -0
- package/src/requests/controllerConsent.ts +108 -0
- package/src/requests/keystoreContext.ts +124 -0
- package/src/requests/resourceInput.ts +118 -0
- package/src/requests/spaceContext.ts +311 -0
- package/src/routes.ts +420 -0
- package/src/server.ts +69 -0
- package/src/start.ts +71 -0
- package/src/storage.ts +60 -0
- package/src/types.ts +1016 -0
- package/src/views/home.hbs +9 -0
- package/src/views/templates/main.hbs +12 -0
- package/src/zcap.ts +657 -0
|
@@ -0,0 +1,1179 @@
|
|
|
1
|
+
import { Transform, type Readable } from 'node:stream';
|
|
2
|
+
import type { FastifyBaseLogger } from 'fastify';
|
|
3
|
+
import type { BlindedIndexQuery, BlindedIndexQueryPage } from '../lib/blindedIndex.js';
|
|
4
|
+
import type { SpaceDescription, CollectionDescription, CollectionSummary, CollectionResourcesList, ResourceResult, ResourceMetadata, ResourceMetadataCustom, ResourceInput, ImportStats, PolicyDocument, BackendDescriptor, BackendUsage, CollectionUsage, StorageBackend, StoredBackendRecord, KeystoreConfig, KmsKeyRecord, RevocationRecord, RevocationScope, CapabilitySummary, IDID } from '../types.js';
|
|
5
|
+
/**
|
|
6
|
+
* The on-disk shape of a Resource's metadata sidecar (`.meta.<resourceId>.json`,
|
|
7
|
+
* see `metaSidecarFileName`). Only the server-managed timestamps, the monotonic
|
|
8
|
+
* `version`, and the user-writable `custom` object are persisted; `contentType`
|
|
9
|
+
* / `size` are always derived from the stored representation, never duplicated
|
|
10
|
+
* here.
|
|
11
|
+
*
|
|
12
|
+
* `createdBy` is the DID of whoever created the Resource (spec "Resource
|
|
13
|
+
* Metadata Data Model"): an OPTIONAL server-managed property, absent when no
|
|
14
|
+
* creator was recorded.
|
|
15
|
+
*
|
|
16
|
+
* `version` is the per-Resource monotonic counter that backs the HTTP `ETag`
|
|
17
|
+
* strong validator (see `formatEtag`): it starts at 1 on first content write and
|
|
18
|
+
* increments on each subsequent content write. It is `undefined` only for a
|
|
19
|
+
* Resource written before versioning existed (a legacy sidecar), in which case
|
|
20
|
+
* the backend treats the current version as 0.
|
|
21
|
+
*
|
|
22
|
+
* `metaVersion` is the independent monotonic counter for the `/meta`
|
|
23
|
+
* sub-resource (spec V2 metadata versioning): it starts at 1 on first metadata
|
|
24
|
+
* write and increments on each subsequent one, backing the `/meta` ETag. It is
|
|
25
|
+
* kept separate from `version` so a metadata-only edit does not bump the content
|
|
26
|
+
* ETag (preserving the content-ETag contract), and is `undefined` until the
|
|
27
|
+
* first metadata write. A content write preserves it unchanged.
|
|
28
|
+
*
|
|
29
|
+
* `deleted` marks a **tombstone**: a soft delete that drops the content
|
|
30
|
+
* representation but keeps the sidecar so the change feed (replication) still
|
|
31
|
+
* surfaces it. A
|
|
32
|
+
* tombstone has no `r.<id>...` content file, so it is invisible to every normal
|
|
33
|
+
* read path (which gates on the content file via `_findFile`); only the
|
|
34
|
+
* (future) change feed reads it. `contentType` records the representation's
|
|
35
|
+
* last-known content-type, which the content filename no longer carries once it
|
|
36
|
+
* is gone -- present only on a tombstone (a live Resource derives its
|
|
37
|
+
* content-type from the filename).
|
|
38
|
+
*/
|
|
39
|
+
interface MetaSidecar {
|
|
40
|
+
createdAt: string;
|
|
41
|
+
updatedAt: string;
|
|
42
|
+
createdBy?: IDID;
|
|
43
|
+
version?: number;
|
|
44
|
+
metaVersion?: number;
|
|
45
|
+
custom?: ResourceMetadataCustom | Record<string, unknown>;
|
|
46
|
+
epoch?: string;
|
|
47
|
+
deleted?: boolean;
|
|
48
|
+
contentType?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Builds the on-disk filename for a Resource's metadata sidecar:
|
|
52
|
+
* `.meta.<resourceId>.json`. A dot-file kept alongside the resource
|
|
53
|
+
* representation in the Collection dir (the same convention as `.policy.` /
|
|
54
|
+
* `.collection.`), holding the timestamps and user-writable `custom` object.
|
|
55
|
+
* @param resourceId {string}
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
export declare function metaSidecarFileName(resourceId: string): string;
|
|
59
|
+
export declare class FileSystemBackend implements StorageBackend {
|
|
60
|
+
spacesDir: string;
|
|
61
|
+
/**
|
|
62
|
+
* Root of the WebKMS keystore tree (`data/keystores/<localId>/`), a sibling
|
|
63
|
+
* of `spacesDir` -- the `/kms` facet is deliberately separable from Spaces
|
|
64
|
+
* (own route family, own storage tree).
|
|
65
|
+
*/
|
|
66
|
+
keystoresDir: string;
|
|
67
|
+
/**
|
|
68
|
+
* Root of the Space zcap revocation tree (`data/space-revocations/<spaceId>/`),
|
|
69
|
+
* a sibling of `spacesDir` rather than a subdirectory of each Space. Space
|
|
70
|
+
* revocations deliberately live OUTSIDE the Space's own directory because
|
|
71
|
+
* `listCollections` and `_countLiveResources` treat every subdirectory of a
|
|
72
|
+
* Space dir as a Collection -- a `revocations/` dir nested inside a Space would
|
|
73
|
+
* surface as a phantom Collection (and could collide with a real one), so it
|
|
74
|
+
* gets its own root.
|
|
75
|
+
*/
|
|
76
|
+
spaceRevocationsDir: string;
|
|
77
|
+
logger: FastifyBaseLogger;
|
|
78
|
+
/**
|
|
79
|
+
* Per-Space storage capacity, in bytes (spec "Quotas"). `undefined` means no
|
|
80
|
+
* configured limit -- the backend reports an unlimited quota (state always
|
|
81
|
+
* `ok`) and skips write-path enforcement. A finite value drives the
|
|
82
|
+
* `near-limit` / `over-quota` state thresholds (see `reportUsage`) and is
|
|
83
|
+
* enforced on the write path: `writeResource` and `importSpace` reject writes
|
|
84
|
+
* that would push a Space over capacity with `QuotaExceededError` (507). The
|
|
85
|
+
* constructor normalizes a non-finite ctor option (`Infinity`) to `undefined`.
|
|
86
|
+
*/
|
|
87
|
+
capacityBytes?: number;
|
|
88
|
+
/**
|
|
89
|
+
* Largest single upload the backend accepts, in bytes (spec "Quotas", the
|
|
90
|
+
* `maxUploadBytes` constraint). `undefined` means no per-upload cap. Distinct
|
|
91
|
+
* from `capacityBytes` (the cumulative per-Space quota): a write larger than
|
|
92
|
+
* this cap is rejected with `PayloadTooLargeError` (413) even when the Space
|
|
93
|
+
* has ample headroom, while smaller writes still succeed. Advertised in quota
|
|
94
|
+
* reports under `constraints.maxUploadBytes` and enforced on `writeResource`.
|
|
95
|
+
* The constructor normalizes an unset ctor option to
|
|
96
|
+
* {@link DEFAULT_MAX_UPLOAD_BYTES} (a default-on cap) and a non-finite option
|
|
97
|
+
* (`Infinity`) to `undefined` (explicitly no cap).
|
|
98
|
+
*/
|
|
99
|
+
maxUploadBytes?: number;
|
|
100
|
+
/**
|
|
101
|
+
* Max Spaces a single controller may create (spec "Quotas", a default-on
|
|
102
|
+
* count quota). `undefined` means no cap. Enforced on the Space create path
|
|
103
|
+
* (`writeSpace`): a new Space whose `controller` already owns this many
|
|
104
|
+
* Spaces is rejected with `CountQuotaExceededError` (507); overwriting an
|
|
105
|
+
* existing Space never trips it. The constructor normalizes an unset ctor
|
|
106
|
+
* option to {@link DEFAULT_MAX_SPACES_PER_CONTROLLER} and a non-finite option
|
|
107
|
+
* (`Infinity`) to `undefined` (explicitly no cap). Soft under concurrency,
|
|
108
|
+
* like the byte quota.
|
|
109
|
+
*/
|
|
110
|
+
maxSpacesPerController?: number;
|
|
111
|
+
/**
|
|
112
|
+
* Max Collections a single Space may hold (spec "Quotas", a default-on count
|
|
113
|
+
* quota). `undefined` means no cap. Enforced on the Collection create path
|
|
114
|
+
* (`writeCollection`); overwriting an existing Collection description never
|
|
115
|
+
* trips it. Normalized like {@link maxSpacesPerController}.
|
|
116
|
+
*/
|
|
117
|
+
maxCollectionsPerSpace?: number;
|
|
118
|
+
/**
|
|
119
|
+
* Max live Resources a single Space may hold across all its Collections (spec
|
|
120
|
+
* "Quotas", a default-on count quota). `undefined` means no cap. Enforced on
|
|
121
|
+
* the Resource create path (`writeResource`); a tombstone does not count, and
|
|
122
|
+
* a write over an existing live Resource never trips it. Normalized like
|
|
123
|
+
* {@link maxSpacesPerController}.
|
|
124
|
+
*/
|
|
125
|
+
maxResourcesPerSpace?: number;
|
|
126
|
+
/**
|
|
127
|
+
* Per-Resource write serialization (the `conditional-writes` feature). A
|
|
128
|
+
* content write or delete that carries a precondition reads the current
|
|
129
|
+
* `version`, evaluates `If-Match` / `If-None-Match`, and writes -- all under
|
|
130
|
+
* this lock, keyed per Resource -- so two concurrent writers cannot both
|
|
131
|
+
* observe the same prior version and both succeed. Single-instance only.
|
|
132
|
+
*/
|
|
133
|
+
private _writeMutex;
|
|
134
|
+
/**
|
|
135
|
+
* Per-Space usage totals for the write-path quota pre-flight, so
|
|
136
|
+
* `_assertSpaceHeadroom` does not spawn `du` (a whole-Space tree walk) on
|
|
137
|
+
* every resource write. Entries live `QUOTA_USAGE_CACHE_TTL` ms; each
|
|
138
|
+
* accepted write adds its incoming bytes to the cached total, and deletes
|
|
139
|
+
* invalidate the Space's entry. Quota reports (`reportUsage`) always
|
|
140
|
+
* re-measure. Single-instance only, like `_writeMutex`.
|
|
141
|
+
*/
|
|
142
|
+
private _usageCache;
|
|
143
|
+
constructor({ dataDir, logger, capacityBytes, maxUploadBytes, maxSpacesPerController, maxCollectionsPerSpace, maxResourcesPerSpace }: {
|
|
144
|
+
dataDir: string;
|
|
145
|
+
logger?: FastifyBaseLogger;
|
|
146
|
+
capacityBytes?: number;
|
|
147
|
+
maxUploadBytes?: number;
|
|
148
|
+
maxSpacesPerController?: number;
|
|
149
|
+
maxCollectionsPerSpace?: number;
|
|
150
|
+
maxResourcesPerSpace?: number;
|
|
151
|
+
});
|
|
152
|
+
/**
|
|
153
|
+
* Self-description advertised at `GET /space/:spaceId/backends`. The
|
|
154
|
+
* filesystem backend is the single server-configured default: it stores both
|
|
155
|
+
* JSON documents and binary blobs on disk, so its data survives restarts.
|
|
156
|
+
*
|
|
157
|
+
* It advertises the `conditional-writes` affordance: it exposes a per-Resource
|
|
158
|
+
* `version` as an HTTP `ETag` validator and honors `If-Match` / `If-None-Match`
|
|
159
|
+
* write preconditions atomically (returning `412 precondition-failed` on a
|
|
160
|
+
* mismatch). The remaining `features` vocabulary token (`chunked-streams`)
|
|
161
|
+
* is not implemented yet and is added when it lands.
|
|
162
|
+
* (Client-side encryption is deliberately not a backend feature: encrypted
|
|
163
|
+
* documents are opaque client-encrypted JSON this backend already stores
|
|
164
|
+
* faithfully, with no server cooperation.)
|
|
165
|
+
* @returns {Required<Omit<BackendDescriptor, 'provider' | 'connection'>>}
|
|
166
|
+
*/
|
|
167
|
+
describe(): Required<Omit<BackendDescriptor, 'provider' | 'connection'>>;
|
|
168
|
+
/**
|
|
169
|
+
* Measures disk usage under the Space dir with `du`, returning the grand total
|
|
170
|
+
* and a per-Collection breakdown in one pass. `du -d 1 -B 1` (GNU coreutils)
|
|
171
|
+
* reports each immediate subdirectory (one per Collection) plus the Space dir
|
|
172
|
+
* itself (the total, which also covers top-level Space files such as the
|
|
173
|
+
* `.space.` / `.policy.` documents), all in bytes. An absent Space dir (ENOENT
|
|
174
|
+
* before the dir is provisioned) reports zero usage rather than throwing.
|
|
175
|
+
* @param spaceDir {string}
|
|
176
|
+
* @returns {Promise<{ total: number, byCollection: CollectionUsage[] }>}
|
|
177
|
+
*/
|
|
178
|
+
_diskUsage(spaceDir: string): Promise<{
|
|
179
|
+
total: number;
|
|
180
|
+
byCollection: CollectionUsage[];
|
|
181
|
+
}>;
|
|
182
|
+
/**
|
|
183
|
+
* Measures the bytes this Space consumes on disk for the Space Quota report
|
|
184
|
+
* (spec "Quotas"). `usageBytes` is the `du` total under the Space dir
|
|
185
|
+
* (Collection dirs plus top-level Space files); `usageByCollection` breaks the
|
|
186
|
+
* per-Collection totals out (they sum to slightly less than `usageBytes`,
|
|
187
|
+
* since the Space-level files belong to no Collection).
|
|
188
|
+
*
|
|
189
|
+
* The per-Collection `usageByCollection` breakdown is included only when
|
|
190
|
+
* `includeCollections` is set -- the spec's `?include=collections` opt-in (see
|
|
191
|
+
* the `quotas` handler, which now tolerates the query string via the
|
|
192
|
+
* `allowTargetQuery` ZCap path). On the filesystem the breakdown is free (the
|
|
193
|
+
* one `du -d 1` pass yields it alongside the total), but it is still omitted by
|
|
194
|
+
* default to keep the hot-path payload lean and match the wire contract.
|
|
195
|
+
*
|
|
196
|
+
* `state` / `restrictedActions` derive from usage vs `capacityBytes`: an
|
|
197
|
+
* unlimited backend is always `ok`; a finite capacity yields `near-limit` at
|
|
198
|
+
* `QUOTA_NEAR_LIMIT_FRACTION` of capacity and `over-quota` (with reads/deletes
|
|
199
|
+
* still allowed, but `POST`/`PUT` restricted) at or above full.
|
|
200
|
+
* @param options {object}
|
|
201
|
+
* @param options.spaceId {string}
|
|
202
|
+
* @param [options.includeCollections] {boolean} include the per-Collection
|
|
203
|
+
* breakdown (spec `?include=collections`)
|
|
204
|
+
* @returns {Promise<BackendUsage>}
|
|
205
|
+
*/
|
|
206
|
+
reportUsage({ spaceId, includeCollections }: {
|
|
207
|
+
spaceId: string;
|
|
208
|
+
includeCollections?: boolean;
|
|
209
|
+
}): Promise<BackendUsage>;
|
|
210
|
+
/**
|
|
211
|
+
* Measures the bytes a single Collection consumes on disk for the
|
|
212
|
+
* per-Collection Quota report (spec "Quotas", `GET /space/{id}/{cid}/quota`).
|
|
213
|
+
* `usageBytes` is scoped to the Collection (its slice of the one-pass
|
|
214
|
+
* `_diskUsage` breakdown; zero if the Collection dir is empty or absent),
|
|
215
|
+
* while `state` / `limit` / `restrictedActions` describe the backend's overall
|
|
216
|
+
* condition (derived from the Space total -- the quota is a per-backend limit,
|
|
217
|
+
* not per-Collection). The per-Collection `usageByCollection` breakdown is
|
|
218
|
+
* omitted (a single Collection is the whole report).
|
|
219
|
+
* @param options {object}
|
|
220
|
+
* @param options.spaceId {string}
|
|
221
|
+
* @param options.collectionId {string}
|
|
222
|
+
* @returns {Promise<BackendUsage>}
|
|
223
|
+
*/
|
|
224
|
+
reportCollectionUsage({ spaceId, collectionId }: {
|
|
225
|
+
spaceId: string;
|
|
226
|
+
collectionId: string;
|
|
227
|
+
}): Promise<BackendUsage>;
|
|
228
|
+
/**
|
|
229
|
+
* Builds the backend-identity and condition fields shared by the Space and
|
|
230
|
+
* per-Collection quota reports. `usageBytes` is what the report shows (the
|
|
231
|
+
* Space total or a single Collection's slice); `spaceTotalBytes` drives the
|
|
232
|
+
* `state` / `restrictedActions`, which are backend-wide (the quota is a
|
|
233
|
+
* per-Space limit) and so always measured against the Space total. The
|
|
234
|
+
* `constraints.maxUploadBytes` cap is advertised when configured.
|
|
235
|
+
* @param options {object}
|
|
236
|
+
* @param options.usageBytes {number} the usage figure to report
|
|
237
|
+
* @param options.spaceTotalBytes {number} the Space total, for state
|
|
238
|
+
* @returns {Omit<BackendUsage, 'measuredAt' | 'usageByCollection'>}
|
|
239
|
+
*/
|
|
240
|
+
_backendUsageFields({ usageBytes, spaceTotalBytes }: {
|
|
241
|
+
usageBytes: number;
|
|
242
|
+
spaceTotalBytes: number;
|
|
243
|
+
}): Omit<BackendUsage, 'measuredAt' | 'usageByCollection'>;
|
|
244
|
+
/**
|
|
245
|
+
* Quota pre-flight for the write path (spec "Quotas"): measures the Space's
|
|
246
|
+
* current on-disk usage and returns the remaining headroom in bytes against
|
|
247
|
+
* `capacityBytes`. Throws `QuotaExceededError` (507) when the Space is already
|
|
248
|
+
* at or over capacity, or when a known `incomingBytes` would not fit. Callers
|
|
249
|
+
* pass the configured `capacityBytes` explicitly (an unlimited backend skips
|
|
250
|
+
* enforcement entirely and never calls this).
|
|
251
|
+
*
|
|
252
|
+
* This is a soft limit under concurrency: two simultaneous writes can each pass
|
|
253
|
+
* against the same usage snapshot and jointly overshoot. The per-write
|
|
254
|
+
* streaming guard (`_quotaGuard`) still bounds each individual write.
|
|
255
|
+
*
|
|
256
|
+
* The `du` measurement (a whole-Space tree walk) is cached per Space for
|
|
257
|
+
* `QUOTA_USAGE_CACHE_TTL` ms (see `_usageCache`): between re-measurements
|
|
258
|
+
* each accepted write's `incomingBytes` is added to the cached total, so a
|
|
259
|
+
* burst of writes costs one tree walk, not one per write.
|
|
260
|
+
* @param options {object}
|
|
261
|
+
* @param options.spaceId {string}
|
|
262
|
+
* @param options.capacityBytes {number} the configured per-Space limit
|
|
263
|
+
* @param [options.incomingBytes] {number} known size of the pending write
|
|
264
|
+
* @returns {Promise<number>} remaining headroom in bytes
|
|
265
|
+
*/
|
|
266
|
+
_assertSpaceHeadroom({ spaceId, capacityBytes, incomingBytes }: {
|
|
267
|
+
spaceId: string;
|
|
268
|
+
capacityBytes: number;
|
|
269
|
+
incomingBytes?: number;
|
|
270
|
+
}): Promise<number>;
|
|
271
|
+
/**
|
|
272
|
+
* A pass-through `Transform` that counts the bytes flowing through it and
|
|
273
|
+
* aborts the pipeline with `QuotaExceededError` (507) once the cumulative total
|
|
274
|
+
* would exceed `headroomBytes`. Hard-caps a streamed blob write whose size is
|
|
275
|
+
* not known up front (so the pre-flight check alone cannot catch it), e.g. a
|
|
276
|
+
* multipart upload or a body without `Content-Length`.
|
|
277
|
+
* @param options {object}
|
|
278
|
+
* @param options.spaceId {string}
|
|
279
|
+
* @param options.capacityBytes {number} the configured per-Space limit
|
|
280
|
+
* @param options.headroomBytes {number} max bytes this write may add
|
|
281
|
+
* @returns {Transform}
|
|
282
|
+
*/
|
|
283
|
+
_quotaGuard({ spaceId, capacityBytes, headroomBytes }: {
|
|
284
|
+
spaceId: string;
|
|
285
|
+
capacityBytes: number;
|
|
286
|
+
headroomBytes: number;
|
|
287
|
+
}): Transform;
|
|
288
|
+
/**
|
|
289
|
+
* A pass-through `Transform` that counts the bytes flowing through it and
|
|
290
|
+
* aborts the pipeline with `PayloadTooLargeError` (413) once the cumulative
|
|
291
|
+
* total exceeds `maxUploadBytes`. Caps a single streamed upload whose size is
|
|
292
|
+
* not known up front (e.g. a multipart part or a body without
|
|
293
|
+
* `Content-Length`), independently of the cumulative Space quota.
|
|
294
|
+
* @param options {object}
|
|
295
|
+
* @param options.maxUploadBytes {number} the per-upload cap in bytes
|
|
296
|
+
* @returns {Transform}
|
|
297
|
+
*/
|
|
298
|
+
_uploadCapGuard({ maxUploadBytes }: {
|
|
299
|
+
maxUploadBytes: number;
|
|
300
|
+
}): Transform;
|
|
301
|
+
/**
|
|
302
|
+
* Defense in depth: asserts that a built path stays within the given storage
|
|
303
|
+
* root (`spacesDir` by default; `keystoresDir` for the keystore tree), so a
|
|
304
|
+
* malformed id that somehow slips past request-layer validation can never
|
|
305
|
+
* escape it (path traversal). The request and tar-import layers reject such
|
|
306
|
+
* ids first; this is the last line of defense.
|
|
307
|
+
* @param targetPath {string}
|
|
308
|
+
* @param [rootDir] {string} the containing root; defaults to `spacesDir`
|
|
309
|
+
* @returns {void}
|
|
310
|
+
*/
|
|
311
|
+
_assertContained(targetPath: string, rootDir?: string): void;
|
|
312
|
+
_spaceDir(spaceId: string): string;
|
|
313
|
+
/**
|
|
314
|
+
* The directory holding one Space's zcap revocation records, under the
|
|
315
|
+
* sibling `spaceRevocationsDir` root (see that property's doc for why it is
|
|
316
|
+
* not inside the Space dir), guarded against escaping it.
|
|
317
|
+
* @param spaceId {string}
|
|
318
|
+
* @returns {string}
|
|
319
|
+
*/
|
|
320
|
+
_spaceRevocationDir(spaceId: string): string;
|
|
321
|
+
_collectionDir({ spaceId, collectionId }: {
|
|
322
|
+
spaceId: string;
|
|
323
|
+
collectionId: string;
|
|
324
|
+
}): string;
|
|
325
|
+
/**
|
|
326
|
+
* @param options {object}
|
|
327
|
+
* @param options.spaceId {string}
|
|
328
|
+
* @returns {Promise<string>} Created space storage directory path.
|
|
329
|
+
*/
|
|
330
|
+
_ensureSpaceDir({ spaceId }: {
|
|
331
|
+
spaceId: string;
|
|
332
|
+
}): Promise<string>;
|
|
333
|
+
/**
|
|
334
|
+
* @param options {object}
|
|
335
|
+
* @param options.spaceId {string}
|
|
336
|
+
* @param options.collectionId {string}
|
|
337
|
+
* @returns {Promise<string>} Created collection storage directory path.
|
|
338
|
+
*/
|
|
339
|
+
_ensureCollectionDir({ spaceId, collectionId }: {
|
|
340
|
+
spaceId: string;
|
|
341
|
+
collectionId: string;
|
|
342
|
+
}): Promise<string>;
|
|
343
|
+
/**
|
|
344
|
+
* Lists every on-disk file belonging to a single Resource: the
|
|
345
|
+
* representation(s) whose name starts with `r.<encodedResourceId>.` in the
|
|
346
|
+
* Collection dir. The trailing `.` anchors to the filename's segment boundary
|
|
347
|
+
* (`r.<encodedResourceId>.<encodedType>.<ext>`) so a resourceId that is a
|
|
348
|
+
* prefix of another (e.g. `note` vs `notebook`) does not match the longer one;
|
|
349
|
+
* the id is dot-escaped to match the stored name (see `fileNameFor`). A Resource
|
|
350
|
+
* normally has a single current representation, so this usually returns one
|
|
351
|
+
* path; it returns more only transiently while a prior representation under a
|
|
352
|
+
* different content-type is being pruned. An absent Collection dir resolves an
|
|
353
|
+
* empty list (it holds no such files).
|
|
354
|
+
* @param options {object}
|
|
355
|
+
* @param options.collectionDir {string}
|
|
356
|
+
* @param options.resourceId {string}
|
|
357
|
+
* @returns {Promise<string[]>} full paths, in directory order
|
|
358
|
+
*/
|
|
359
|
+
_resourceFilesFor({ collectionDir, resourceId }: {
|
|
360
|
+
collectionDir: string;
|
|
361
|
+
resourceId: string;
|
|
362
|
+
}): Promise<string[]>;
|
|
363
|
+
/**
|
|
364
|
+
* @param options {object}
|
|
365
|
+
* @param options.collectionDir {string}
|
|
366
|
+
* @param options.resourceId {string}
|
|
367
|
+
* @returns {Promise<string|undefined>} First matching resource file path.
|
|
368
|
+
*/
|
|
369
|
+
_findFile({ collectionDir, resourceId }: {
|
|
370
|
+
collectionDir: string;
|
|
371
|
+
resourceId: string;
|
|
372
|
+
}): Promise<string | undefined>;
|
|
373
|
+
/**
|
|
374
|
+
* @param options {object}
|
|
375
|
+
* @param options.spaceId {string}
|
|
376
|
+
* @param options.spaceDescription {SpaceDescription}
|
|
377
|
+
* @param [options.createdBy] {string} DID of the invoker, recorded as the
|
|
378
|
+
* Space's `createdBy` on first write only
|
|
379
|
+
* @returns {Promise<void>} Resolved value is implementation-defined and ignored.
|
|
380
|
+
*/
|
|
381
|
+
writeSpace({ spaceId, spaceDescription, createdBy }: {
|
|
382
|
+
spaceId: string;
|
|
383
|
+
spaceDescription: SpaceDescription;
|
|
384
|
+
createdBy?: IDID;
|
|
385
|
+
}): Promise<void>;
|
|
386
|
+
/**
|
|
387
|
+
* @param options {object}
|
|
388
|
+
* @param options.spaceId {string}
|
|
389
|
+
* @returns {Promise<SpaceDescription|undefined>}
|
|
390
|
+
* Resolves falsy when the Space does not exist (must not throw).
|
|
391
|
+
*/
|
|
392
|
+
getSpaceDescription({ spaceId }: {
|
|
393
|
+
spaceId: string;
|
|
394
|
+
}): Promise<SpaceDescription | undefined>;
|
|
395
|
+
/**
|
|
396
|
+
* Removes a Space and everything scoped to it. The Space's zcap revocations
|
|
397
|
+
* live in a sibling root (`spaceRevocationsDir`), not under the Space dir, so
|
|
398
|
+
* they are removed explicitly here rather than falling out of the Space dir
|
|
399
|
+
* rm.
|
|
400
|
+
* @param options {object}
|
|
401
|
+
* @param options.spaceId {string}
|
|
402
|
+
* @returns {Promise<void>}
|
|
403
|
+
*/
|
|
404
|
+
deleteSpace({ spaceId }: {
|
|
405
|
+
spaceId: string;
|
|
406
|
+
}): Promise<void>;
|
|
407
|
+
/**
|
|
408
|
+
* Enumerates every Space stored on this backend (each immediate subdirectory
|
|
409
|
+
* of the spaces root), sorted by Space id. An absent spaces root (nothing
|
|
410
|
+
* stored yet) resolves an empty list, not an error; a directory without a
|
|
411
|
+
* readable description file (e.g. a partially deleted Space) is skipped.
|
|
412
|
+
* @returns {Promise<SpaceDescription[]>}
|
|
413
|
+
*/
|
|
414
|
+
listSpaces(): Promise<SpaceDescription[]>;
|
|
415
|
+
/**
|
|
416
|
+
* Counts the live Resources across every Collection of a Space, for the
|
|
417
|
+
* Resource count quota (`maxResourcesPerSpace`). Enumerates each Collection
|
|
418
|
+
* dir and counts distinct Resource ids that have a live representation file
|
|
419
|
+
* (`r.<id>...`); a tombstone (a `.meta.` sidecar with no `r.` file) does not
|
|
420
|
+
* count. An absent Space dir counts zero (not yet provisioned). Soft under
|
|
421
|
+
* concurrency, like the byte quota -- measured at check time by enumeration.
|
|
422
|
+
* @param options {object}
|
|
423
|
+
* @param options.spaceId {string}
|
|
424
|
+
* @returns {Promise<number>} the number of live Resources
|
|
425
|
+
*/
|
|
426
|
+
_countLiveResources({ spaceId }: {
|
|
427
|
+
spaceId: string;
|
|
428
|
+
}): Promise<number>;
|
|
429
|
+
/**
|
|
430
|
+
* @param options {object}
|
|
431
|
+
* @param options.spaceId {string}
|
|
432
|
+
* @returns {Promise<CollectionSummary[]>}
|
|
433
|
+
*/
|
|
434
|
+
listCollections({ spaceId }: {
|
|
435
|
+
spaceId: string;
|
|
436
|
+
}): Promise<CollectionSummary[]>;
|
|
437
|
+
/**
|
|
438
|
+
* @param options {object}
|
|
439
|
+
* @param options.spaceId {string}
|
|
440
|
+
* @returns {Promise<Readable>} tar-stream pack
|
|
441
|
+
*/
|
|
442
|
+
exportSpace({ spaceId }: {
|
|
443
|
+
spaceId: string;
|
|
444
|
+
}): Promise<Readable>;
|
|
445
|
+
/**
|
|
446
|
+
* Merges a WAS space-export tarball into an existing Space (collections and
|
|
447
|
+
* resources that already exist are skipped, not overwritten).
|
|
448
|
+
* @param options {object}
|
|
449
|
+
* @param options.spaceId {string}
|
|
450
|
+
* @param options.tarStream {Readable}
|
|
451
|
+
* @returns {Promise<ImportStats>}
|
|
452
|
+
*/
|
|
453
|
+
importSpace({ spaceId, tarStream }: {
|
|
454
|
+
spaceId: string;
|
|
455
|
+
tarStream: Readable;
|
|
456
|
+
}): Promise<ImportStats>;
|
|
457
|
+
/**
|
|
458
|
+
* @param options {object}
|
|
459
|
+
* @param options.spaceId {string}
|
|
460
|
+
* @param options.collectionId {string}
|
|
461
|
+
* @param options.collectionDescription {CollectionDescription}
|
|
462
|
+
* @param [options.createdBy] {string} DID of the invoker, recorded as the
|
|
463
|
+
* Collection's `createdBy` on first write only
|
|
464
|
+
* @param [options.ifMatch] {string} an `If-Match` compare-and-swap on the
|
|
465
|
+
* current description version (the `key-epochs` feature); a stale validator
|
|
466
|
+
* throws `PreconditionFailedError` (412)
|
|
467
|
+
* @returns {Promise<{ version: number }>} the Collection's new description
|
|
468
|
+
* version (the `ETag` validator)
|
|
469
|
+
*/
|
|
470
|
+
writeCollection({ spaceId, collectionId, collectionDescription, createdBy, ifMatch, assertTransition }: {
|
|
471
|
+
spaceId: string;
|
|
472
|
+
collectionId: string;
|
|
473
|
+
collectionDescription: CollectionDescription;
|
|
474
|
+
createdBy?: IDID;
|
|
475
|
+
ifMatch?: string;
|
|
476
|
+
assertTransition?: (prior?: CollectionDescription & {
|
|
477
|
+
descriptionVersion?: number;
|
|
478
|
+
}) => void;
|
|
479
|
+
}): Promise<{
|
|
480
|
+
version: number;
|
|
481
|
+
}>;
|
|
482
|
+
/**
|
|
483
|
+
* Builds the per-Collection-description serialization key for `_writeMutex`
|
|
484
|
+
* (`desc:<spaceId>/<collectionId>`), so a Collection Description compare-and-
|
|
485
|
+
* swap serializes with itself while staying disjoint from the per-Resource and
|
|
486
|
+
* unique-scan lock namespaces.
|
|
487
|
+
* @param options {object}
|
|
488
|
+
* @param options.spaceId {string}
|
|
489
|
+
* @param options.collectionId {string}
|
|
490
|
+
* @returns {string}
|
|
491
|
+
*/
|
|
492
|
+
_collectionDescLockKey({ spaceId, collectionId }: {
|
|
493
|
+
spaceId: string;
|
|
494
|
+
collectionId: string;
|
|
495
|
+
}): string;
|
|
496
|
+
/**
|
|
497
|
+
* Writes a Collection's description file (creating the Collection dir if
|
|
498
|
+
* needed), with NO count-quota check. The count guard lives in the public
|
|
499
|
+
* `writeCollection`; `importSpace` tracks the Space's Collection count itself
|
|
500
|
+
* (measured once up front) and calls this directly, so it does not
|
|
501
|
+
* re-enumerate the Space per created Collection.
|
|
502
|
+
* @param options {object}
|
|
503
|
+
* @param options.spaceId {string}
|
|
504
|
+
* @param options.collectionId {string}
|
|
505
|
+
* @param options.collectionDescription {CollectionDescription}
|
|
506
|
+
* @param [options.descriptionVersion] {number} the monotonic description
|
|
507
|
+
* version to persist (the `ETag` validator; the `key-epochs` feature). Kept
|
|
508
|
+
* OUT of the wire body -- stored under a reserved `_version` member that
|
|
509
|
+
* `getCollectionDescription` strips and re-surfaces as `descriptionVersion`.
|
|
510
|
+
* When omitted (the import path), a `_version` already on the incoming
|
|
511
|
+
* description is preserved, else it defaults to `1`.
|
|
512
|
+
* @returns {Promise<void>}
|
|
513
|
+
*/
|
|
514
|
+
_persistCollection({ spaceId, collectionId, collectionDescription, descriptionVersion }: {
|
|
515
|
+
spaceId: string;
|
|
516
|
+
collectionId: string;
|
|
517
|
+
collectionDescription: CollectionDescription;
|
|
518
|
+
descriptionVersion?: number;
|
|
519
|
+
}): Promise<void>;
|
|
520
|
+
/**
|
|
521
|
+
* @param options {object}
|
|
522
|
+
* @param options.spaceId {string}
|
|
523
|
+
* @param options.collectionId {string}
|
|
524
|
+
* @returns {Promise<CollectionDescription|undefined>}
|
|
525
|
+
* Resolves falsy when the Collection does not exist (must not throw).
|
|
526
|
+
*/
|
|
527
|
+
getCollectionDescription({ spaceId, collectionId }: {
|
|
528
|
+
spaceId: string;
|
|
529
|
+
collectionId: string;
|
|
530
|
+
}): Promise<(CollectionDescription & {
|
|
531
|
+
descriptionVersion?: number;
|
|
532
|
+
}) | undefined>;
|
|
533
|
+
/**
|
|
534
|
+
* @param options {object}
|
|
535
|
+
* @param options.spaceId {string}
|
|
536
|
+
* @param options.collectionId {string}
|
|
537
|
+
* @returns {Promise<void>}
|
|
538
|
+
*/
|
|
539
|
+
deleteCollection({ spaceId, collectionId }: {
|
|
540
|
+
spaceId: string;
|
|
541
|
+
collectionId: string;
|
|
542
|
+
}): Promise<void>;
|
|
543
|
+
/**
|
|
544
|
+
* Lists a Collection's Resources, OPTIONALLY cursor-paginated (spec
|
|
545
|
+
* "Pagination"). Items are returned in a stable total order -- ascending by
|
|
546
|
+
* `resourceId`, read straight from the `r.<resourceId>.<type>.<ext>` filename
|
|
547
|
+
* (the keyset). A `cursor` resumes the scan at the first id strictly greater
|
|
548
|
+
* than the cursor's anchor, so paging stays correct even if the anchor id was
|
|
549
|
+
* deleted between pages; `limit` bounds the page (clamped to
|
|
550
|
+
* `[1, MAX_PAGE_SIZE]`, default `DEFAULT_PAGE_SIZE`). `next` is built (and
|
|
551
|
+
* present) only when a further page may follow.
|
|
552
|
+
* @param options {object}
|
|
553
|
+
* @param options.spaceId {string}
|
|
554
|
+
* @param options.collectionId {string}
|
|
555
|
+
* @param [options.limit] {number} requested page size
|
|
556
|
+
* @param [options.cursor] {string} opaque cursor from a prior page's `next`
|
|
557
|
+
* @returns {Promise<CollectionResourcesList>}
|
|
558
|
+
*/
|
|
559
|
+
listCollectionItems({ spaceId, collectionId, limit, cursor, collectionDescription: providedDescription }: {
|
|
560
|
+
spaceId: string;
|
|
561
|
+
collectionId: string;
|
|
562
|
+
limit?: number;
|
|
563
|
+
cursor?: string;
|
|
564
|
+
collectionDescription?: CollectionDescription;
|
|
565
|
+
}): Promise<CollectionResourcesList>;
|
|
566
|
+
/**
|
|
567
|
+
* Writes a resource representation (JSON value or byte stream) to disk, under
|
|
568
|
+
* the per-Resource write lock (the `conditional-writes` feature), and bumps
|
|
569
|
+
* the Resource's monotonic `version`. The new version is returned so the
|
|
570
|
+
* request layer can surface it as the response `ETag`.
|
|
571
|
+
*
|
|
572
|
+
* When a conditional-write precondition is supplied it is evaluated against
|
|
573
|
+
* the Resource's current state atomically with the write (under the lock),
|
|
574
|
+
* throwing `PreconditionFailedError` (412) on a mismatch:
|
|
575
|
+
* - `ifNoneMatch` (a create-if-absent `If-None-Match: *`) fails if the
|
|
576
|
+
* Resource already exists.
|
|
577
|
+
* - `ifMatch` (an update-if-unchanged `If-Match: "<etag>"`) fails if the
|
|
578
|
+
* Resource is absent or its current `ETag` does not equal `ifMatch`.
|
|
579
|
+
* `ifNoneMatch` takes precedence when both are supplied (RFC9110).
|
|
580
|
+
*
|
|
581
|
+
* @param options {object}
|
|
582
|
+
* @param options.spaceId {string}
|
|
583
|
+
* @param options.collectionId {string}
|
|
584
|
+
* @param options.resourceId {string}
|
|
585
|
+
* @param options.input {ResourceInput}
|
|
586
|
+
* @param [options.createdBy] {string} DID of the invoker, recorded as the
|
|
587
|
+
* Resource's `createdBy` on first write only
|
|
588
|
+
* @param [options.ifMatch] {string} `If-Match` precondition (a quoted ETag)
|
|
589
|
+
* @param [options.ifNoneMatch] {boolean} `If-None-Match: *` (create-if-absent)
|
|
590
|
+
* @returns {Promise<{ version: number }>} the Resource's new version
|
|
591
|
+
*/
|
|
592
|
+
writeResource({ spaceId, collectionId, resourceId, input, createdBy, epoch, ifMatch, ifNoneMatch }: {
|
|
593
|
+
spaceId: string;
|
|
594
|
+
collectionId: string;
|
|
595
|
+
resourceId: string;
|
|
596
|
+
input: ResourceInput;
|
|
597
|
+
createdBy?: IDID;
|
|
598
|
+
epoch?: string;
|
|
599
|
+
ifMatch?: string;
|
|
600
|
+
ifNoneMatch?: boolean;
|
|
601
|
+
}): Promise<{
|
|
602
|
+
version: number;
|
|
603
|
+
}>;
|
|
604
|
+
/**
|
|
605
|
+
* The critical section of `writeResource`, run under the per-Resource lock:
|
|
606
|
+
* evaluates any precondition, writes the representation, prunes a stale
|
|
607
|
+
* representation under a different content-type, and persists the bumped
|
|
608
|
+
* `version` in the sidecar. See `writeResource` for the parameters.
|
|
609
|
+
* @returns {Promise<{ version: number }>}
|
|
610
|
+
*/
|
|
611
|
+
private _writeResourceLocked;
|
|
612
|
+
/**
|
|
613
|
+
* Builds the per-Resource serialization key for `_writeMutex`
|
|
614
|
+
* (`<spaceId>/<collectionId>/<resourceId>`), so conditional writes to distinct
|
|
615
|
+
* Resources run concurrently while writes to the same Resource are ordered.
|
|
616
|
+
* @param options {object}
|
|
617
|
+
* @param options.spaceId {string}
|
|
618
|
+
* @param options.collectionId {string}
|
|
619
|
+
* @param options.resourceId {string}
|
|
620
|
+
* @returns {string}
|
|
621
|
+
*/
|
|
622
|
+
_resourceLockKey({ spaceId, collectionId, resourceId }: {
|
|
623
|
+
spaceId: string;
|
|
624
|
+
collectionId: string;
|
|
625
|
+
resourceId: string;
|
|
626
|
+
}): string;
|
|
627
|
+
/**
|
|
628
|
+
* The Collection-level mutex key, held by unique-blinded-attribute writes to
|
|
629
|
+
* serialize their conflict scans. Namespaced (`unique:` prefix) so it can
|
|
630
|
+
* never collide with a per-Resource key.
|
|
631
|
+
* @param options {object}
|
|
632
|
+
* @param options.spaceId {string}
|
|
633
|
+
* @param options.collectionId {string}
|
|
634
|
+
* @returns {string}
|
|
635
|
+
*/
|
|
636
|
+
_collectionLockKey({ spaceId, collectionId }: {
|
|
637
|
+
spaceId: string;
|
|
638
|
+
collectionId: string;
|
|
639
|
+
}): string;
|
|
640
|
+
/**
|
|
641
|
+
* Evaluates a conditional-write precondition against a Resource's current
|
|
642
|
+
* on-disk state. MUST be called inside the per-Resource write lock so the
|
|
643
|
+
* check is atomic with the write that follows. Throws
|
|
644
|
+
* `PreconditionFailedError` (412) when the precondition is not met.
|
|
645
|
+
* @param options {object}
|
|
646
|
+
* @param options.collectionDir {string}
|
|
647
|
+
* @param options.resourceId {string}
|
|
648
|
+
* @param [options.ifMatch] {string} a quoted ETag (`If-Match`)
|
|
649
|
+
* @param [options.ifNoneMatch] {boolean} `If-None-Match: *` (create-if-absent)
|
|
650
|
+
* @returns {Promise<void>}
|
|
651
|
+
*/
|
|
652
|
+
_assertWritePrecondition({ collectionDir, resourceId, ifMatch, ifNoneMatch }: {
|
|
653
|
+
collectionDir: string;
|
|
654
|
+
resourceId: string;
|
|
655
|
+
ifMatch?: string;
|
|
656
|
+
ifNoneMatch?: boolean;
|
|
657
|
+
}): Promise<void>;
|
|
658
|
+
/**
|
|
659
|
+
* @param options {object}
|
|
660
|
+
* @param options.spaceId {string}
|
|
661
|
+
* @param options.collectionId {string}
|
|
662
|
+
* @param options.resourceId {string}
|
|
663
|
+
* @param [options.contentType] {string}
|
|
664
|
+
* @returns {Promise<ResourceResult>} includes the Resource's current
|
|
665
|
+
* `version` (the ETag validator) when one is recorded in its sidecar.
|
|
666
|
+
*/
|
|
667
|
+
getResource({ spaceId, collectionId, resourceId }: {
|
|
668
|
+
spaceId: string;
|
|
669
|
+
collectionId: string;
|
|
670
|
+
resourceId: string;
|
|
671
|
+
/**
|
|
672
|
+
* `contentType` is advisory and ignored for lookup: a Resource has a
|
|
673
|
+
* single current representation, resolved by `resourceId` alone.
|
|
674
|
+
*/
|
|
675
|
+
contentType?: string;
|
|
676
|
+
}): Promise<ResourceResult>;
|
|
677
|
+
/**
|
|
678
|
+
* Builds the on-disk path for a Resource's metadata sidecar
|
|
679
|
+
* (`.meta.<resourceId>.json`) in its Collection dir.
|
|
680
|
+
* @param options {object}
|
|
681
|
+
* @param options.collectionDir {string}
|
|
682
|
+
* @param options.resourceId {string}
|
|
683
|
+
* @returns {string}
|
|
684
|
+
*/
|
|
685
|
+
_metaSidecarPath({ collectionDir, resourceId }: {
|
|
686
|
+
collectionDir: string;
|
|
687
|
+
resourceId: string;
|
|
688
|
+
}): string;
|
|
689
|
+
/**
|
|
690
|
+
* Reads a Resource's metadata sidecar. Resolves `undefined` when none has been
|
|
691
|
+
* written yet (e.g. a Resource created before sidecars existed).
|
|
692
|
+
* @param options {object}
|
|
693
|
+
* @param options.collectionDir {string}
|
|
694
|
+
* @param options.resourceId {string}
|
|
695
|
+
* @returns {Promise<MetaSidecar|undefined>}
|
|
696
|
+
*/
|
|
697
|
+
_readMetaSidecar({ collectionDir, resourceId }: {
|
|
698
|
+
collectionDir: string;
|
|
699
|
+
resourceId: string;
|
|
700
|
+
}): Promise<MetaSidecar | undefined>;
|
|
701
|
+
/**
|
|
702
|
+
* Writes a Resource's metadata sidecar (full replacement).
|
|
703
|
+
* @param options {object}
|
|
704
|
+
* @param options.collectionDir {string}
|
|
705
|
+
* @param options.resourceId {string}
|
|
706
|
+
* @param options.sidecar {MetaSidecar}
|
|
707
|
+
* @returns {Promise<void>}
|
|
708
|
+
*/
|
|
709
|
+
_writeMetaSidecar({ collectionDir, resourceId, sidecar }: {
|
|
710
|
+
collectionDir: string;
|
|
711
|
+
resourceId: string;
|
|
712
|
+
sidecar: MetaSidecar;
|
|
713
|
+
}): Promise<void>;
|
|
714
|
+
/**
|
|
715
|
+
* Reads the metadata of a Resource's current representation: the REQUIRED
|
|
716
|
+
* server-managed fields (`contentType`, `size`, both derived from the stored
|
|
717
|
+
* file), plus the OPTIONAL `createdAt` / `updatedAt` timestamps and the
|
|
718
|
+
* user-writable `custom` object read from the sidecar. For a Resource written
|
|
719
|
+
* before sidecars existed, the timestamps fall back to the file's birth/modify
|
|
720
|
+
* times and `custom` is omitted. Resolves `undefined` when the Resource is
|
|
721
|
+
* absent (including a delete race on `stat`).
|
|
722
|
+
*
|
|
723
|
+
* Also surfaces the Resource's content `version` and its `metaVersion` (the
|
|
724
|
+
* two ETag validators) when recorded in the sidecar, so the request layer can
|
|
725
|
+
* set the `ETag` header: HEAD / the resource itself use the content `version`,
|
|
726
|
+
* while `GET /meta` uses `metaVersion`. Both are out-of-band fields the request
|
|
727
|
+
* layer reads for the header; neither is part of the Resource Metadata wire
|
|
728
|
+
* body.
|
|
729
|
+
* @param options {object}
|
|
730
|
+
* @param options.spaceId {string}
|
|
731
|
+
* @param options.collectionId {string}
|
|
732
|
+
* @param options.resourceId {string}
|
|
733
|
+
* @returns {Promise<(ResourceMetadata & { version?: number; metaVersion?:
|
|
734
|
+
* number }) | undefined>}
|
|
735
|
+
*/
|
|
736
|
+
getResourceMetadata({ spaceId, collectionId, resourceId }: {
|
|
737
|
+
spaceId: string;
|
|
738
|
+
collectionId: string;
|
|
739
|
+
resourceId: string;
|
|
740
|
+
}): Promise<(ResourceMetadata & {
|
|
741
|
+
version?: number;
|
|
742
|
+
metaVersion?: number;
|
|
743
|
+
}) | undefined>;
|
|
744
|
+
/**
|
|
745
|
+
* Replaces the user-writable `custom` object of a Resource's metadata sidecar
|
|
746
|
+
* (full replacement; `{}` clears it), bumping `updatedAt` and the independent
|
|
747
|
+
* `metaVersion` (the `/meta` ETag). Does not create a Resource: resolves
|
|
748
|
+
* `undefined` when the Resource is absent so the handler can 404. The content
|
|
749
|
+
* `version` and the two REQUIRED server-managed fields are untouched (a
|
|
750
|
+
* metadata write does not change the stored representation, preserving the
|
|
751
|
+
* content ETag contract). On an encrypted Collection `custom` is the opaque
|
|
752
|
+
* encryption envelope, stored verbatim.
|
|
753
|
+
*
|
|
754
|
+
* Runs under the per-Resource write lock -- the same lock content writes take
|
|
755
|
+
* -- so an `If-Match` / `If-None-Match` precondition (evaluated on
|
|
756
|
+
* `metaVersion`) is atomic with the write and serializes with concurrent
|
|
757
|
+
* content/metadata writes to the same Resource. A precondition mismatch throws
|
|
758
|
+
* `PreconditionFailedError` (412).
|
|
759
|
+
* @param options {object}
|
|
760
|
+
* @param options.spaceId {string}
|
|
761
|
+
* @param options.collectionId {string}
|
|
762
|
+
* @param options.resourceId {string}
|
|
763
|
+
* @param options.custom {ResourceMetadataCustom | Record<string, unknown>}
|
|
764
|
+
* @param [options.ifMatch] {string} `If-Match` on the current `metaVersion`
|
|
765
|
+
* @param [options.ifNoneMatch] {boolean} `If-None-Match: *` -- write only if
|
|
766
|
+
* no metadata has been written yet (`metaVersion` unset)
|
|
767
|
+
* @returns {Promise<{ metaVersion: number } | undefined>} the new
|
|
768
|
+
* `metaVersion`, or `undefined` when the Resource does not exist
|
|
769
|
+
*/
|
|
770
|
+
writeResourceMetadata({ spaceId, collectionId, resourceId, custom, epoch, ifMatch, ifNoneMatch }: {
|
|
771
|
+
spaceId: string;
|
|
772
|
+
collectionId: string;
|
|
773
|
+
resourceId: string;
|
|
774
|
+
custom: ResourceMetadataCustom | Record<string, unknown>;
|
|
775
|
+
epoch?: string;
|
|
776
|
+
ifMatch?: string;
|
|
777
|
+
ifNoneMatch?: boolean;
|
|
778
|
+
}): Promise<{
|
|
779
|
+
metaVersion: number;
|
|
780
|
+
} | undefined>;
|
|
781
|
+
/**
|
|
782
|
+
* Soft-deletes a Resource: drops its content representation but keeps the
|
|
783
|
+
* sidecar as a **tombstone** (`deleted: true`, a bumped `version` and
|
|
784
|
+
* `updatedAt`, the last-known `contentType` retained) so the change feed
|
|
785
|
+
* (replication) still surfaces it until clients catch up (GC of tombstones is
|
|
786
|
+
* future work). With no content file left, the tombstone is invisible to
|
|
787
|
+
* every normal read path
|
|
788
|
+
* (`getResource` / `getResourceMetadata` / `listCollectionItems` all gate on
|
|
789
|
+
* the content file via `_findFile`, so they 404 / skip it), making soft delete
|
|
790
|
+
* transparent to the existing API.
|
|
791
|
+
*
|
|
792
|
+
* When `ifMatch` is supplied (the `conditional-writes` feature), the delete
|
|
793
|
+
* proceeds only if the Resource exists and its current `ETag` matches, else
|
|
794
|
+
* `PreconditionFailedError` (412). The whole read-modify-write runs under the
|
|
795
|
+
* per-Resource write lock so it serializes with concurrent writes. The delete
|
|
796
|
+
* is idempotent: an already-absent Resource (never created, or an existing
|
|
797
|
+
* tombstone) is a no-op, leaving any tombstone's change-feed entry stable.
|
|
798
|
+
* @param options {object}
|
|
799
|
+
* @param options.spaceId {string}
|
|
800
|
+
* @param options.collectionId {string}
|
|
801
|
+
* @param options.resourceId {string}
|
|
802
|
+
* @param [options.ifMatch] {string} `If-Match` precondition (a quoted ETag)
|
|
803
|
+
* @returns {Promise<void>}
|
|
804
|
+
*/
|
|
805
|
+
deleteResource({ spaceId, collectionId, resourceId, ifMatch }: {
|
|
806
|
+
spaceId: string;
|
|
807
|
+
collectionId: string;
|
|
808
|
+
resourceId: string;
|
|
809
|
+
ifMatch?: string;
|
|
810
|
+
}): Promise<void>;
|
|
811
|
+
/**
|
|
812
|
+
* Replication change feed (the `changes` query profile; see the
|
|
813
|
+
* `StorageBackend.changesSince` contract.
|
|
814
|
+
* Enumerates the Collection once, builds a lightweight descriptor for every
|
|
815
|
+
* JSON-document Resource (live) and JSON tombstone, orders them by
|
|
816
|
+
* `(updatedAt, resourceId)`, seeks strictly past `checkpoint`, takes a page of
|
|
817
|
+
* `limit`, and reads JSON bodies ONLY for that page. O(n) over the Collection
|
|
818
|
+
* per call (it must read every sidecar to order by `updatedAt`) -- acceptable
|
|
819
|
+
* for this teaching backend; an indexed backend would answer it from an
|
|
820
|
+
* `updatedAt` index.
|
|
821
|
+
* @param options {object}
|
|
822
|
+
* @param options.spaceId {string}
|
|
823
|
+
* @param options.collectionId {string}
|
|
824
|
+
* @param [options.checkpoint] {{ id: string, updatedAt: string }} resume position
|
|
825
|
+
* @param options.limit {number} page cap (clamped to the backend maximum)
|
|
826
|
+
* @returns {Promise<{ documents: Array<object>, checkpoint: object | null }>}
|
|
827
|
+
*/
|
|
828
|
+
changesSince({ spaceId, collectionId, checkpoint, limit }: {
|
|
829
|
+
spaceId: string;
|
|
830
|
+
collectionId: string;
|
|
831
|
+
checkpoint?: {
|
|
832
|
+
id: string;
|
|
833
|
+
updatedAt: string;
|
|
834
|
+
};
|
|
835
|
+
limit: number;
|
|
836
|
+
}): Promise<{
|
|
837
|
+
documents: Array<{
|
|
838
|
+
resourceId: string;
|
|
839
|
+
version: number;
|
|
840
|
+
metaVersion?: number;
|
|
841
|
+
createdBy?: IDID;
|
|
842
|
+
updatedAt: string;
|
|
843
|
+
deleted: boolean;
|
|
844
|
+
data?: unknown;
|
|
845
|
+
custom?: ResourceMetadataCustom | Record<string, unknown>;
|
|
846
|
+
epoch?: string;
|
|
847
|
+
}>;
|
|
848
|
+
checkpoint: {
|
|
849
|
+
id: string;
|
|
850
|
+
updatedAt: string;
|
|
851
|
+
} | null;
|
|
852
|
+
}>;
|
|
853
|
+
/**
|
|
854
|
+
* Blinded-index query (the `blinded-index` query profile; see the
|
|
855
|
+
* `StorageBackend.queryByBlindedIndex` contract). Enumerates the Collection
|
|
856
|
+
* dir, reads and parses every live JSON Resource, and hands the candidates
|
|
857
|
+
* to the shared evaluator (`lib/blindedIndex.ts`) for matching, ordering,
|
|
858
|
+
* and cursor pagination. O(n) over the Collection per call, with every JSON
|
|
859
|
+
* body read -- acceptable for this teaching backend; an indexed backend
|
|
860
|
+
* would answer from flattened attribute tokens. Tombstones are excluded
|
|
861
|
+
* naturally (no live content file); binary Resources and unparsable JSON
|
|
862
|
+
* are skipped.
|
|
863
|
+
* @param options {object}
|
|
864
|
+
* @param options.spaceId {string}
|
|
865
|
+
* @param options.collectionId {string}
|
|
866
|
+
* @param options.query {BlindedIndexQuery}
|
|
867
|
+
* @param [options.count] {boolean} return only the match count
|
|
868
|
+
* @param [options.limit] {number} requested page size
|
|
869
|
+
* @param [options.cursor] {string} opaque cursor from a prior page
|
|
870
|
+
* @returns {Promise<{ count: number } | BlindedIndexQueryPage>}
|
|
871
|
+
*/
|
|
872
|
+
queryByBlindedIndex({ spaceId, collectionId, query, count, limit, cursor }: {
|
|
873
|
+
spaceId: string;
|
|
874
|
+
collectionId: string;
|
|
875
|
+
query: BlindedIndexQuery;
|
|
876
|
+
count?: boolean;
|
|
877
|
+
limit?: number;
|
|
878
|
+
cursor?: string;
|
|
879
|
+
}): Promise<{
|
|
880
|
+
count: number;
|
|
881
|
+
} | BlindedIndexQueryPage>;
|
|
882
|
+
/**
|
|
883
|
+
* Reads and parses every live JSON Resource of a Collection -- the candidate
|
|
884
|
+
* set for both the blinded-index query and the unique-attribute conflict
|
|
885
|
+
* scan. Tombstones are excluded naturally (no live content file); binary
|
|
886
|
+
* Resources, unparsable JSON, and (optionally) one excluded Resource are
|
|
887
|
+
* skipped. An absent Collection dir resolves empty.
|
|
888
|
+
* @param options {object}
|
|
889
|
+
* @param options.spaceId {string}
|
|
890
|
+
* @param options.collectionId {string}
|
|
891
|
+
* @param [options.excludeResourceId] {string} omit this Resource (the
|
|
892
|
+
* conflict scan excludes the document being written)
|
|
893
|
+
* @returns {Promise<Array<{ resourceId: string, document: unknown }>>}
|
|
894
|
+
*/
|
|
895
|
+
private _readJsonCandidates;
|
|
896
|
+
/**
|
|
897
|
+
* Builds the on-disk path for a policy document. Stored as a dot-file keyed by
|
|
898
|
+
* the entity id, alongside the matching `.space.` / `.collection.` description:
|
|
899
|
+
* Space policy in the space dir, Collection and Resource policy in the
|
|
900
|
+
* collection dir (the keying id differs, so they never collide).
|
|
901
|
+
* @param options {object}
|
|
902
|
+
* @param options.spaceId {string}
|
|
903
|
+
* @param [options.collectionId] {string}
|
|
904
|
+
* @param [options.resourceId] {string}
|
|
905
|
+
* @returns {string}
|
|
906
|
+
*/
|
|
907
|
+
_policyFile({ spaceId, collectionId, resourceId }: {
|
|
908
|
+
spaceId: string;
|
|
909
|
+
collectionId?: string;
|
|
910
|
+
resourceId?: string;
|
|
911
|
+
}): string;
|
|
912
|
+
/**
|
|
913
|
+
* @param options {object}
|
|
914
|
+
* @param options.spaceId {string}
|
|
915
|
+
* @param [options.collectionId] {string}
|
|
916
|
+
* @param [options.resourceId] {string}
|
|
917
|
+
* @returns {Promise<PolicyDocument|undefined>}
|
|
918
|
+
* Resolves falsy when no policy is set at that level (must not throw).
|
|
919
|
+
*/
|
|
920
|
+
getPolicy({ spaceId, collectionId, resourceId }: {
|
|
921
|
+
spaceId: string;
|
|
922
|
+
collectionId?: string;
|
|
923
|
+
resourceId?: string;
|
|
924
|
+
}): Promise<PolicyDocument | undefined>;
|
|
925
|
+
/**
|
|
926
|
+
* @param options {object}
|
|
927
|
+
* @param options.spaceId {string}
|
|
928
|
+
* @param [options.collectionId] {string}
|
|
929
|
+
* @param [options.resourceId] {string}
|
|
930
|
+
* @param options.policy {PolicyDocument}
|
|
931
|
+
* @returns {Promise<void>}
|
|
932
|
+
*/
|
|
933
|
+
writePolicy({ spaceId, collectionId, resourceId, policy }: {
|
|
934
|
+
spaceId: string;
|
|
935
|
+
collectionId?: string;
|
|
936
|
+
resourceId?: string;
|
|
937
|
+
policy: PolicyDocument;
|
|
938
|
+
}): Promise<void>;
|
|
939
|
+
/**
|
|
940
|
+
* @param options {object}
|
|
941
|
+
* @param options.spaceId {string}
|
|
942
|
+
* @param [options.collectionId] {string}
|
|
943
|
+
* @param [options.resourceId] {string}
|
|
944
|
+
* @returns {Promise<void>} idempotent (no error if absent)
|
|
945
|
+
*/
|
|
946
|
+
deletePolicy({ spaceId, collectionId, resourceId }: {
|
|
947
|
+
spaceId: string;
|
|
948
|
+
collectionId?: string;
|
|
949
|
+
resourceId?: string;
|
|
950
|
+
}): Promise<void>;
|
|
951
|
+
/**
|
|
952
|
+
* Builds the on-disk path for a registered backend record: a
|
|
953
|
+
* `.backend.<backendId>.json` dot-file in the Space dir (the same per-file
|
|
954
|
+
* convention as `.policy.` / `.space.`). One file per backend id.
|
|
955
|
+
* @param options {object}
|
|
956
|
+
* @param options.spaceId {string}
|
|
957
|
+
* @param options.backendId {string}
|
|
958
|
+
* @returns {string}
|
|
959
|
+
*/
|
|
960
|
+
_backendFile({ spaceId, backendId }: {
|
|
961
|
+
spaceId: string;
|
|
962
|
+
backendId: string;
|
|
963
|
+
}): string;
|
|
964
|
+
/**
|
|
965
|
+
* Persists a full (secret-bearing) backend-registration record. Upsert.
|
|
966
|
+
* @param options {object}
|
|
967
|
+
* @param options.spaceId {string}
|
|
968
|
+
* @param options.backendId {string}
|
|
969
|
+
* @param options.record {StoredBackendRecord}
|
|
970
|
+
* @returns {Promise<void>}
|
|
971
|
+
*/
|
|
972
|
+
writeBackend({ spaceId, backendId, record }: {
|
|
973
|
+
spaceId: string;
|
|
974
|
+
backendId: string;
|
|
975
|
+
record: StoredBackendRecord;
|
|
976
|
+
}): Promise<void>;
|
|
977
|
+
/**
|
|
978
|
+
* Reads the full (secret-bearing) record for one backend, for internal use
|
|
979
|
+
* (existence checks, the future provider adapter). Resolves `undefined` when
|
|
980
|
+
* absent. The only method that exposes secret connection material.
|
|
981
|
+
* @param options {object}
|
|
982
|
+
* @param options.spaceId {string}
|
|
983
|
+
* @param options.backendId {string}
|
|
984
|
+
* @returns {Promise<StoredBackendRecord|undefined>}
|
|
985
|
+
*/
|
|
986
|
+
getBackend({ spaceId, backendId }: {
|
|
987
|
+
spaceId: string;
|
|
988
|
+
backendId: string;
|
|
989
|
+
}): Promise<StoredBackendRecord | undefined>;
|
|
990
|
+
/**
|
|
991
|
+
* Enumerates the Space's registered backends and returns them **sanitized**
|
|
992
|
+
* (each mapped through `sanitizeBackendRecord`, so the secret connection
|
|
993
|
+
* material never reaches the listing), sorted by id. An absent Space dir
|
|
994
|
+
* reports no registered backends rather than throwing.
|
|
995
|
+
* @param options {object}
|
|
996
|
+
* @param options.spaceId {string}
|
|
997
|
+
* @returns {Promise<BackendDescriptor[]>}
|
|
998
|
+
*/
|
|
999
|
+
listBackends({ spaceId }: {
|
|
1000
|
+
spaceId: string;
|
|
1001
|
+
}): Promise<BackendDescriptor[]>;
|
|
1002
|
+
/**
|
|
1003
|
+
* Removes a registered backend record. Idempotent (no error if absent).
|
|
1004
|
+
* @param options {object}
|
|
1005
|
+
* @param options.spaceId {string}
|
|
1006
|
+
* @param options.backendId {string}
|
|
1007
|
+
* @returns {Promise<void>}
|
|
1008
|
+
*/
|
|
1009
|
+
deleteBackend({ spaceId, backendId }: {
|
|
1010
|
+
spaceId: string;
|
|
1011
|
+
backendId: string;
|
|
1012
|
+
}): Promise<void>;
|
|
1013
|
+
/**
|
|
1014
|
+
* WebKMS keystores (the `/kms` facet). A sibling tree to Spaces:
|
|
1015
|
+
* `keystores/<localId>/` holds a keystore's `config.json` now, and its key
|
|
1016
|
+
* records / revocations in later tracks -- hence a directory per keystore
|
|
1017
|
+
* rather than a flat file.
|
|
1018
|
+
*
|
|
1019
|
+
* The directory holding one keystore's records, contained in `keystoresDir`.
|
|
1020
|
+
* @param keystoreId {string} the keystore's server-generated local id
|
|
1021
|
+
* @returns {string}
|
|
1022
|
+
*/
|
|
1023
|
+
_keystoreDir(keystoreId: string): string;
|
|
1024
|
+
_keystoreConfigFile(keystoreId: string): string;
|
|
1025
|
+
/**
|
|
1026
|
+
* Persists a keystore config unconditionally (the create path -- local ids
|
|
1027
|
+
* are server-generated random values, so create never collides). The
|
|
1028
|
+
* sequence-gated update path is `updateKeystore`.
|
|
1029
|
+
* @param options {object}
|
|
1030
|
+
* @param options.keystoreId {string} the keystore's local id
|
|
1031
|
+
* @param options.config {KeystoreConfig}
|
|
1032
|
+
* @returns {Promise<void>}
|
|
1033
|
+
*/
|
|
1034
|
+
writeKeystore({ keystoreId, config }: {
|
|
1035
|
+
keystoreId: string;
|
|
1036
|
+
config: KeystoreConfig;
|
|
1037
|
+
}): Promise<void>;
|
|
1038
|
+
/**
|
|
1039
|
+
* @param options {object}
|
|
1040
|
+
* @param options.keystoreId {string} the keystore's local id
|
|
1041
|
+
* @returns {Promise<KeystoreConfig|undefined>}
|
|
1042
|
+
* Resolves falsy when the keystore does not exist (must not throw).
|
|
1043
|
+
*/
|
|
1044
|
+
getKeystore({ keystoreId }: {
|
|
1045
|
+
keystoreId: string;
|
|
1046
|
+
}): Promise<KeystoreConfig | undefined>;
|
|
1047
|
+
/**
|
|
1048
|
+
* Replaces a keystore config, gated atomically (under the per-keystore write
|
|
1049
|
+
* mutex) on: the keystore existing, `config.sequence` being exactly the
|
|
1050
|
+
* stored sequence + 1, and `config.kmsModule` matching the stored one (the
|
|
1051
|
+
* module is immutable). Any other state rejects with the protocol's single
|
|
1052
|
+
* merged 409 conflict.
|
|
1053
|
+
* @param options {object}
|
|
1054
|
+
* @param options.keystoreId {string} the keystore's local id
|
|
1055
|
+
* @param options.config {KeystoreConfig}
|
|
1056
|
+
* @returns {Promise<void>}
|
|
1057
|
+
*/
|
|
1058
|
+
updateKeystore({ keystoreId, config }: {
|
|
1059
|
+
keystoreId: string;
|
|
1060
|
+
config: KeystoreConfig;
|
|
1061
|
+
}): Promise<void>;
|
|
1062
|
+
/**
|
|
1063
|
+
* Every stored keystore config whose `controller` matches, sorted by local
|
|
1064
|
+
* id (the request layer caps the wire result). An absent keystores root
|
|
1065
|
+
* (nothing stored yet) resolves an empty list; a directory without a
|
|
1066
|
+
* readable config file is skipped.
|
|
1067
|
+
* @param options {object}
|
|
1068
|
+
* @param options.controller {IDID}
|
|
1069
|
+
* @returns {Promise<KeystoreConfig[]>}
|
|
1070
|
+
*/
|
|
1071
|
+
listKeystoresByController({ controller }: {
|
|
1072
|
+
controller: IDID;
|
|
1073
|
+
}): Promise<KeystoreConfig[]>;
|
|
1074
|
+
/**
|
|
1075
|
+
* The file holding one key record, contained in its keystore's `keys/`
|
|
1076
|
+
* subdirectory. The record is a plain JSON file (not a metadata store):
|
|
1077
|
+
* records are immutable once inserted, so there is no read-modify-write to
|
|
1078
|
+
* protect.
|
|
1079
|
+
* @param options {object}
|
|
1080
|
+
* @param options.keystoreId {string} the owning keystore's local id
|
|
1081
|
+
* @param options.localId {string} the key's local id
|
|
1082
|
+
* @returns {string}
|
|
1083
|
+
*/
|
|
1084
|
+
_keyFile({ keystoreId, localId }: {
|
|
1085
|
+
keystoreId: string;
|
|
1086
|
+
localId: string;
|
|
1087
|
+
}): string;
|
|
1088
|
+
/**
|
|
1089
|
+
* Inserts a key record, create-only: the exclusive-create write (`wx`)
|
|
1090
|
+
* enforces the `(keystoreId, localId)` uniqueness atomically, rejecting a
|
|
1091
|
+
* duplicate with the protocol's 409 (`KeyIdConflictError`).
|
|
1092
|
+
* @param options {object}
|
|
1093
|
+
* @param options.keystoreId {string} the owning keystore's local id
|
|
1094
|
+
* @param options.localId {string} the key's local id
|
|
1095
|
+
* @param options.record {KmsKeyRecord} the full (secret-bearing) record
|
|
1096
|
+
* @returns {Promise<void>}
|
|
1097
|
+
*/
|
|
1098
|
+
insertKey({ keystoreId, localId, record }: {
|
|
1099
|
+
keystoreId: string;
|
|
1100
|
+
localId: string;
|
|
1101
|
+
record: KmsKeyRecord;
|
|
1102
|
+
}): Promise<void>;
|
|
1103
|
+
/**
|
|
1104
|
+
* @param options {object}
|
|
1105
|
+
* @param options.keystoreId {string} the owning keystore's local id
|
|
1106
|
+
* @param options.localId {string} the key's local id
|
|
1107
|
+
* @returns {Promise<KmsKeyRecord|undefined>}
|
|
1108
|
+
* Resolves falsy when the key does not exist (must not throw).
|
|
1109
|
+
*/
|
|
1110
|
+
getKey({ keystoreId, localId }: {
|
|
1111
|
+
keystoreId: string;
|
|
1112
|
+
localId: string;
|
|
1113
|
+
}): Promise<KmsKeyRecord | undefined>;
|
|
1114
|
+
/**
|
|
1115
|
+
* Every stored key record under the keystore (`keys/*.json`), sorted by local
|
|
1116
|
+
* id (the file name's stem). An absent keystore or `keys/` directory (no keys
|
|
1117
|
+
* yet) resolves an empty list; a non-`.json` entry is skipped. The record is
|
|
1118
|
+
* returned verbatim -- the at-rest cipher applies above the backend.
|
|
1119
|
+
* @param options {object}
|
|
1120
|
+
* @param options.keystoreId {string} the owning keystore's local id
|
|
1121
|
+
* @returns {Promise<Array<{ localId: string, record: KmsKeyRecord }>>}
|
|
1122
|
+
*/
|
|
1123
|
+
listKeys({ keystoreId }: {
|
|
1124
|
+
keystoreId: string;
|
|
1125
|
+
}): Promise<Array<{
|
|
1126
|
+
localId: string;
|
|
1127
|
+
record: KmsKeyRecord;
|
|
1128
|
+
}>>;
|
|
1129
|
+
/**
|
|
1130
|
+
* The file holding one zcap revocation record. For a keystore scope it lives
|
|
1131
|
+
* in that keystore's `revocations/` subdirectory; for a Space scope it lives
|
|
1132
|
+
* under the sibling `spaceRevocationsDir` root (NOT inside the Space dir --
|
|
1133
|
+
* see the `spaceRevocationsDir` property doc). The file name folds the
|
|
1134
|
+
* `(delegator, capabilityId)` unique key into a SHA-256 digest (the shared
|
|
1135
|
+
* `revocationFileName` codec).
|
|
1136
|
+
* @param options {object}
|
|
1137
|
+
* @param options.scope {RevocationScope} the owning keystore or Space
|
|
1138
|
+
* @param options.delegator {string} the revoked capability's delegator
|
|
1139
|
+
* @param options.capabilityId {string} the revoked capability's id
|
|
1140
|
+
* @returns {string}
|
|
1141
|
+
*/
|
|
1142
|
+
_revocationFile({ scope, delegator, capabilityId }: {
|
|
1143
|
+
scope: RevocationScope;
|
|
1144
|
+
delegator: string;
|
|
1145
|
+
capabilityId: string;
|
|
1146
|
+
}): string;
|
|
1147
|
+
/**
|
|
1148
|
+
* Inserts a revocation record, create-only: the exclusive-create write
|
|
1149
|
+
* (`wx`) enforces the `(delegator, capability.id)` uniqueness atomically,
|
|
1150
|
+
* rejecting a duplicate with the protocol's 409
|
|
1151
|
+
* (`DuplicateRevocationError`).
|
|
1152
|
+
* @param options {object}
|
|
1153
|
+
* @param options.scope {RevocationScope} the owning keystore or Space
|
|
1154
|
+
* @param options.record {RevocationRecord} the revocation to store
|
|
1155
|
+
* @returns {Promise<void>}
|
|
1156
|
+
*/
|
|
1157
|
+
insertRevocation({ scope, record }: {
|
|
1158
|
+
scope: RevocationScope;
|
|
1159
|
+
record: RevocationRecord;
|
|
1160
|
+
}): Promise<void>;
|
|
1161
|
+
/**
|
|
1162
|
+
* True when any of the given capabilities has a stored, unexpired
|
|
1163
|
+
* revocation under the scope (keystore or Space). A record past its
|
|
1164
|
+
* `meta.expires` GC horizon is pruned on the way through and counts as not
|
|
1165
|
+
* revoked -- the capability itself has expired, so verification already
|
|
1166
|
+
* rejects it on expiry (this is the filesystem analogue of a TTL index).
|
|
1167
|
+
* @param options {object}
|
|
1168
|
+
* @param options.scope {RevocationScope} the owning keystore or Space
|
|
1169
|
+
* @param options.capabilities {CapabilitySummary[]} the
|
|
1170
|
+
* `(capabilityId, delegator)` pairs to check
|
|
1171
|
+
* @returns {Promise<boolean>}
|
|
1172
|
+
*/
|
|
1173
|
+
isRevoked({ scope, capabilities }: {
|
|
1174
|
+
scope: RevocationScope;
|
|
1175
|
+
capabilities: CapabilitySummary[];
|
|
1176
|
+
}): Promise<boolean>;
|
|
1177
|
+
}
|
|
1178
|
+
export {};
|
|
1179
|
+
//# sourceMappingURL=filesystem.d.ts.map
|