@doubling/types 0.2.1 → 0.4.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 +23 -2
- package/dist/crdt-store.d.ts +86 -0
- package/dist/crdt-store.d.ts.map +1 -0
- package/dist/crdt-store.js +140 -0
- package/dist/crdt-store.js.map +1 -0
- package/dist/crdt-testing.d.ts +34 -0
- package/dist/crdt-testing.d.ts.map +1 -0
- package/dist/crdt-testing.js +79 -0
- package/dist/crdt-testing.js.map +1 -0
- package/dist/firestore.d.ts +2 -0
- package/dist/firestore.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/scrub.d.ts +41 -0
- package/dist/scrub.d.ts.map +1 -0
- package/dist/scrub.js +206 -0
- package/dist/scrub.js.map +1 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @doubling/types
|
|
2
2
|
|
|
3
|
-
Shared TypeScript interfaces and pure helpers consumed by `web/` and `sync
|
|
3
|
+
Shared TypeScript interfaces and pure helpers consumed by `web/` and `sync/` (verified via each package's `package.json` dependency and `import … from '@doubling/types'` sites). Single source of truth for Firestore document shapes (file/folder entities, org membership, teams, user profiles), file-extension to MIME mappings, and storage-path helpers.
|
|
4
|
+
|
|
5
|
+
`functions/` does **not** depend on this package: it deliberately keeps a local mirror of `OrgMember` (see `functions/src/index.ts`, "kept local because functions/…") pending a later migration. Root [`../DESIGN.md` §5](../DESIGN.md) describes the shapes as consumed by web, sync, and functions — that is a *structural* statement (functions stores the same documents), not a package dependency.
|
|
4
6
|
|
|
5
7
|
Lives at the repo root as an npm workspace (`@doubling/types`). The compiled output is shipped at `dist/`, regenerated on `npm install` via the `prepare` script and rebuilt explicitly via `npm run build`.
|
|
6
8
|
|
|
@@ -16,6 +18,13 @@ The duplication was load-bearing for shipping (web and sync run in different env
|
|
|
16
18
|
|
|
17
19
|
## Layout
|
|
18
20
|
|
|
21
|
+
- `src/crdt-store.ts`: `UpdateRecord`, `Snapshot`, `UpdateStore`, and
|
|
22
|
+
`updateStoreConformance`. The CRDT update-store contract plus the behavioural
|
|
23
|
+
cases both `web/` and `sync/` run against their own backends. It lives here
|
|
24
|
+
because it is the only dependency-free part of the CRDT stack — types over
|
|
25
|
+
`Uint8Array` and `string`, since a store treats updates as opaque bytes — and
|
|
26
|
+
because a package with shared runtime deps cannot be shared at all under this
|
|
27
|
+
repo's nested install strategy (DOU-379).
|
|
19
28
|
- `src/firestore.ts`: `FileEntity`, `FolderEntity`, `Entity`, `OrgMembership`, `OrgMember`, `Team`, `TeamMember`, `UserProfile`, `Scope`.
|
|
20
29
|
- `src/mime.ts`: `EXT_TO_MIME`, `mimeTypeFromExt`, `isTextMimeType`.
|
|
21
30
|
- `src/paths.ts`: `extFromPath`, `storagePathFor`.
|
|
@@ -31,4 +40,16 @@ Edit `EXT_TO_MIME` in `src/mime.ts`. That's it. Both `web/` and `sync/` pick up
|
|
|
31
40
|
npm run -w types build
|
|
32
41
|
```
|
|
33
42
|
|
|
34
|
-
Emits `dist/index.js` plus `dist/*.d.ts`. The `dist/` directory is gitignored; CI runs the build automatically via the `prepare` script on `npm install
|
|
43
|
+
Emits `dist/index.js` plus `dist/*.d.ts`. The `dist/` directory is gitignored; CI runs the build automatically via the `prepare` script on `npm install` (`prepack` re-runs `tsc` before packaging). Only `dist/` and `README.md` are shipped in the published tarball (see the `files` field in `package.json`).
|
|
44
|
+
|
|
45
|
+
## Publish
|
|
46
|
+
|
|
47
|
+
Published to the public npm registry as `@doubling/types`. The [`.github/workflows/publish-types.yml`](../.github/workflows/publish-types.yml) workflow (**Publish Types to npm**) fires on every published GitHub Release, gated on the `@doubling/types@` tag prefix so a sibling `sync` release does not trigger it. It runs `npm ci`, `npm run build -w types`, skips cleanly if `types/package.json`'s version is already on npm, then `npm publish --access public` via npm Trusted Publishing (OIDC — no `NPM_TOKEN`). `workflow_dispatch` is exposed for bootstrap and transient-failure recovery. Version bumps are cut by Changesets (see `CHANGELOG.md`).
|
|
48
|
+
|
|
49
|
+
The package is published because `@doubling/compound-sync` is itself a published npm package that depends on `@doubling/types` at runtime, so the dependency must resolve from the registry outside the monorepo.
|
|
50
|
+
|
|
51
|
+
## Docs
|
|
52
|
+
|
|
53
|
+
- [`DESIGN.md`](DESIGN.md) — detailed design (exported symbols, path scheme, trade-offs).
|
|
54
|
+
- Root [`../DESIGN.md` §5](../DESIGN.md) — canonical data model.
|
|
55
|
+
- [`../docs/features/sync/index.md`](../docs/features/sync/index.md) — shared-schema bullet in the sync feature docs.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/** One stored update: the opaque CRDT bytes plus its ordering id. */
|
|
2
|
+
export interface UpdateRecord {
|
|
3
|
+
id: string;
|
|
4
|
+
update: Uint8Array;
|
|
5
|
+
}
|
|
6
|
+
/** A folded snapshot plus the highest update id it covers. */
|
|
7
|
+
export interface Snapshot {
|
|
8
|
+
state: Uint8Array;
|
|
9
|
+
/** null means "no known baseline" — replay the whole log. */
|
|
10
|
+
baselineId: string | null;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The storage seam between a CRDT provider and any concrete backend.
|
|
14
|
+
*
|
|
15
|
+
* Identity contract:
|
|
16
|
+
*
|
|
17
|
+
* - Every `UpdateRecord` has a unique, monotonically-ordered `id`.
|
|
18
|
+
* Backends choose the encoding (server timestamp + doc id, a counter,
|
|
19
|
+
* a ULID) but iterating in id order must replay in append order.
|
|
20
|
+
* - `baselineId` is the highest id covered by the current snapshot.
|
|
21
|
+
* `loadUpdatesAfter(baselineId)` returns strictly newer records.
|
|
22
|
+
* - Ids exist only on records **read back** from the store.
|
|
23
|
+
* `appendUpdate` returns void: the backend assigns the id (a server
|
|
24
|
+
* timestamp, in Firestore's case), so resolving it at append time
|
|
25
|
+
* costs a round trip for a value no caller uses (DOU-377).
|
|
26
|
+
*
|
|
27
|
+
* Deliberately absent: any method that folds or deletes. Compaction is
|
|
28
|
+
* destructive and only safe when the reader, the snapshot writer and the
|
|
29
|
+
* deleter are one actor in one consistent view, which a client cannot be
|
|
30
|
+
* while others append. It is the `compactYjsDoc` callable, and
|
|
31
|
+
* `firestore.rules` denies clients `delete` on `yupdates` and writes on
|
|
32
|
+
* `yjs/state` so this is enforced rather than conventional (DOU-409).
|
|
33
|
+
*
|
|
34
|
+
* This is a deliberate narrowing of what the two local interfaces
|
|
35
|
+
* declared before DOU-379. Both carried an optional
|
|
36
|
+
* `saveSnapshotAndCompact?(...)`, present so the in-memory stores could
|
|
37
|
+
* exercise fold semantics while the Firestore stores omitted it. Nothing
|
|
38
|
+
* ever called it *through the interface* — the tests that seed snapshots
|
|
39
|
+
* hold a concrete `MemoryUpdateStore` — so it was optional surface on a
|
|
40
|
+
* contract that no longer describes a store's job. The in-memory stores
|
|
41
|
+
* keep the method for those tests; it is simply not part of the contract.
|
|
42
|
+
*/
|
|
43
|
+
export interface UpdateStore {
|
|
44
|
+
loadSnapshot(): Promise<Snapshot | null>;
|
|
45
|
+
loadUpdatesAfter(baselineId: string | null): Promise<UpdateRecord[]>;
|
|
46
|
+
appendUpdate(update: Uint8Array): Promise<void>;
|
|
47
|
+
/** Stream records as they are appended.
|
|
48
|
+
*
|
|
49
|
+
* `baselineId` filters the stream exactly as `loadUpdatesAfter` does,
|
|
50
|
+
* and the provider passes what it already loaded. Without it every
|
|
51
|
+
* client re-received the document's whole history on attach --
|
|
52
|
+
* history it had just loaded itself -- and again on every reconnect.
|
|
53
|
+
* DOU-388 measured the shape: one append reaches every subscribed
|
|
54
|
+
* client, so at 200 collaborators a reconnect storm meant 200 clients
|
|
55
|
+
* each re-reading everything (DOU-395).
|
|
56
|
+
*
|
|
57
|
+
* Implementations must honour it, including in-memory ones, so tests
|
|
58
|
+
* exercise the same contract production does. */
|
|
59
|
+
subscribeUpdates(onUpdate: (record: UpdateRecord) => void, baselineId?: string | null): () => void;
|
|
60
|
+
}
|
|
61
|
+
/** Minimal assertion surface, so this module stays dependency-free and
|
|
62
|
+
* can be driven by `node:assert`, vitest, or anything else. */
|
|
63
|
+
export interface ConformanceAsserts {
|
|
64
|
+
equal(actual: unknown, expected: unknown, message?: string): void;
|
|
65
|
+
deepEqual(actual: unknown, expected: unknown, message?: string): void;
|
|
66
|
+
ok(value: unknown, message?: string): void;
|
|
67
|
+
}
|
|
68
|
+
export interface ConformanceCase {
|
|
69
|
+
name: string;
|
|
70
|
+
run(store: UpdateStore, a: ConformanceAsserts): Promise<void>;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Behavioural cases every `UpdateStore` must satisfy, expressed against
|
|
74
|
+
* the interface alone so one list can drive every backend.
|
|
75
|
+
*
|
|
76
|
+
* Each case gets a *fresh* store from the caller's factory — several
|
|
77
|
+
* assert on the empty state, and a shared store would make them
|
|
78
|
+
* order-dependent.
|
|
79
|
+
*
|
|
80
|
+
* Scope note: these cover the store contract, not Yjs semantics. Updates
|
|
81
|
+
* are arbitrary bytes here on purpose — a store that only works for
|
|
82
|
+
* well-formed Yjs payloads is a store that inspects payloads it should be
|
|
83
|
+
* treating as opaque. Convergence is covered separately.
|
|
84
|
+
*/
|
|
85
|
+
export declare const updateStoreConformance: ConformanceCase[];
|
|
86
|
+
//# sourceMappingURL=crdt-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crdt-store.d.ts","sourceRoot":"","sources":["../src/crdt-store.ts"],"names":[],"mappings":"AA6BA,qEAAqE;AACrE,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,UAAU,CAAC;CACpB;AAED,8DAA8D;AAC9D,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,UAAU,CAAC;IAClB,6DAA6D;IAC7D,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,WAAW,WAAW;IAC1B,YAAY,IAAI,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;IACzC,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IACrE,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD;;;;;;;;;;;qDAWiD;IACjD,gBAAgB,CACd,QAAQ,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,EACxC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,GACzB,MAAM,IAAI,CAAC;CACf;AAID;+DAC+D;AAC/D,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClE,SAAS,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtE,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5C;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,KAAK,EAAE,WAAW,EAAE,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/D;AAaD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,sBAAsB,EAAE,eAAe,EAgGnD,CAAC"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// The CRDT update-store contract, shared by web/ and sync/.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS LIVES IN @doubling/types
|
|
4
|
+
//
|
|
5
|
+
// The Yjs provider and its store backends exist twice — once in
|
|
6
|
+
// `web/src/yjs/`, once in `sync/`. That duplication is not carelessness:
|
|
7
|
+
// the root `.npmrc` sets `install-strategy=nested` on purpose, because
|
|
8
|
+
// `desktop/scripts/after-pack.cjs` needs `sync/node_modules/firebase` to
|
|
9
|
+
// be a real directory when it copies the daemon into the .app bundle,
|
|
10
|
+
// `functions/` deploys with its own `node_modules`, and
|
|
11
|
+
// `@doubling/compound-sync` publishes standalone. So a shared workspace
|
|
12
|
+
// package that imported `yjs` or `firebase` would resolve its *own*
|
|
13
|
+
// nested copies of them, and two instances of either is worse than the
|
|
14
|
+
// duplication: two `yjs` copies break Yjs' client-id identity, two
|
|
15
|
+
// `firebase` copies break the SDK's runtime type checks on
|
|
16
|
+
// `collection()` / `doc()` inputs (DOU-379).
|
|
17
|
+
//
|
|
18
|
+
// This file is the part that *can* be shared, because it has no runtime
|
|
19
|
+
// dependencies at all. It is types and plain functions over `Uint8Array`
|
|
20
|
+
// and `string` — the store treats updates as opaque bytes, so nothing
|
|
21
|
+
// here needs to know what Yjs is.
|
|
22
|
+
//
|
|
23
|
+
// The payoff is compile-time: both `FirestoreUpdateStore`s and both
|
|
24
|
+
// `MemoryUpdateStore`s declare `implements UpdateStore` against *this*
|
|
25
|
+
// interface, so a signature drifting on one side is a type error rather
|
|
26
|
+
// than something a reader has to notice. That is what actually failed
|
|
27
|
+
// before — the divergences found in this subsystem were all found by
|
|
28
|
+
// reading, never by a check.
|
|
29
|
+
const encoder = new TextEncoder();
|
|
30
|
+
const decoder = new TextDecoder();
|
|
31
|
+
function bytes(s) {
|
|
32
|
+
return encoder.encode(s);
|
|
33
|
+
}
|
|
34
|
+
function text(b) {
|
|
35
|
+
return decoder.decode(b);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Behavioural cases every `UpdateStore` must satisfy, expressed against
|
|
39
|
+
* the interface alone so one list can drive every backend.
|
|
40
|
+
*
|
|
41
|
+
* Each case gets a *fresh* store from the caller's factory — several
|
|
42
|
+
* assert on the empty state, and a shared store would make them
|
|
43
|
+
* order-dependent.
|
|
44
|
+
*
|
|
45
|
+
* Scope note: these cover the store contract, not Yjs semantics. Updates
|
|
46
|
+
* are arbitrary bytes here on purpose — a store that only works for
|
|
47
|
+
* well-formed Yjs payloads is a store that inspects payloads it should be
|
|
48
|
+
* treating as opaque. Convergence is covered separately.
|
|
49
|
+
*/
|
|
50
|
+
export const updateStoreConformance = [
|
|
51
|
+
{
|
|
52
|
+
name: 'loadSnapshot returns null before anything is written',
|
|
53
|
+
async run(store, a) {
|
|
54
|
+
a.equal(await store.loadSnapshot(), null);
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'loadUpdatesAfter(null) is empty on a fresh store',
|
|
59
|
+
async run(store, a) {
|
|
60
|
+
a.deepEqual(await store.loadUpdatesAfter(null), []);
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'appended updates read back in append order',
|
|
65
|
+
async run(store, a) {
|
|
66
|
+
await store.appendUpdate(bytes('one'));
|
|
67
|
+
await store.appendUpdate(bytes('two'));
|
|
68
|
+
await store.appendUpdate(bytes('three'));
|
|
69
|
+
const all = await store.loadUpdatesAfter(null);
|
|
70
|
+
a.deepEqual(all.map((r) => text(r.update)), ['one', 'two', 'three']);
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'ids sort in the same order records were appended',
|
|
75
|
+
async run(store, a) {
|
|
76
|
+
await store.appendUpdate(bytes('a'));
|
|
77
|
+
await store.appendUpdate(bytes('b'));
|
|
78
|
+
await store.appendUpdate(bytes('c'));
|
|
79
|
+
const ids = (await store.loadUpdatesAfter(null)).map((r) => r.id);
|
|
80
|
+
const sorted = [...ids].sort();
|
|
81
|
+
a.deepEqual(ids, sorted, 'iterating in id order must replay in append order — the ordering contract');
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'loadUpdatesAfter(baselineId) excludes the baseline itself',
|
|
86
|
+
async run(store, a) {
|
|
87
|
+
await store.appendUpdate(bytes('first'));
|
|
88
|
+
await store.appendUpdate(bytes('second'));
|
|
89
|
+
const all = await store.loadUpdatesAfter(null);
|
|
90
|
+
a.equal(all.length, 2);
|
|
91
|
+
const after = await store.loadUpdatesAfter(all[0].id);
|
|
92
|
+
a.deepEqual(after.map((r) => text(r.update)), ['second']);
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'the newest id yields an empty tail',
|
|
97
|
+
async run(store, a) {
|
|
98
|
+
await store.appendUpdate(bytes('only'));
|
|
99
|
+
const all = await store.loadUpdatesAfter(null);
|
|
100
|
+
a.deepEqual(await store.loadUpdatesAfter(all[all.length - 1].id), []);
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'an empty update round-trips as empty, not as absent',
|
|
105
|
+
async run(store, a) {
|
|
106
|
+
// Conflating "zero bytes" with "no record" is the shape of bug this
|
|
107
|
+
// subsystem keeps producing: DOU-404 (blob read failure faked as
|
|
108
|
+
// empty content) and the DOU-378 drain() bug (wrote '' over real
|
|
109
|
+
// content) were both that mistake. A store must keep them distinct.
|
|
110
|
+
await store.appendUpdate(new Uint8Array(0));
|
|
111
|
+
const all = await store.loadUpdatesAfter(null);
|
|
112
|
+
a.equal(all.length, 1, 'an empty update is still a record');
|
|
113
|
+
a.equal(all[0].update.length, 0);
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'subscribeUpdates delivers records appended after subscribing',
|
|
118
|
+
async run(store, a) {
|
|
119
|
+
const seen = [];
|
|
120
|
+
const unsubscribe = store.subscribeUpdates((r) => seen.push(text(r.update)));
|
|
121
|
+
await store.appendUpdate(bytes('live'));
|
|
122
|
+
// Backends deliver asynchronously; give the listener a turn.
|
|
123
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
124
|
+
a.ok(seen.includes('live'), `expected delivery, saw ${JSON.stringify(seen)}`);
|
|
125
|
+
unsubscribe();
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'unsubscribe stops delivery',
|
|
130
|
+
async run(store, a) {
|
|
131
|
+
const seen = [];
|
|
132
|
+
const unsubscribe = store.subscribeUpdates((r) => seen.push(text(r.update)));
|
|
133
|
+
unsubscribe();
|
|
134
|
+
await store.appendUpdate(bytes('after-unsubscribe'));
|
|
135
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
136
|
+
a.ok(!seen.includes('after-unsubscribe'), `listener fired after unsubscribe: ${JSON.stringify(seen)}`);
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
//# sourceMappingURL=crdt-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crdt-store.js","sourceRoot":"","sources":["../src/crdt-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** The slice of `Y.Text` the generators need. `Y.Text` satisfies this
|
|
2
|
+
* structurally, which is what keeps this module dependency-free. */
|
|
3
|
+
export interface TextLike {
|
|
4
|
+
readonly length: number;
|
|
5
|
+
insert(index: number, content: string): void;
|
|
6
|
+
delete(index: number, length: number): void;
|
|
7
|
+
}
|
|
8
|
+
/** mulberry32. Small, fast, adequate for shuffling and picking offsets;
|
|
9
|
+
* not cryptographic and does not need to be. */
|
|
10
|
+
export declare function makeRng(seed: number): () => number;
|
|
11
|
+
export declare function pick<T>(rng: () => number, xs: readonly T[]): T;
|
|
12
|
+
export declare function shuffled<T>(rng: () => number, xs: readonly T[]): T[];
|
|
13
|
+
/** Fixed seeds, named rather than a range so a failing one can be pinned
|
|
14
|
+
* in isolation while debugging. */
|
|
15
|
+
export declare const SEEDS: readonly [1, 7, 42, 1337, 90210, 555555];
|
|
16
|
+
/** Deliberately includes a newline, a non-ASCII letter and an astral-plane
|
|
17
|
+
* emoji: CRDT text indices are UTF-16 code units, so a surrogate pair is
|
|
18
|
+
* where off-by-one position handling shows up. */
|
|
19
|
+
export declare const WORDS: readonly ["alpha", "beta ", "gamma", " delta", "eps", "\n", "ζ", "🙂"];
|
|
20
|
+
export type Edit = {
|
|
21
|
+
kind: 'insert';
|
|
22
|
+
index: number;
|
|
23
|
+
text: string;
|
|
24
|
+
} | {
|
|
25
|
+
kind: 'delete';
|
|
26
|
+
index: number;
|
|
27
|
+
length: number;
|
|
28
|
+
};
|
|
29
|
+
/** Generate an edit valid against a text of the given length. Positions and
|
|
30
|
+
* lengths are clamped so generated edits are always applicable — the
|
|
31
|
+
* property under test is convergence, not the CRDT's bounds checking. */
|
|
32
|
+
export declare function generateEdit(rng: () => number, currentLength: number): Edit;
|
|
33
|
+
export declare function applyEdit(text: TextLike, edit: Edit): void;
|
|
34
|
+
//# sourceMappingURL=crdt-testing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crdt-testing.d.ts","sourceRoot":"","sources":["../src/crdt-testing.ts"],"names":[],"mappings":"AAwBA;oEACoE;AACpE,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7C;AAED;gDACgD;AAChD,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,MAAM,CASlD;AAED,wBAAgB,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,MAAM,EAAE,EAAE,EAAE,SAAS,CAAC,EAAE,GAAG,CAAC,CAE9D;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,MAAM,EAAE,EAAE,EAAE,SAAS,CAAC,EAAE,GAAG,CAAC,EAAE,CAOpE;AAED;mCACmC;AACnC,eAAO,MAAM,KAAK,0CAA4C,CAAC;AAE/D;;kDAEkD;AAClD,eAAO,MAAM,KAAK,wEAAyE,CAAC;AAE5F,MAAM,MAAM,IAAI,GACZ;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtD;;yEAEyE;AACzE,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI,CAW3E;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,CAQ1D"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Deterministic edit generators for CRDT convergence testing.
|
|
2
|
+
//
|
|
3
|
+
// WHY THESE ARE SHARED
|
|
4
|
+
//
|
|
5
|
+
// The provider and its stores exist twice (web/src/yjs/ and sync/) and
|
|
6
|
+
// cannot be collapsed into one package — see the header of ./crdt-store.ts
|
|
7
|
+
// for why. That makes it easy for the two sides' *tests* to drift as well
|
|
8
|
+
// as their implementations, and a shared implementation with two different
|
|
9
|
+
// generators would still let them disagree about what "an edit" is.
|
|
10
|
+
//
|
|
11
|
+
// These live here for the same reason the contract does: they have no
|
|
12
|
+
// runtime dependencies. `applyEdit` takes a structural `TextLike` rather
|
|
13
|
+
// than a `Y.Text`, which `Y.Text` satisfies without this module importing
|
|
14
|
+
// yjs — so it stays shareable under the repo's nested install strategy,
|
|
15
|
+
// where a shared package with runtime deps would resolve its own copies.
|
|
16
|
+
//
|
|
17
|
+
// DETERMINISM
|
|
18
|
+
//
|
|
19
|
+
// Seeded, never Math.random. A property test whose failures cannot be
|
|
20
|
+
// replayed is a flake generator: the seed appears in every assertion
|
|
21
|
+
// message so a failure reproduces exactly. The seed list is fixed rather
|
|
22
|
+
// than time-derived so CI runs the same cases every time — widening
|
|
23
|
+
// coverage is a deliberate edit to SEEDS, not a side effect of the clock.
|
|
24
|
+
/** mulberry32. Small, fast, adequate for shuffling and picking offsets;
|
|
25
|
+
* not cryptographic and does not need to be. */
|
|
26
|
+
export function makeRng(seed) {
|
|
27
|
+
let a = seed >>> 0;
|
|
28
|
+
return () => {
|
|
29
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
30
|
+
let t = a;
|
|
31
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
32
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
33
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function pick(rng, xs) {
|
|
37
|
+
return xs[Math.floor(rng() * xs.length)];
|
|
38
|
+
}
|
|
39
|
+
export function shuffled(rng, xs) {
|
|
40
|
+
const out = xs.slice();
|
|
41
|
+
for (let i = out.length - 1; i > 0; i -= 1) {
|
|
42
|
+
const j = Math.floor(rng() * (i + 1));
|
|
43
|
+
[out[i], out[j]] = [out[j], out[i]];
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
/** Fixed seeds, named rather than a range so a failing one can be pinned
|
|
48
|
+
* in isolation while debugging. */
|
|
49
|
+
export const SEEDS = [1, 7, 42, 1337, 90210, 555_555];
|
|
50
|
+
/** Deliberately includes a newline, a non-ASCII letter and an astral-plane
|
|
51
|
+
* emoji: CRDT text indices are UTF-16 code units, so a surrogate pair is
|
|
52
|
+
* where off-by-one position handling shows up. */
|
|
53
|
+
export const WORDS = ['alpha', 'beta ', 'gamma', ' delta', 'eps', '\n', 'ζ', '🙂'];
|
|
54
|
+
/** Generate an edit valid against a text of the given length. Positions and
|
|
55
|
+
* lengths are clamped so generated edits are always applicable — the
|
|
56
|
+
* property under test is convergence, not the CRDT's bounds checking. */
|
|
57
|
+
export function generateEdit(rng, currentLength) {
|
|
58
|
+
if (currentLength > 0 && rng() < 0.3) {
|
|
59
|
+
const index = Math.floor(rng() * currentLength);
|
|
60
|
+
const length = Math.max(1, Math.floor(rng() * Math.min(5, currentLength - index)));
|
|
61
|
+
return { kind: 'delete', index, length };
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
kind: 'insert',
|
|
65
|
+
index: Math.floor(rng() * (currentLength + 1)),
|
|
66
|
+
text: pick(rng, WORDS),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function applyEdit(text, edit) {
|
|
70
|
+
if (edit.kind === 'insert') {
|
|
71
|
+
text.insert(Math.min(edit.index, text.length), edit.text);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const index = Math.min(edit.index, text.length);
|
|
75
|
+
const length = Math.min(edit.length, text.length - index);
|
|
76
|
+
if (length > 0)
|
|
77
|
+
text.delete(index, length);
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=crdt-testing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crdt-testing.js","sourceRoot":"","sources":["../src/crdt-testing.ts"],"names":[],"mappings":"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"}
|
package/dist/firestore.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface FileEntity extends EntityBase {
|
|
|
19
19
|
storagePath?: string;
|
|
20
20
|
content?: string | null;
|
|
21
21
|
frontmatter?: unknown;
|
|
22
|
+
contentLoadFailed?: true;
|
|
22
23
|
}
|
|
23
24
|
export interface FolderEntity extends EntityBase {
|
|
24
25
|
type: 'folder';
|
|
@@ -37,6 +38,7 @@ export interface OrgMember {
|
|
|
37
38
|
email?: string;
|
|
38
39
|
role?: string;
|
|
39
40
|
orgName?: string;
|
|
41
|
+
teamIds?: string[];
|
|
40
42
|
}
|
|
41
43
|
export interface Team {
|
|
42
44
|
id: string;
|
package/dist/firestore.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"firestore.d.ts","sourceRoot":"","sources":["../src/firestore.ts"],"names":[],"mappings":"AAoBA,MAAM,MAAM,KAAK,GAAG,MAAM,GAAG,SAAS,CAAC;AAKvC,UAAU,UAAU;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC7B;AASD,MAAM,WAAW,UAAW,SAAQ,UAAU;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"firestore.d.ts","sourceRoot":"","sources":["../src/firestore.ts"],"names":[],"mappings":"AAoBA,MAAM,MAAM,KAAK,GAAG,MAAM,GAAG,SAAS,CAAC;AAKvC,UAAU,UAAU;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC7B;AASD,MAAM,WAAW,UAAW,SAAQ,UAAU;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;IAQtB,iBAAiB,CAAC,EAAE,IAAI,CAAC;CAC1B;AAGD,MAAM,WAAW,YAAa,SAAQ,UAAU;IAC9C,IAAI,EAAE,QAAQ,CAAC;CAChB;AAID,MAAM,MAAM,MAAM,GAAG,UAAU,GAAG,YAAY,CAAC;AAM/C,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAKD,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IAQjB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAGD,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAGD,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAKD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
// relative imports in TypeScript source, even though the actual file
|
|
5
5
|
// on disk is `.ts`. tsc and Node's ESM loader rewrite to the
|
|
6
6
|
// compiled `.js` at build / runtime.
|
|
7
|
+
export * from './crdt-store.js';
|
|
8
|
+
export * from './crdt-testing.js';
|
|
7
9
|
export * from './firestore.js';
|
|
8
10
|
export * from './mime.js';
|
|
9
11
|
export * from './paths.js';
|
|
12
|
+
export * from './scrub.js';
|
|
10
13
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,EAAE;AACF,6DAA6D;AAC7D,qEAAqE;AACrE,6DAA6D;AAC7D,qCAAqC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,EAAE;AACF,6DAA6D;AAC7D,qEAAqE;AACrE,6DAA6D;AAC7D,qCAAqC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC"}
|
package/dist/scrub.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The only shape allowed for context we attach deliberately.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is non-identifying metadata that the operation log
|
|
5
|
+
* already models (see sync/operation-log.ts). Notably absent, and
|
|
6
|
+
* absent on purpose: `path`, `name`, `content`, `email`.
|
|
7
|
+
*/
|
|
8
|
+
export interface SafeContext {
|
|
9
|
+
orgId?: string;
|
|
10
|
+
userId?: string;
|
|
11
|
+
teamId?: string;
|
|
12
|
+
opType?: string;
|
|
13
|
+
scope?: string;
|
|
14
|
+
entityType?: 'file' | 'folder';
|
|
15
|
+
/** Extension only — 'md', 'pdf'. Never the filename. */
|
|
16
|
+
ext?: string;
|
|
17
|
+
mimeType?: string;
|
|
18
|
+
sizeBytes?: number;
|
|
19
|
+
/** Shape of the path, not its content. */
|
|
20
|
+
pathDepth?: number;
|
|
21
|
+
nameLength?: number;
|
|
22
|
+
errorCode?: string;
|
|
23
|
+
agentVersion?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Redact a single string.
|
|
27
|
+
*
|
|
28
|
+
* Order matters: query strings first (Firebase Storage signed URLs
|
|
29
|
+
* carry `?token=`), then the most specific path forms, then bare
|
|
30
|
+
* filenames, then the length cap.
|
|
31
|
+
*/
|
|
32
|
+
export declare function redactString(input: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Scrub an error event in place-safe fashion (returns a new object).
|
|
35
|
+
*
|
|
36
|
+
* Returns `null` if scrubbing itself throws. Failing closed is the only
|
|
37
|
+
* acceptable behaviour here: dropping one event costs a diagnostic,
|
|
38
|
+
* leaking one costs customer data.
|
|
39
|
+
*/
|
|
40
|
+
export declare function scrubEvent<T extends object>(event: T): T | null;
|
|
41
|
+
//# sourceMappingURL=scrub.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scrub.d.ts","sourceRoot":"","sources":["../src/scrub.ts"],"names":[],"mappings":"AA2BA;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC/B,wDAAwD;IACxD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA8DD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CA6BlD;AAmCD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,IAAI,CA0E/D"}
|
package/dist/scrub.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// PII scrubbing for error telemetry.
|
|
2
|
+
//
|
|
3
|
+
// Compound is a file-sync product. File paths, file names and file
|
|
4
|
+
// CONTENTS are customer data, and the daemon runs on customer machines.
|
|
5
|
+
// Nothing resembling any of it may leave with an error report.
|
|
6
|
+
//
|
|
7
|
+
// Two layers, deliberately:
|
|
8
|
+
//
|
|
9
|
+
// 1. SafeContext — a typed allowlist for anything we attach on
|
|
10
|
+
// purpose. With `exactOptionalPropertyTypes` on, passing `{ path }`
|
|
11
|
+
// is a COMPILE error. PII discipline enforced by the compiler
|
|
12
|
+
// rather than by review.
|
|
13
|
+
//
|
|
14
|
+
// 2. scrubEvent — a redaction pass over everything the SDK populates
|
|
15
|
+
// on its own: messages, exception values, stack frames, breadcrumbs,
|
|
16
|
+
// request data. Layer 1 cannot help there, because none of it is
|
|
17
|
+
// ours to choose.
|
|
18
|
+
//
|
|
19
|
+
// Lives in @doubling/types so web, sync and desktop share one
|
|
20
|
+
// implementation. `functions/` keeps a small local copy: `firebase
|
|
21
|
+
// deploy` runs a standalone `npm ci --workspaces=false`, which cannot
|
|
22
|
+
// resolve a workspace dependency.
|
|
23
|
+
//
|
|
24
|
+
// Deliberately dependency-free and framework-agnostic — it takes and
|
|
25
|
+
// returns a plain object, so it is trivially testable without a Sentry
|
|
26
|
+
// SDK present.
|
|
27
|
+
// Folder names that begin a sync-relative path. Kept as literals rather
|
|
28
|
+
// than imported from paths.ts so this module stays dependency-free and
|
|
29
|
+
// so a rename there cannot silently narrow the redaction.
|
|
30
|
+
const SYNC_ROOTS = ['Private', 'Shared with Me', 'Shared by Me'];
|
|
31
|
+
const EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
|
|
32
|
+
// Absolute filesystem paths. These leak the OS username as well as the
|
|
33
|
+
// file, which is why they are redacted even though the daemon's own
|
|
34
|
+
// install directory is not itself secret.
|
|
35
|
+
const ABS_PATH_RE = /(?:\/Users\/|\/home\/|\/var\/folders\/|[A-Za-z]:\\Users\\)[^\s"')\]]+/g;
|
|
36
|
+
// Extensions we recognise. Used both to anchor path matching and to
|
|
37
|
+
// catch bare filenames.
|
|
38
|
+
const EXT_ALT = 'md|markdown|txt|json|ya?ml|csv|pdf|png|jpe?g|gif|heic|docx?|xlsx?|base';
|
|
39
|
+
const SYNC_ROOT_ALT = `(?:${SYNC_ROOTS.map(escapeRegExp).join('|')}|[^\\s"']+ Teamspace)`;
|
|
40
|
+
// A sync-relative path ENDING IN A KNOWN EXTENSION, allowing spaces
|
|
41
|
+
// inside segments.
|
|
42
|
+
//
|
|
43
|
+
// Spaces matter here: "Private/notes/Q3 Budget.md" is an entirely
|
|
44
|
+
// ordinary filename, and a pattern that stops at the first space
|
|
45
|
+
// reports it as two tokens with a misleading depth and extension. Both
|
|
46
|
+
// halves still get redacted — nothing leaks either way — but the shape
|
|
47
|
+
// metadata is the whole point of keeping a token at all, so it should
|
|
48
|
+
// be right.
|
|
49
|
+
//
|
|
50
|
+
// Non-greedy and anchored on the extension so it cannot run on into the
|
|
51
|
+
// prose that follows the path in a log line.
|
|
52
|
+
const SYNC_PATH_EXT_RE = new RegExp(`${SYNC_ROOT_ALT}/[^"')\\]]+?\\.(?:${EXT_ALT})\\b`, 'gi');
|
|
53
|
+
// Extension-less sync paths (folders). No space allowance: without an
|
|
54
|
+
// extension to anchor on, permitting spaces would swallow the rest of
|
|
55
|
+
// the sentence.
|
|
56
|
+
const SYNC_PATH_RE = new RegExp(`${SYNC_ROOT_ALT}/[^\\s"')\\]]+`, 'g');
|
|
57
|
+
// Any remaining bare filename with a plausible extension.
|
|
58
|
+
const FILENAME_RE = new RegExp(`[^\\s"'/\\\\)\\]]+\\.(?:${EXT_ALT})\\b`, 'gi');
|
|
59
|
+
// Content interpolated into a message is the failure mode this cannot
|
|
60
|
+
// enumerate its way out of, so there is a blunt length cap as backstop.
|
|
61
|
+
const MAX_STRING = 200;
|
|
62
|
+
function escapeRegExp(s) {
|
|
63
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
64
|
+
}
|
|
65
|
+
function depthOf(p) {
|
|
66
|
+
return p.split('/').filter(Boolean).length;
|
|
67
|
+
}
|
|
68
|
+
/** Replace a matched path with its shape: depth, extension, name length. */
|
|
69
|
+
function summarisePath(match) {
|
|
70
|
+
const name = match.split('/').pop() ?? '';
|
|
71
|
+
const dot = name.lastIndexOf('.');
|
|
72
|
+
const ext = dot > 0 ? name.slice(dot + 1).toLowerCase() : 'none';
|
|
73
|
+
return `<path:d${depthOf(match)}:ext=${ext}:len=${name.length}>`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Redact a single string.
|
|
77
|
+
*
|
|
78
|
+
* Order matters: query strings first (Firebase Storage signed URLs
|
|
79
|
+
* carry `?token=`), then the most specific path forms, then bare
|
|
80
|
+
* filenames, then the length cap.
|
|
81
|
+
*/
|
|
82
|
+
export function redactString(input) {
|
|
83
|
+
if (typeof input !== 'string' || input.length === 0)
|
|
84
|
+
return input;
|
|
85
|
+
let out = input;
|
|
86
|
+
// Strip query strings and fragments wholesale. A Storage download URL
|
|
87
|
+
// carries an access token; a redacted path with a live token attached
|
|
88
|
+
// would be worse than the path alone.
|
|
89
|
+
out = out.replace(/([?#])[^\s"')\]]*/g, '$1<redacted>');
|
|
90
|
+
out = out.replace(EMAIL_RE, '<email>');
|
|
91
|
+
out = out.replace(ABS_PATH_RE, (m) => `<abs-path:d${depthOf(m)}>`);
|
|
92
|
+
// Extension-anchored first: it is the more specific pattern, and
|
|
93
|
+
// running the looser one first would truncate at the first space.
|
|
94
|
+
out = out.replace(SYNC_PATH_EXT_RE, summarisePath);
|
|
95
|
+
out = out.replace(SYNC_PATH_RE, summarisePath);
|
|
96
|
+
out = out.replace(FILENAME_RE, (m) => {
|
|
97
|
+
const dot = m.lastIndexOf('.');
|
|
98
|
+
return `<file:ext=${m.slice(dot + 1).toLowerCase()}:len=${m.length}>`;
|
|
99
|
+
});
|
|
100
|
+
if (out.length > MAX_STRING) {
|
|
101
|
+
out = `${out.slice(0, MAX_STRING)}…[truncated]`;
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
/** Recursively redact every string in a value, depth-capped. */
|
|
106
|
+
function redactDeep(value, depth = 0) {
|
|
107
|
+
if (depth > 4)
|
|
108
|
+
return '<depth-capped>';
|
|
109
|
+
if (typeof value === 'string')
|
|
110
|
+
return redactString(value);
|
|
111
|
+
if (Array.isArray(value))
|
|
112
|
+
return value.map((v) => redactDeep(v, depth + 1));
|
|
113
|
+
if (value !== null && typeof value === 'object') {
|
|
114
|
+
const out = {};
|
|
115
|
+
for (const [k, v] of Object.entries(value)) {
|
|
116
|
+
out[k] = redactDeep(v, depth + 1);
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Scrub an error event in place-safe fashion (returns a new object).
|
|
124
|
+
*
|
|
125
|
+
* Returns `null` if scrubbing itself throws. Failing closed is the only
|
|
126
|
+
* acceptable behaviour here: dropping one event costs a diagnostic,
|
|
127
|
+
* leaking one costs customer data.
|
|
128
|
+
*/
|
|
129
|
+
export function scrubEvent(event) {
|
|
130
|
+
try {
|
|
131
|
+
// `T extends object` rather than `T extends ScrubbableEvent`: the
|
|
132
|
+
// point of this module is to stay usable without the Sentry SDK,
|
|
133
|
+
// and an SDK's own ErrorEvent type does not structurally satisfy a
|
|
134
|
+
// hand-written interface (its optional fields are narrower than the
|
|
135
|
+
// `unknown` used here). Callers get their exact type back; the
|
|
136
|
+
// internal view is the structural subset we actually walk.
|
|
137
|
+
const out = { ...event };
|
|
138
|
+
if (typeof out.message === 'string')
|
|
139
|
+
out.message = redactString(out.message);
|
|
140
|
+
if (typeof out.transaction === 'string')
|
|
141
|
+
out.transaction = redactString(out.transaction);
|
|
142
|
+
if (typeof out.culprit === 'string')
|
|
143
|
+
out.culprit = redactString(out.culprit);
|
|
144
|
+
if (out.logentry)
|
|
145
|
+
out.logentry = redactDeep(out.logentry);
|
|
146
|
+
if (out.exception?.values) {
|
|
147
|
+
out.exception = {
|
|
148
|
+
...out.exception,
|
|
149
|
+
values: out.exception.values.map((v) => {
|
|
150
|
+
const val = { ...v };
|
|
151
|
+
if (typeof val['value'] === 'string')
|
|
152
|
+
val['value'] = redactString(val['value']);
|
|
153
|
+
if (typeof val['type'] === 'string')
|
|
154
|
+
val['type'] = redactString(val['type']);
|
|
155
|
+
const st = val['stacktrace'];
|
|
156
|
+
if (st?.frames) {
|
|
157
|
+
val['stacktrace'] = {
|
|
158
|
+
...st,
|
|
159
|
+
frames: st.frames.map((f) => {
|
|
160
|
+
const frame = { ...f };
|
|
161
|
+
for (const k of ['filename', 'abs_path', 'module']) {
|
|
162
|
+
if (typeof frame[k] === 'string')
|
|
163
|
+
frame[k] = redactString(frame[k]);
|
|
164
|
+
}
|
|
165
|
+
// Local variables can hold an entire file body: the
|
|
166
|
+
// daemon throws from inside pushFileToCloud, whose
|
|
167
|
+
// scope holds `data: string | Buffer`. Never send them.
|
|
168
|
+
delete frame['vars'];
|
|
169
|
+
return frame;
|
|
170
|
+
}),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return val;
|
|
174
|
+
}),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (out.breadcrumbs)
|
|
178
|
+
out.breadcrumbs = redactDeep(out.breadcrumbs);
|
|
179
|
+
if (out.request) {
|
|
180
|
+
// Keep only a redacted URL. Bodies, headers and cookies carry
|
|
181
|
+
// auth tokens and file content and are never worth the risk.
|
|
182
|
+
const url = out.request['url'];
|
|
183
|
+
out.request = typeof url === 'string' ? { url: redactString(url) } : {};
|
|
184
|
+
}
|
|
185
|
+
// Identify the user by opaque uid only. Email is PII and the
|
|
186
|
+
// username is often the person's real name.
|
|
187
|
+
if (out.user) {
|
|
188
|
+
const id = out.user['id'];
|
|
189
|
+
out.user = id === undefined ? {} : { id };
|
|
190
|
+
}
|
|
191
|
+
// Dropped wholesale rather than redacted: `extra` is the field
|
|
192
|
+
// callers reach for when they have something interesting and
|
|
193
|
+
// unstructured, which is exactly when it holds a path or a body.
|
|
194
|
+
// Deliberate context goes through SafeContext instead.
|
|
195
|
+
delete out.extra;
|
|
196
|
+
if (out.contexts)
|
|
197
|
+
out.contexts = redactDeep(out.contexts);
|
|
198
|
+
if (out.tags)
|
|
199
|
+
out.tags = redactDeep(out.tags);
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=scrub.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scrub.js","sourceRoot":"","sources":["../src/scrub.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doubling/types",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Shared Firestore document interfaces and MIME / path helpers consumed by web/ and sync/. Single source of truth so the two consumers can never drift on a document shape (DOU-182, TS Phase 2 of the monorepo TypeScript migration).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "tsc",
|
|
20
|
+
"test": "node --test \"src/**/*.test.ts\"",
|
|
20
21
|
"typecheck": "tsc --noEmit",
|
|
21
22
|
"prepare": "tsc",
|
|
22
23
|
"prepack": "tsc"
|