@cloudbitmaps/roaring 0.9.0 → 0.10.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/NOTICE +3 -3
- package/PRIVACY.md +198 -72
- package/README.md +121 -57
- package/dist/bin/export-segments.d.ts +21 -0
- package/dist/bin/export-segments.js +1508 -30
- package/dist/bin/export-segments.js.map +7 -1
- package/dist/codec-bound.d.ts +38 -0
- package/dist/index.d.ts +805 -447
- package/dist/index.js +911 -325
- package/dist/index.js.map +7 -1
- package/dist/moved-options.d.ts +14 -0
- package/dist/portable/decode.d.ts +54 -0
- package/dist/roaring-codec.d.ts +61 -0
- package/dist/system-clock.d.ts +40 -0
- package/dist/testing/conformance.d.ts +34 -0
- package/dist/testing/fuzz-codec.d.ts +9 -0
- package/package.json +11 -116
- package/dist/azure/index.cjs +0 -14
- package/dist/azure/index.cjs.map +0 -1
- package/dist/azure/index.d.cts +0 -9
- package/dist/azure/index.d.ts +0 -9
- package/dist/azure/index.js +0 -3
- package/dist/azure/index.js.map +0 -1
- package/dist/bin/compact-segments.js +0 -201
- package/dist/bin/compact-segments.js.map +0 -1
- package/dist/cassandra/index.cjs +0 -14
- package/dist/cassandra/index.cjs.map +0 -1
- package/dist/cassandra/index.d.cts +0 -1
- package/dist/cassandra/index.d.ts +0 -1
- package/dist/cassandra/index.js +0 -3
- package/dist/cassandra/index.js.map +0 -1
- package/dist/chunk-D6LIJSGF.js +0 -906
- package/dist/chunk-D6LIJSGF.js.map +0 -1
- package/dist/dynamodb/index.cjs +0 -14
- package/dist/dynamodb/index.cjs.map +0 -1
- package/dist/dynamodb/index.d.cts +0 -1
- package/dist/dynamodb/index.d.ts +0 -1
- package/dist/dynamodb/index.js +0 -3
- package/dist/dynamodb/index.js.map +0 -1
- package/dist/gcs/index.cjs +0 -14
- package/dist/gcs/index.cjs.map +0 -1
- package/dist/gcs/index.d.cts +0 -1
- package/dist/gcs/index.d.ts +0 -1
- package/dist/gcs/index.js +0 -3
- package/dist/gcs/index.js.map +0 -1
- package/dist/index.cjs +0 -894
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -734
- package/dist/mongodb/index.cjs +0 -14
- package/dist/mongodb/index.cjs.map +0 -1
- package/dist/mongodb/index.d.cts +0 -1
- package/dist/mongodb/index.d.ts +0 -1
- package/dist/mongodb/index.js +0 -3
- package/dist/mongodb/index.js.map +0 -1
- package/dist/mysql/index.cjs +0 -14
- package/dist/mysql/index.cjs.map +0 -1
- package/dist/mysql/index.d.cts +0 -1
- package/dist/mysql/index.d.ts +0 -1
- package/dist/mysql/index.js +0 -3
- package/dist/mysql/index.js.map +0 -1
- package/dist/postgres/index.cjs +0 -14
- package/dist/postgres/index.cjs.map +0 -1
- package/dist/postgres/index.d.cts +0 -1
- package/dist/postgres/index.d.ts +0 -1
- package/dist/postgres/index.js +0 -3
- package/dist/postgres/index.js.map +0 -1
- package/dist/redis/index.cjs +0 -14
- package/dist/redis/index.cjs.map +0 -1
- package/dist/redis/index.d.cts +0 -1
- package/dist/redis/index.d.ts +0 -1
- package/dist/redis/index.js +0 -3
- package/dist/redis/index.js.map +0 -1
- package/dist/s3/index.cjs +0 -14
- package/dist/s3/index.cjs.map +0 -1
- package/dist/s3/index.d.cts +0 -1
- package/dist/s3/index.d.ts +0 -1
- package/dist/s3/index.js +0 -3
- package/dist/s3/index.js.map +0 -1
package/NOTICE
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
@cloudbitmaps/roaring
|
|
2
2
|
Copyright 2026 Sharvil Kadam
|
|
3
3
|
|
|
4
4
|
This product includes software developed as an open-source project.
|
|
@@ -7,6 +7,6 @@ file. You may obtain a copy of the License at:
|
|
|
7
7
|
|
|
8
8
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
@cloudbitmaps/roaring builds on the Roaring Bitmap data structure and uses the CRoaring
|
|
11
|
+
engine (via the `roaring` Node.js binding) for in-memory bit math.
|
|
12
12
|
Those projects are independent works under their own licenses.
|
package/PRIVACY.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Privacy, data protection & shared responsibility
|
|
2
2
|
|
|
3
|
+
> This is the copy published with the npm package; it is kept identical in substance to
|
|
4
|
+
> [`PRIVACY.md`](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/PRIVACY.md) in the repository.
|
|
5
|
+
|
|
6
|
+
|
|
3
7
|
> **Engineering guidance, not legal advice.** This document explains CloudBitmaps' data-handling posture and
|
|
4
8
|
> the controls it gives you, so you (and your privacy counsel / DPO) can place it correctly in your compliance
|
|
5
9
|
> program. Regulatory references (GDPR, CCPA/CPRA, HIPAA, …) are illustrative. Nothing here is a compliance
|
|
@@ -8,7 +12,7 @@
|
|
|
8
12
|
## TL;DR — the trust boundary
|
|
9
13
|
|
|
10
14
|
**CloudBitmaps is an embedded library.** It runs inside *your* process/function and talks to *your* storage
|
|
11
|
-
accounts (S3,
|
|
15
|
+
accounts (S3, GCS, Azure Blob, local disk — whatever drivers you wire). **It has no hosted backend and transmits no
|
|
12
16
|
data to the authors or any third party** — there is no telemetry, no phone-home, no usage ping. The metrics
|
|
13
17
|
and audit sinks are local and off by default; they only go where *you* send them.
|
|
14
18
|
|
|
@@ -38,105 +42,227 @@ from constructing a cross-region topology. The points where personal data moves
|
|
|
38
42
|
|
|
39
43
|
| Location | What's there | Residency note |
|
|
40
44
|
|---|---|---|
|
|
41
|
-
| **
|
|
42
|
-
| **
|
|
43
|
-
| **
|
|
44
|
-
| **
|
|
45
|
+
| **Storage** (object store) | immutable `.crbm` generations — every generation a segment has had, until a superseded one is collected | the region of the bucket you wire |
|
|
46
|
+
| **Registry** (S3 / GCS / Azure Blob / local) | one row per segment: the current-generation pointer, wrapped keys, retention metadata — no IDs | the region of the bucket you wire |
|
|
47
|
+
| **cache** (process RAM) | decoded chunks, bounded LRU | **wherever your process/Lambda runs** — an EU segment queried from a US function is processed in the US |
|
|
48
|
+
| **Loads and rewrites** (`bulkLoadCrbmGeneration`, the `*Into` verbs, `eraseSubject`) | read your source (or existing generations), write a new generation | run wherever you run them — a loader in one region writing to a bucket in another is a transfer |
|
|
45
49
|
| **Intersection** | pulls chunks from N segments into one process | co-locates those segments in one region |
|
|
46
50
|
|
|
47
51
|
**Guidance (not enforced by the library):** to keep EU data in EU infrastructure, wire region-local drivers
|
|
48
|
-
*and* run
|
|
49
|
-
jurisdiction; treat the
|
|
52
|
+
*and* run your loaders and erasure jobs in-region; keep a segment's storage objects, its registry row, and the querying compute in
|
|
53
|
+
one jurisdiction; treat the cache and the intersection runtime as **processing locations** in your transfer
|
|
50
54
|
assessment and breach scope (process RAM, and any heap/core dumps, hold personal data). A fail-closed
|
|
51
55
|
residency-enforcement policy in the library was considered and deferred as over-engineering for v1 — the honest
|
|
52
56
|
posture is "you wire it correctly," documented here.
|
|
53
57
|
|
|
54
58
|
## Erasure — what "delete" actually means
|
|
55
59
|
|
|
56
|
-
CloudBitmaps gives you three
|
|
60
|
+
CloudBitmaps gives you three levers with different guarantees. Use them deliberately:
|
|
57
61
|
|
|
58
62
|
| Lever | API | Guarantee | Use for |
|
|
59
63
|
|---|---|---|---|
|
|
60
|
-
| **
|
|
61
|
-
| **
|
|
62
|
-
| **Crypto-shred** | `destroySegment` / `eraseNamespace` | *Instant + total* — destroys the segment's wrapped key, so **every** copy (current, prior generations, backups, WORM-locked objects) becomes unreadable without touching the bytes. Requires the segment to be encrypted. | whole-segment / tenant offboarding; erasure under immutable backups (see below) |
|
|
64
|
+
| **Subject erasure** | `store.eraseSubject(id, { namespace })` (or `eraseIdFromSegment(ref, id, deps)` for one segment) | *Physical on return* — the segment's current generation is rewritten without the id (every chunk streamed through, one bit cleared), published **fenced on the generation it streamed**, and **the generation that held the bit is deleted before the call returns**. A retained *superseded* generation still holding the id (an ex-member dropped by a re-seed) is found and collected too. A per-segment fault is reported as an `error: …` ledger entry rather than thrown, so "on return" is a claim about every segment whose entry is **not** `error: …`. The store that performs the call stops serving the id immediately; **other processes converge on their own read TTL** — see [One process, and the rest of your fleet](#one-process-and-the-rest-of-your-fleet). Does **not** reach backups, replicas or noncurrent object versions — those hold the old generation until their own lifecycle removes it. | "forget this person" — GDPR Art. 17 |
|
|
65
|
+
| **Dispose** | `store.dropSegment(ref, { confirmSegment })` | *Immediate* — the segment is tombstoned and its Storage generations deleted, reclaiming the space. **Check `generationsRemaining`:** if it is non-empty the space was *not* fully reclaimed and the drop should be re-run (a load that was already writing when the tombstone landed still finishes its object). Works on cleartext; on an encrypted segment it *also* discards the key. Does **not** reach noncurrent versions / replicas / PITR snapshots — deleting an object is weaker than destroying a key. | retiring a dated bucket; rolling-window retention |
|
|
66
|
+
| **Crypto-shred** | `destroySegment` / `eraseNamespace` | *Instant + total at rest* — destroys the segment's wrapped key, so **every** copy (current, prior generations, backups, WORM-locked objects) becomes unreadable without touching the bytes. Requires the segment to be encrypted. A process that had already opened the segment holds the **unwrapped** key in memory and keeps reading until it is told — call `store.invalidate(ref)` there; see [One process, and the rest of your fleet](#one-process-and-the-rest-of-your-fleet). | whole-segment / tenant offboarding; erasure under immutable backups (see below) |
|
|
63
67
|
|
|
64
68
|
**Subject-wide erasure** (GDPR Art. 17 — "forget this person everywhere") is
|
|
65
|
-
`store.eraseSubject(id, {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
`
|
|
73
|
-
|
|
69
|
+
`store.eraseSubject(id, { namespace })` — or `{ allNamespaces: true }` to sweep every tenant deliberately. For
|
|
70
|
+
every **registered** segment the id is in, it rewrites the current generation without the id, publishes the
|
|
71
|
+
rewrite, and deletes the generation that held the bit — so the bit is physically gone from the bucket on
|
|
72
|
+
return, for idle and archival segments as much as busy ones. There is no logical-then-physical gap and no
|
|
73
|
+
scheduled step to wait for: an erasure *is* a new generation, the same shape as every other write in the library.
|
|
74
|
+
It reuses the store's own drivers (so build the store with a backend). It returns an
|
|
75
|
+
**erasure ledger** — one entry per segment the id was found in, `{ segment, namespace?, erased, fromGeneration,
|
|
76
|
+
generation, note? }` — as your proof of deletion; persist it or route it to your audit sink, which also receives
|
|
77
|
+
one `segment.rewrite { fromGeneration, generation }` event per rewrite when you pass `audit` — an id erased out
|
|
78
|
+
of a retained *superseded* generation is collected rather than rewritten, so it emits no event and its ledger
|
|
79
|
+
entry carries no `generation`. Segments the id is
|
|
80
|
+
not in are not listed. `store.subjectReport(id, { namespace })` answers the read side (Art. 15 — which segments
|
|
81
|
+
an id is in).
|
|
82
|
+
|
|
83
|
+
Two rules. **Do not load a segment while erasing from it**: a load that lands after the rewrite carries whatever
|
|
84
|
+
its source held, and the library cannot know that source was meant to exclude the id — fix the source first, or
|
|
85
|
+
quiesce loads of the affected segments for the duration. A writer that lands *during* the rewrite is caught and
|
|
86
|
+
the entry says `erased: false, note: 'superseded'` — either because the publish was refused **by that fence**, or
|
|
87
|
+
because a racing **erasure** (which collects with `keep: 0`) deleted the generation the rewrite was still
|
|
88
|
+
streaming. **Read the ledger**: an `erased: false` entry means *this run* did not erase the id from that segment,
|
|
89
|
+
which is not the same as the id still being present — if the racing writer was another erasure of the same id, it
|
|
90
|
+
is already gone. For `'superseded'` re-run `eraseSubject`: it is idempotent, it erases the id if the id is still
|
|
91
|
+
there, and a segment the id is no longer in is simply not listed — though "not listed" alone does not prove the
|
|
92
|
+
id is gone, since a segment whose **registry row** has been purged is not scanned either and its objects outlive
|
|
93
|
+
it as orphans (`store.checkConsistency()` finds those). Because a settled segment drops out of the
|
|
94
|
+
ledger entirely, **the attestation for a subject is the ledger of the run that reported `erased: true`** — if you
|
|
95
|
+
must hold one artifact per request, re-run until no entry carries a `'superseded'` note, and keep that run's
|
|
96
|
+
ledger alongside any earlier one. An `error: …` note is an isolated per-segment fault; if it occurred *after* the
|
|
97
|
+
rewrite was published — a Storage `delete` fault, or a collection pass that could not prove the segment was still
|
|
98
|
+
the same one — then the pointer has already moved, and the re-run searches the *superseded* generations as well
|
|
99
|
+
as the current one. (The same refusal can come from the collect-only path, where the bit was found in a
|
|
100
|
+
superseded generation and nothing was published; there the pointer has not moved, and a re-run simply repeats
|
|
101
|
+
the attempt.) **Read what it says.** Usually it reports `erased: true` against the generation it found the
|
|
102
|
+
id in, which is the receipt the failed call could not give you. If a racing collector took that generation
|
|
103
|
+
first it reports nothing for the segment: the bit is gone, but no run holds a receipt for it, so keep the failed
|
|
104
|
+
call's error alongside your ledger. And if the segment's registry row has since been purged, it is not a segment
|
|
105
|
+
any more and `eraseSubject` does not scan it at all — anything left in its bucket is an **orphan**, found by
|
|
106
|
+
`store.checkConsistency()` and collected by `gcOrphanGenerations`. An empty ledger is not by itself proof the id
|
|
107
|
+
is gone. That note is the signal that a *published* rewrite's physical half did not complete **on that call**,
|
|
108
|
+
which is why it is reported rather than swallowed.
|
|
109
|
+
|
|
110
|
+
### One process, and the rest of your fleet
|
|
111
|
+
|
|
112
|
+
Erasure and crypto-shred are **immediate in storage and immediate in the process that performed them**. They are
|
|
113
|
+
not immediate in *other* processes, and this library ships nothing that could make them so — there is no daemon,
|
|
114
|
+
no bus, and no connection between two stores that happen to point at the same bucket.
|
|
115
|
+
|
|
116
|
+
| | when the id stops being readable |
|
|
117
|
+
|---|---|
|
|
118
|
+
| storage | on return — the generation holding it is deleted, the DEK is destroyed |
|
|
119
|
+
| the store that performed the call | on return — it invalidates what it cached |
|
|
120
|
+
| another store, with a clock and a registry | within `cache.genTtlMs` (default 2 s), when its snapshot re-resolves |
|
|
121
|
+
| another store with **no clock**, or `cache: { genTtlMs: 0 }` | **never**, until something tells it |
|
|
122
|
+
|
|
123
|
+
That last row is the one to design around. `cache: { genTtlMs: 0 }` means "pin forever" and is a legitimate setting for
|
|
124
|
+
a read-only replica of immutable data — but a segment pinned that way does not observe a shred at all. If a
|
|
125
|
+
compliance deadline depends on every reader converging, fan the reference out to your fleet and have each
|
|
126
|
+
process call `store.invalidate(ref)`; that is the hook, and delivering it is yours because the transport is
|
|
127
|
+
yours.
|
|
128
|
+
|
|
129
|
+
A read served from a stale cache is bounded by the same window and answers from memory, so nothing on the
|
|
130
|
+
storage side — a bucket policy, a lifecycle rule, the object's own deletion — can shorten it.
|
|
74
131
|
|
|
75
132
|
**Your exit path** (and a building block for a **data-portability / Art. 20** response): `store.exportSegments(sink,
|
|
76
|
-
{ format })` (and the `export-segments` CLI) dumps every registered segment's
|
|
77
|
-
`roaring` (loadable by any roaring library) or `ndjson` (newline ids) — readable without CloudBitmaps.
|
|
78
|
-
**controller-side bulk dump** (all segments, opaque ids), not a per-subject deliverable — the per-subject
|
|
79
|
-
are `subjectReport` (Art. 15) / `eraseSubject` (Art. 17). Encrypted segments are decrypted transparently
|
|
80
|
-
store has the keystore, so the **export is cleartext — protect it** (the CLI writes owner-only files;
|
|
81
|
-
the dump at rest, restrict access, and delete it when done).
|
|
82
|
-
|
|
83
|
-
**Honest limits.**
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
**
|
|
90
|
-
|
|
133
|
+
{ format })` (and the `export-segments` CLI) dumps every registered segment's current generation to a portable
|
|
134
|
+
file — `roaring` (loadable by any roaring library) or `ndjson` (newline ids) — readable without CloudBitmaps.
|
|
135
|
+
It's a **controller-side bulk dump** (all segments, opaque ids), not a per-subject deliverable — the per-subject
|
|
136
|
+
rights are `subjectReport` (Art. 15) / `eraseSubject` (Art. 17). Encrypted segments are decrypted transparently
|
|
137
|
+
if the store has the keystore, so the **export is cleartext — protect it** (the CLI writes owner-only files;
|
|
138
|
+
also encrypt the dump at rest, restrict access, and delete it when done).
|
|
139
|
+
|
|
140
|
+
**Honest limits.** Subject erasure deletes the object that held the bit; it does not reach the copies of that
|
|
141
|
+
object your storage keeps on its own — noncurrent versions (S3 versioning), cross-region replicas, backups and
|
|
142
|
+
WORM-locked copies hold the old generation until their own lifecycle expires it. **Per-subject crypto-shred is
|
|
143
|
+
infeasible** (one DEK covers the whole segment, and a subject's bit is co-mingled with millions of others), so
|
|
144
|
+
single-subject erasure is a rewrite (`eraseSubject`), while crypto-shred (`destroySegment` / `eraseNamespace`)
|
|
145
|
+
handles segment/tenant-level erasure and is the only erasure that survives immutable backups / WORM. And a
|
|
146
|
+
**materialised segment** (`intersectInto` / `unionInto` / `andNotInto`) is a point-in-time snapshot of its
|
|
147
|
+
inputs: erasing a subject from a source does not touch a destination computed earlier — which is exactly why
|
|
148
|
+
`eraseSubject` scans *every* registered segment, destinations included, rather than erasing per source.
|
|
149
|
+
|
|
150
|
+
**Erasure vs. backups / WORM (the trap).** If you enable S3 versioning, Object Lock, or registry PITR for
|
|
151
|
+
durability, a subject-erasure rewrite *does not* reach the retained copies — the deleted bit survives in
|
|
91
152
|
noncurrent versions, locked objects, and backups. **Crypto-shred is the only erasure that survives all of
|
|
92
153
|
them**, because it destroys the key, not the bytes. So: use **per-segment/tenant encryption** as your erasure
|
|
93
|
-
posture under immutable
|
|
94
|
-
|
|
95
|
-
|
|
154
|
+
posture under immutable objects; give noncurrent versions a short expiry (a lifecycle rule on *noncurrent*
|
|
155
|
+
versions is fine — it is *current* generations that a rule must never expire, see below); reserve S3 Object
|
|
156
|
+
Lock **COMPLIANCE** mode for data under a genuine legal hold (it *cannot* be deleted before its retention date,
|
|
157
|
+
by anyone — incompatible with on-demand erasure), and prefer **GOVERNANCE** mode where erasure must remain
|
|
158
|
+
possible.
|
|
96
159
|
|
|
97
160
|
## Retention & data minimization
|
|
98
161
|
|
|
99
|
-
The library
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
162
|
+
The library will age a **segment** out for you once you tell it when to — but it will not schedule that for you,
|
|
163
|
+
and it will never age out an individual **id** (a bitmap stores ids, not timestamps). "Cheap to keep forever" is a
|
|
164
|
+
storage-limitation anti-pattern if the data is personal, so: **the policy is yours to set, the heartbeat is yours
|
|
165
|
+
to run, and the deletion is ours to perform correctly.** Practical patterns:
|
|
166
|
+
|
|
167
|
+
- **A per-segment expiry** — `store.setRetention(ref, { expiresAt })` records when a segment becomes eligible for
|
|
168
|
+
retirement, and `store.retireExpired()` retires every expired segment (through `dropSegment`, so the ordering
|
|
169
|
+
and the storage reclamation below are inherited rather than reimplemented). The expiry is an absolute instant
|
|
170
|
+
the writer sets; the sweep is a call **you** schedule (EventBridge, a CronJob, cron, a queue job), because this
|
|
171
|
+
library starts no background timer. Bounded per cycle, previewable with `dryRun`, and it returns a per-segment
|
|
172
|
+
ledger you should inspect — an entry can say a segment's storage was not fully reclaimed.
|
|
173
|
+
- **Rolling windows** (e.g. "active this week"): keep N daily sub-segments, union them for reads, and let the
|
|
174
|
+
policy above retire the oldest wholesale — turning retention into cheap whole-segment disposal, not per-bit aging.
|
|
175
|
+
- **A reload is the per-id retention.** Since a segment's content is whatever its last load said, ageing members
|
|
176
|
+
out *within* a segment is a matter of loading the next generation from a source that no longer includes them.
|
|
177
|
+
The registry row's `retention` is untouched by a load, an `*Into` materialisation or an erasure rewrite, so the
|
|
178
|
+
`expiresAt` you set stays put across all of them.
|
|
106
179
|
- Surface segment age/size via the **metrics sink** so unbounded growth is visible, not silent.
|
|
107
180
|
|
|
181
|
+
**Be precise about what "drop the oldest" involves**, because the three levers differ in what they guarantee:
|
|
182
|
+
|
|
183
|
+
- **`store.dropSegment(ref, { confirmSegment })` is the retention primitive**, and `setRetention` +
|
|
184
|
+
`retireExpired` is the policy-driven form that calls it — and its result must be inspected, not
|
|
185
|
+
assumed: `generationsRemaining` non-empty means bytes survived and the call should be repeated. It tombstones
|
|
186
|
+
the segment and deletes **every Storage generation** — so the space is actually reclaimed. It works on a
|
|
187
|
+
cleartext segment, and on an encrypted one it *also* discards the key, making it a strict superset there.
|
|
188
|
+
- **`destroySegment` crypto-shreds** — it discards the key, so the Storage bytes become unreadable *everywhere
|
|
189
|
+
including backups, replicas and WORM-locked copies*, which no object deletion can achieve. But **the objects
|
|
190
|
+
remain in your bucket** and you keep paying for them, and it **requires encryption at rest** (a cleartext
|
|
191
|
+
segment has no key to discard; `allowCleartext` writes the tombstone while leaving the Storage bytes readable — and
|
|
192
|
+
still in the bucket).
|
|
193
|
+
- **`gcOrphanGenerations`** deletes only *superseded* generations, never the current one — with `keep: 0` it is
|
|
194
|
+
how a subject erasure removes the generation that held the bit.
|
|
195
|
+
|
|
196
|
+
So the two are complements, not alternatives: **`dropSegment` for "stop paying for it", `destroySegment` for
|
|
197
|
+
"it must be unreadable even in backups"** — and on an encrypted segment `dropSegment` gives you both at once.
|
|
198
|
+
|
|
199
|
+
> ⚠️ **Do not use an object-store lifecycle rule as your retention mechanism.** It deletes the bytes while the
|
|
200
|
+
> registry still points at them, which is the `missing-storage-generation` torn state — and it surfaces
|
|
201
|
+
> *intermittently*, because a read consults the in-process cache before Storage, so it passes testing on a warm
|
|
202
|
+
> process and starts failing after a restart. A lifecycle rule is a fine **backstop** for orphans left by a
|
|
203
|
+
> failed `dropSegment`, and a fine way to expire *noncurrent* object versions; give it a window comfortably
|
|
204
|
+
> longer than your retention and never let it touch a current generation. Earlier revisions of this document
|
|
205
|
+
> recommended it as the primary mechanism, which was wrong.
|
|
206
|
+
|
|
207
|
+
> ⚠️ **Never apply a lifecycle-expiration rule to the registry prefix either** — a separate trap, and a worse
|
|
208
|
+
> one. Deleting a registry row does not remove it: it writes a **tombstone** carrying a counter that only ever
|
|
209
|
+
> advances, which is what makes the row's token unique for all time. Expire those tombstones and a segment
|
|
210
|
+
> re-created under the same name starts the counter again and re-issues a token that was already used. That
|
|
211
|
+
> token is the segment's **identity**: it is what a cached reader, a fenced publish and a generation-collection
|
|
212
|
+
> pass each compare to decide whether two observations describe the same segment. Re-issue one and they can all
|
|
213
|
+
> answer "yes" about a segment that no longer exists — serving a deleted incarnation's data, or collecting a
|
|
214
|
+
> live one's objects. Storage for a tombstone is a few bytes per retired segment; treat it as permanent.
|
|
215
|
+
|
|
216
|
+
Full detail, including the dated-bucket pattern and the pitfalls, is in the retention section of the
|
|
217
|
+
[guide](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/getting-started.md).
|
|
218
|
+
|
|
108
219
|
## Legal hold
|
|
109
220
|
|
|
110
221
|
A litigation/regulatory hold *forbids* deletion — the opposite of erasure — and can apply to the same segment.
|
|
111
222
|
**Enforce a hold with S3 Object Lock**, which is the real, tamper-proof mechanism: in **COMPLIANCE** mode a
|
|
112
|
-
locked
|
|
223
|
+
locked Storage object *cannot* be deleted before its retention date by anyone (not even the account root); in
|
|
113
224
|
**GOVERNANCE** mode a privileged role can override. To place a hold on a segment:
|
|
114
225
|
|
|
115
|
-
1. Enable **Object Lock** on the segment's
|
|
116
|
-
2. **Exclude the segment from your
|
|
117
|
-
or
|
|
226
|
+
1. Enable **Object Lock** on the segment's Storage `.crbm` objects (and enable versioning) for the hold period.
|
|
227
|
+
2. **Exclude the segment from your erasure runs and your loads** — don't call `eraseSubject` in a scope that
|
|
228
|
+
reaches it, `destroySegment`, **`dropSegment`**, or load a new generation onto a held segment, so the current
|
|
229
|
+
generation (and its members) is preserved. Because `eraseSubject` scans a whole namespace, the practical
|
|
230
|
+
shape is to keep held segments in a namespace of their own. **`dropSegment` matters most here**: it is the only
|
|
231
|
+
call that deletes the bytes a hold exists to protect. In COMPLIANCE mode Object Lock will refuse the delete,
|
|
232
|
+
so the drop reports the generation as not deleted rather than silently succeeding — but the tombstone is
|
|
233
|
+
still written, so the segment reads as empty while the locked bytes remain. **Do not rely on Object Lock to be
|
|
234
|
+
the guard**, and note how exclusion works today: `retireExpired` has **no exclusion predicate**, so a held
|
|
235
|
+
segment must either *never carry a retention policy*, or have it removed with `clearRetention` for the
|
|
236
|
+
duration of the hold and re-set afterwards. Recording the hold itself is free — any key other than
|
|
237
|
+
`expiresAt` in the row's `retention` metadata is yours and survives both `setRetention` and
|
|
238
|
+
`clearRetention`, so `{ legalHold: 'case-1234' }` is a durable marker; it is simply not yet something the
|
|
239
|
+
sweep reads. (An `exclude` predicate is under consideration; say so on an issue if you need it.)
|
|
118
240
|
3. Decide hold-vs-erasure precedence when both apply to the same subject — that is a **legal determination**;
|
|
119
241
|
under a hold, erasure is suspended.
|
|
120
242
|
|
|
121
243
|
CloudBitmaps deliberately does **not** ship a native `legalHold` flag: Object Lock is a stronger guarantee than
|
|
122
|
-
an in-library flag (which our own
|
|
123
|
-
advisory where Object Lock is enforced at rest. If a real deployment needs a library-managed hold that
|
|
124
|
-
|
|
125
|
-
exclusion, documented
|
|
244
|
+
an in-library flag (which our own erasure and sweep could respect but a direct caller could bypass), so a flag
|
|
245
|
+
would be advisory where Object Lock is enforced at rest. If a real deployment needs a library-managed hold that
|
|
246
|
+
`eraseSubject` and `retireExpired` refuse to touch, it's a clean fast-follow — but the enforced posture is
|
|
247
|
+
Object Lock + operational exclusion, documented in this section.
|
|
126
248
|
|
|
127
249
|
## Audit & accountability (GDPR Art. 30 / Art. 5(2))
|
|
128
250
|
|
|
129
251
|
Wire the **audit sink** (`IAuditSink`) to get an append-only, vendor-neutral record of the compliance-relevant
|
|
130
|
-
state changes — `segment.publish
|
|
131
|
-
`
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
252
|
+
state changes — `segment.publish` (a loaded generation became current), `segment.rewrite` (a subject-erasure
|
|
253
|
+
rewrite: `fromGeneration` → `generation`), `segment.erase` (a genuine crypto-shred), `segment.dispose` (a
|
|
254
|
+
`dropSegment`, including every retirement the sweep performs) and `namespace.erase` — for your audit log / SIEM.
|
|
255
|
+
It is off by default and exception-safe. See the [dashboards guide](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/dashboards.md), which also says
|
|
256
|
+
which event is the receipt for which claim. An erasure *ledger* — per-subject-request proof of physical deletion
|
|
257
|
+
— is returned by **`eraseSubject`**. `subjectReport` is the read side and returns only which segments an id is
|
|
258
|
+
in; it performs no deletion and issues no ledger.
|
|
135
259
|
|
|
136
260
|
**Log hygiene:** `segment` / `namespace` are *your* strings and may encode sensitive purpose; IDs are personal
|
|
137
261
|
data. The library never logs bitmap contents or raw IDs, but **you** should treat segment names and IDs as PII
|
|
138
262
|
in your own logs, error reporting, and metric/trace tags — hash or redact them, and prefer opaque/coded
|
|
139
|
-
segment names for sensitive segments (keeping the human label in your own classified registry).
|
|
263
|
+
segment names for sensitive segments (keeping the human label in your own classified registry). The name rules are deliberately permissive — any non-empty string — so a name can carry an email
|
|
264
|
+
address, free text, or anything else an upstream system hands you. That makes this advice stronger rather than
|
|
265
|
+
weaker: the library will store what you name, and will not sanitise it for you.
|
|
140
266
|
|
|
141
267
|
## Shared-responsibility matrix
|
|
142
268
|
|
|
@@ -144,12 +270,12 @@ segment names for sensitive segments (keeping the human label in your own classi
|
|
|
144
270
|
|---|---|---|
|
|
145
271
|
| **Controller/processor role** | an embedded library; sends nothing to us | be the controller/processor; run your own DPAs with *your* cloud providers |
|
|
146
272
|
| **Encryption at rest** | AES-256-GCM envelope encryption, BYOK keystore (`InProcessKeystore`), per-segment DEK + active/recovery KEK | hold and protect your keys (KMS/HSM); enable encryption for sensitive segments |
|
|
147
|
-
| **Erasure** | `
|
|
148
|
-
| **Residency** | region-agnostic drivers; you choose every location | wire region-correct drivers; run compute in-region; assess transfers |
|
|
273
|
+
| **Erasure** | `eraseSubject` (per-id rewrite, physical on return), `dropSegment` (dispose), `destroySegment`/`eraseNamespace` (crypto-shred) | read the erasure ledger and re-run on `erased: false`; don't load a segment while erasing from it; choose crypto-shred under WORM/backups; classify what needs erasing |
|
|
274
|
+
| **Residency** | region-agnostic drivers; you choose every location | wire region-correct drivers; run loaders and compute in-region; assess transfers |
|
|
149
275
|
| **Classification** | opaque handling; a place to keep sensitive segments encrypted + audited | classify your segments (the library can't infer sensitivity) |
|
|
150
|
-
| **Retention** |
|
|
151
|
-
| **Legal hold** | Object Lock guidance (the enforced mechanism; no in-library flag) | place holds via Object Lock;
|
|
152
|
-
| **Accountability** | `IAuditSink` (publish/
|
|
276
|
+
| **Retention** | per-segment `expiresAt` + `retireExpired` (bounded, previewable, ledgered); rolling-window pattern | set the policy per segment; **schedule the sweep yourself** and alarm on its ledger |
|
|
277
|
+
| **Legal hold** | Object Lock guidance (the enforced mechanism; no in-library flag, and no sweep-level exclusion — see above) | place holds via Object Lock; keep a held segment out of the retention sweep by not giving it a policy, and out of `eraseSubject`'s scope; decide precedence |
|
|
278
|
+
| **Accountability** | `IAuditSink` (publish / rewrite / erase / dispose events) | route it to durable, append-only storage; keep your Art. 30 record |
|
|
153
279
|
| **Telemetry** | **none** — no phone-home | (nothing — there is nothing to disable) |
|
|
154
280
|
|
|
155
281
|
## DPIA skeleton (for deployments with sensitive segments)
|
|
@@ -159,13 +285,13 @@ A minimal Data-Protection-Impact-Assessment outline to adapt:
|
|
|
159
285
|
1. **Processing description** — which segments, what each membership *means*, source of the IDs, volume.
|
|
160
286
|
2. **Necessity & proportionality** — lawful basis per segment (esp. Art. 9 special-category); why membership
|
|
161
287
|
is retained and for how long.
|
|
162
|
-
3. **Data flow & residency** —
|
|
163
|
-
transfers and their safeguards.
|
|
288
|
+
3. **Data flow & residency** — Storage and registry regions, where compute (loaders, the cache, intersection)
|
|
289
|
+
runs, cross-border transfers and their safeguards.
|
|
164
290
|
4. **Risks** — re-identification, sensitive inference (incl. *derived* segments from intersections — treat a
|
|
165
291
|
`paying ∩ pregnant` result as at least as sensitive as its inputs, and note it's a point-in-time snapshot
|
|
166
292
|
that does not auto-honour later erasures of source members), breach scope (incl. process RAM / dumps).
|
|
167
|
-
5. **Controls** — encryption + crypto-shred, erasure
|
|
168
|
-
sink, access control on keys and storage, redaction of names/IDs in logs.
|
|
293
|
+
5. **Controls** — encryption + crypto-shred, subject erasure that is physical on return (`eraseSubject`),
|
|
294
|
+
retention windows, audit sink, access control on keys and storage, redaction of names/IDs in logs.
|
|
169
295
|
6. **Residual risk & sign-off** — DPO review.
|
|
170
296
|
|
|
171
297
|
## Art. 30 record — mapping template
|
|
@@ -174,14 +300,14 @@ Map CloudBitmaps' processing onto the categories a record of processing needs:
|
|
|
174
300
|
|
|
175
301
|
| Art. 30 field | CloudBitmaps mapping |
|
|
176
302
|
|---|---|
|
|
177
|
-
| Categories of processing | storage of set-membership; set intersection;
|
|
303
|
+
| Categories of processing | storage of set-membership; set intersection (incl. materialised results); loading of generations; caching |
|
|
178
304
|
| Categories of data subjects / data | your IDs' subjects; membership (possibly special-category) |
|
|
179
305
|
| Recipients | none external to your infrastructure (no sub-processor) |
|
|
180
306
|
| Transfers | any cross-region driver/compute topology *you* configure |
|
|
181
|
-
| Retention | your per-segment policy (enforced
|
|
307
|
+
| Retention | your per-segment policy — `setRetention(ref, { expiresAt })`, enforced by a `retireExpired` sweep **you schedule** |
|
|
182
308
|
| Security measures | AES-256-GCM at rest, crypto-shred erasure, audit sink, your access controls |
|
|
183
309
|
|
|
184
310
|
---
|
|
185
311
|
|
|
186
|
-
**See also:** the [getting-started guide](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/getting-started.md) (
|
|
187
|
-
|
|
312
|
+
**See also:** the [getting-started guide](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/getting-started.md) (its encryption/crypto-shred, metrics,
|
|
313
|
+
audit, and subject access & erasure sections) and the [dashboards guide](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/dashboards.md).
|