@cloudbitmaps/gcs 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,13 +14,23 @@ second service to run.
14
14
  pnpm add @cloudbitmaps/roaring @cloudbitmaps/gcs
15
15
  ```
16
16
 
17
- On npm 12 and pnpm 10 and later, allow `roaring`'s one install script first, or the install exits 0 and the package throws at `import`. Put this in your `package.json`: `{ "allowScripts": { "roaring": true }, "pnpm": { "onlyBuiltDependencies": ["roaring"] } }`. npm 11 runs the script but warns until you allow it the same way; pnpm 9 needs nothing extra. [Details](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/getting-started.md#cannot-find-module-buildreleaseroaringnode-after-a-successful-install).
17
+ Install the same version of every `@cloudbitmaps/*` package: they release together, and each pins the exact version of `core` it was built with.
18
+
19
+ `roaring` needs its one install script to fetch its native binary, and newer clients run a dependency's script only if you allow it. Allow it before you install:
20
+
21
+ - **npm 12:** add `{ "allowScripts": { "roaring": true } }` to your `package.json`. Without it the install exits 0 and the package throws at `import`. npm 11 runs the script but warns until you allow it the same way.
22
+ - **pnpm 11 and later:** put `allowBuilds: { roaring: true }` in a `pnpm-workspace.yaml` beside your `package.json`. Those versions no longer read the `package.json` `pnpm` key. Without it the install exits 1 with `ERR_PNPM_IGNORED_BUILDS`.
23
+ - **pnpm 10:** add `{ "pnpm": { "onlyBuiltDependencies": ["roaring"] } }` to your `package.json`. Without it the install exits 0 and the package throws at `import`.
24
+ - **pnpm 9:** nothing extra.
25
+
26
+ [Details](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/getting-started.md#cannot-find-module-buildreleaseroaringnode-after-a-successful-install).
18
27
 
19
28
  `@google-cloud/storage` (`^7 || ^8`) is a real dependency of this package, so installing it is the whole step.
20
29
  `@cloudbitmaps/core`, the engine underneath, arrives with it and you never name it.
21
30
 
22
31
  ## Use
23
32
 
33
+ <!-- typecheck -->
24
34
  ```ts
25
35
  import { CloudRoaring } from '@cloudbitmaps/roaring';
26
36
  import { GcsStorage } from '@cloudbitmaps/gcs';
@@ -49,10 +59,18 @@ It builds its own client from Application Default Credentials. Any other key is
49
59
 
50
60
  - **This backend is outside the validated envelope.** It passes the same conformance suites as S3, but the proven
51
61
  scale and the calibration were done on S3. You are an early user.
62
+ - **Run `await store.preflight()` once before the first load.** It writes, tries to write again, reads, lists and deletes one probe
63
+ object, and reads the registry and the bucket's lifecycle rules (`storage.buckets.get` for the last), and answers
64
+ `{ ok, checks }`. It does not check a read after a delete or versioning, among others: [the full list](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/production.md#what-preflight-does-not-check). A first run is eight requests here.
52
65
  - **Grant the bucket's object permissions.** The library creates, reads, lists and deletes objects:
53
66
  `roles/storage.objectAdmin` covers all four.
54
67
  - **The write-once guarantee rides GCS preconditions** (`ifGenerationMatch`), so nothing else needs enabling.
55
68
  - **Never add a lifecycle rule that expires current objects or the `registry/` prefix.**
69
+ - **Check the bucket's lifecycle rules with `await backend.checkLifecycle()`.** It reads the bucket's metadata with one
70
+ request (bounded by `readTimeoutMs`, when set) and answers `problem` for a `Delete` rule that applies to live objects and can match your prefix, `ok`, or
71
+ `unchecked`. It needs `storage.buckets.get`, which a load never uses, `store.preflight()`'s lifecycle step does, and `roles/storage.objectAdmin` does
72
+ not include: without it the answer is `unchecked`, naming the permission
73
+ ([check the lifecycle rules from code](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/production.md#check-the-lifecycle-rules-from-code)).
56
74
  - **The SDK does not retry downloads; the driver does.** In `@google-cloud/storage` 7.x and 8.x (checked on 7.22.0 and
57
75
  8.1.0), a download the SDK retries after any status it retries (408, 429, 500, 502, 503 or 504) can crash the process
58
76
  with `ERR_STREAM_UNABLE_TO_PIPE`, thrown outside any promise, even though the retried request succeeded. The client
@@ -98,17 +116,43 @@ It builds its own client from Application Default Credentials. Any other key is
98
116
  global agent closes a connection idle for 5 seconds (the SDK's own pool does not), so a read after a longer pause
99
117
  opens a new connection, with its TCP and TLS handshake, and downloads share that agent with any other `http` or
100
118
  `https` request in the process. Raising `https.globalAgent.options.timeout` keeps idle connections longer.
101
- - **A transient failure of a write throws `TransientError`, and the write may or may not have landed.** Re-run the
102
- call, or check `store.generations(ref)`. The driver sends each registry write once; a load that gets no answer reads
103
- the row and sends a fresh compare-and-swap from it, at most three times. A generation's object of up to
104
- `simpleUploadThresholdBytes` is sent again, up to three more times, only after a `429` or `503`; every object carries
105
- a random id in its metadata, so a first send that landed is told from another writer's object. A load whose row writes
106
- all go unanswered throws `TransientError` and keeps its object. The waits before an object's re-sends use real
107
- timers: the store's injected clock does not reach them, and `GcsStorage` takes no clock, so a test that throttles
108
- one object write three times waits about 3.5 s at most (the publish's waits before a fresh row write do use the
109
- store's clock).
119
+ - **A transient failure of a write throws `TransientError`, and the write may or may not have landed.** Re-run the call,
120
+ or check `store.generations(ref)`. The driver [sends each registry write
121
+ once](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/correctness.md#sdk-retry); a load that gets no
122
+ answer [reads the row and sends a fresh compare-and-swap from
123
+ it](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/correctness.md#row-write-settled), at most three
124
+ times. A generation's object of up to `simpleUploadThresholdBytes` is sent again, up to three more times, only after a
125
+ `429` or `503`; every object carries a random id in its metadata, so a first send that landed is told from another
126
+ writer's object. A load whose row writes all go unanswered throws `TransientError` and keeps its object. The waits
127
+ before an object's re-sends use real timers: the store's injected clock does not reach them, and `GcsStorage` takes no
128
+ clock, so a test that throttles one object write three times waits about 3.5 s at most (the publish's waits before a
129
+ fresh row write do use the store's clock).
110
130
 
111
131
  The [production checklist](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/production.md) covers each item, and the ones every backend shares: backups of the data and the registry, and a schedule for your loads.
132
+ - **The driver redacts the credentials it recognises from an error before it throws it.** The SDK keeps the response it
133
+ got on the error (`response`), and through it the request it sent, with the bearer token in its `Authorization`
134
+ header, and the HTTP agent it shares with other calls; a resumable upload's or a token request's error keeps the
135
+ request's `config`. On a download, and wherever a body lists no errors, the SDK makes the response body the error's
136
+ message, so a proxy's page or a JSON body that quotes the request puts the token in the message. In the error the
137
+ driver throws, the value under a credential's name is replaced by `[redacted]` (`authorization`,
138
+ `x-goog-encryption-key`, `x-goog-api-key`, `X-Goog-Signature`, `X-Goog-Credential`, `access_token`, `refresh_token`,
139
+ `client_secret`, `private_key` and the like; a service account's `client_email` and `private_key_id` are kept), and so
140
+ are the value after one in a header list or under one in a map, the value beside one in a record (a `{ name, value }`
141
+ pair, a `{ th, td }` row; a number, a boolean, and the `message`, `reason` and `location` of a Google error item are
142
+ kept), and, anywhere in its text, a credential header line; an `Authorization` value, an encryption key, an API key,
143
+ an OAuth token or client secret, or a signed URL's signature or credential after its name; a cookie header's value
144
+ after its name; a bearer token after its scheme; a Google access token (`ya29.`) or a JWT on its own; a V2 signed
145
+ URL's `Signature=`; a PEM private key's body; the text of an element named for a credential, such as the
146
+ `<CanonicalRequest>` a service quotes back; and the text of the cell after one that names a credential (a
147
+ `<th>`/`<td>` row, a `<dt>`/`<dd>` pair, after a heading or a caption too). A credential in another form is not
148
+ recognised (a header table laid out by column, a user and password written into a URL, a colon written as an HTML
149
+ entity such as `&colon;` in a body a download quotes as it came), and a text these steps cannot finish is replaced
150
+ whole. An SDK error that carries its transport or shows a credential, and every error it reaches (its `cause` chain,
151
+ an `AggregateError`'s `errors`), is thrown as a copy: a native error with the same prototype, name, message, stack,
152
+ `code`, `status` and `errors`, with `response` reduced to its status and status text, so classification is unchanged,
153
+ without `config`, the request, its agent or socket, any function or accessor property, or anything nested deeper than
154
+ ten levels. The live error is never changed, and an error that holds only plain data and shows no credential is thrown
155
+ as it is.
112
156
 
113
157
  ## Documentation
114
158
 
package/dist/backend.d.ts CHANGED
@@ -1,20 +1,14 @@
1
- /**
2
- * `GcsStorage` — the Google Cloud Storage backend as one object: generations and pointer, one bucket, stated once.
3
- *
4
- * See {@link StorageBackend} for why the two halves are configured together rather than separately.
5
- *
6
- * **A note on the word.** `@google-cloud/storage` names its own client class `Storage`, so the driver option
7
- * that takes it is also called `storage` — which, since this library calls its durable tier storage too, reads
8
- * as the same word meaning two things. That is why this class builds the client itself: `new GcsStorage({ bucket })`
9
- * never makes you write `{ storage: storage }`. Supply `client` when you need your own.
10
- */
11
1
  import { STORAGE_BACKEND } from '@cloudbitmaps/core/driver-kit';
12
- import type { IRegistryDriver, IStorageDriver, StorageBackend } from '@cloudbitmaps/core/driver-kit';
2
+ import type { IRegistryDriver, IStorageDriver, LifecycleVerdict, StorageBackend } from '@cloudbitmaps/core/driver-kit';
13
3
  import { Storage as GcsClient } from '@google-cloud/storage';
4
+ /**
5
+ * What {@link GcsStorage} takes: a `bucket`, and either a `client` or the two settings that build one (`projectId`,
6
+ * `apiEndpoint`), never both.
7
+ */
14
8
  export interface GcsStorageOptions {
15
9
  /** Target bucket (must already exist). */
16
10
  readonly bucket: string;
17
- /** Optional object-name prefix under which everything lives — generations and the registry alike. */
11
+ /** Optional object-name prefix under which everything lives — generations and the registry alike. A string, else `ValidationError`. */
18
12
  readonly prefix?: string;
19
13
  /**
20
14
  * A constructed `@google-cloud/storage` client. One is built from the ambient credentials when absent. Its retry
@@ -31,9 +25,15 @@ export interface GcsStorageOptions {
31
25
  * built, other than an interceptor, is not carried to the twin.
32
26
  */
33
27
  readonly client?: GcsClient;
34
- /** Project id for the client built when `client` is absent (refused beside `client`). Falls back to the SDK's own resolution. */
28
+ /**
29
+ * Project id for the client built when `client` is absent (refused beside `client`): a non-empty string, else
30
+ * `ValidationError`. Falls back to the SDK's own resolution.
31
+ */
35
32
  readonly projectId?: string;
36
- /** Endpoint override — point it at fake-gcs-server locally. Refused beside `client`, which carries its own. */
33
+ /**
34
+ * Endpoint override — point it at fake-gcs-server locally: a non-empty string, since an empty one would send
35
+ * requests to the public endpoint, else `ValidationError`. Refused beside `client`, which carries its own.
36
+ */
37
37
  readonly apiEndpoint?: string;
38
38
  /** Largest object the backend will write and advertise (default = GCS's 5 TiB max). Must be a positive safe integer. */
39
39
  readonly maxObjectBytes?: number;
@@ -77,6 +77,16 @@ export interface GcsStorageOptions {
77
77
  * for a client pointed at an emulator that is production traffic from a wiring typo.
78
78
  */
79
79
  export declare const GCS_STORAGE_OPTION_KEYS: readonly ["bucket", "prefix", "client", "projectId", "apiEndpoint", "maxObjectBytes", "simpleUploadThresholdBytes", "now", "readTimeoutMs", "conditionalDelete"];
80
+ /**
81
+ * `GcsStorage` — the Google Cloud Storage backend as one object: generations and pointer, one bucket, stated once.
82
+ *
83
+ * See {@link StorageBackend} for why the two halves are configured together rather than separately.
84
+ *
85
+ * **A note on the word.** `@google-cloud/storage` names its own client class `Storage`, so the driver option
86
+ * that takes it is also called `storage` — which, since this library calls its durable tier storage too, reads
87
+ * as the same word meaning two things. That is why this class builds the client itself: `new GcsStorage({ bucket })`
88
+ * never makes you write `{ storage: storage }`. Supply `client` when you need your own.
89
+ */
80
90
  export declare class GcsStorage implements StorageBackend {
81
91
  /** Cross-bundle brand, stamped non-enumerably in the constructor so a spread cannot carry it. */
82
92
  readonly [STORAGE_BACKEND]: true;
@@ -84,5 +94,25 @@ export declare class GcsStorage implements StorageBackend {
84
94
  readonly registry: IRegistryDriver;
85
95
  /** The client both halves share — built here unless one was supplied. */
86
96
  readonly client: GcsClient;
97
+ private readonly bucket;
98
+ /** The name prefix every object sits under: `''`, or `trimmed/`. */
99
+ private readonly keyPrefix;
100
+ private readonly readTimeoutMs;
101
+ /**
102
+ * Builds the GCS backend over one bucket, which `bucket` names. Pass `client`, or `projectId` and `apiEndpoint` to
103
+ * have one built, not both. `prefix` puts every object under a name prefix. Throws `ValidationError` for an unknown
104
+ * option, no bucket, a `now` that is not a function, a client setting beside `client`, a `maxObjectBytes` or
105
+ * `simpleUploadThresholdBytes` that is not a positive integer, a `conditionalDelete` that is not a boolean, an
106
+ * empty or non-string `apiEndpoint` or `projectId`, or an invalid `prefix` or `readTimeoutMs`.
107
+ */
87
108
  constructor(options: GcsStorageOptions);
109
+ /**
110
+ * Reads the bucket's metadata (one request, plus the SDK's own retries, bounded by `readTimeoutMs` when it is set) and
111
+ * reports a lifecycle rule with a `Delete` action (or one it does not know) that applies to live
112
+ * objects whose names can be this store's: generations and registry rows alike. Needs `storage.buckets.get`, which
113
+ * a load never uses (`store.preflight()`'s lifecycle step does) and `roles/storage.objectAdmin` does not include: without it the answer is
114
+ * `unchecked`, naming the permission. Never rejects; a `detail` carries no credential. Rule reading: see the
115
+ * production guide.
116
+ */
117
+ checkLifecycle(): Promise<LifecycleVerdict>;
88
118
  }