@acornly/adapter-firebase 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +103 -2
- package/dist/index.d.ts +96 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +696 -0
- package/dist/index.js.map +1 -0
- package/firestore.indexes.json +41 -0
- package/firestore.rules +200 -0
- package/package.json +38 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Free Solo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,104 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @acornly/adapter-firebase
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A `StorageAdapter` that keeps Acornly notes in a site's own Firestore, with no Acornly Cloud involved. The site pays its own Firebase bill and Acornly charges nothing.
|
|
4
|
+
|
|
5
|
+
The adapter reads and writes as whoever is signed in on the site's Firebase Auth, or as a visitor when nobody is. The Firestore security rules this package ships decide what each of them may see and change.
|
|
6
|
+
|
|
7
|
+
## Set up Firebase
|
|
8
|
+
|
|
9
|
+
1. Deploy the rules and indexes that ship in the package, [`firestore.rules`](firestore.rules) and [`firestore.indexes.json`](firestore.indexes.json) (also under `node_modules/@acornly/adapter-firebase/`). If the site has no rules of its own, point `firebase.json` at them and run `firebase deploy --only firestore`:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"firestore": {
|
|
14
|
+
"rules": "node_modules/@acornly/adapter-firebase/firestore.rules",
|
|
15
|
+
"indexes": "node_modules/@acornly/adapter-firebase/firestore.indexes.json"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
If it has, copy the functions and the `acornly_` matches into its rules file, and the indexes into its indexes file. Everything they cover is in collections prefixed with `acornly_`.
|
|
21
|
+
|
|
22
|
+
2. List the scopes the site accepts notes in, as documents in `acornly_scopes` whose id is the scope. Signed-in users can write only to these, plus their own `user:<uid>` scope. Clients cannot write this collection, so add them in the Firebase console or with the Admin SDK:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
await admin.firestore().doc("acornly_scopes/app:my-site").set({});
|
|
26
|
+
// Let visitors who are not signed in read the public notes in this scope:
|
|
27
|
+
await admin.firestore().doc("acornly_scopes/web:example.com").set({ anonymousRead: true });
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
3. Turn on a sign-in method in Firebase Authentication. Notes are written as the signed-in Firebase user, and anonymous sign-ins cannot write.
|
|
31
|
+
|
|
32
|
+
## Use it
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { initializeApp } from "firebase/app";
|
|
36
|
+
import { getAuth } from "firebase/auth";
|
|
37
|
+
import { getFirestore } from "firebase/firestore";
|
|
38
|
+
import { FirebaseAdapter } from "@acornly/adapter-firebase";
|
|
39
|
+
|
|
40
|
+
const app = initializeApp(firebaseConfig);
|
|
41
|
+
const adapter = new FirebaseAdapter({ firestore: getFirestore(app), auth: getAuth(app) });
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`firebase` 12 or later in 12.x is a peer dependency: the adapter uses the site's app and adds nothing to the bundle for it.
|
|
45
|
+
|
|
46
|
+
## Access rules
|
|
47
|
+
|
|
48
|
+
| Who | Can read | Can write |
|
|
49
|
+
| ------------------------------ | --------------------------------------------- | --------------------------------------------------------------------------- |
|
|
50
|
+
| The note's creator | Their own notes, always | Edit or remove their own notes, in listed scopes or their own `user:` scope |
|
|
51
|
+
| Anyone signed in | `public` notes | New notes in listed scopes and their own `user:` scope |
|
|
52
|
+
| A visitor who is not signed in | `public` notes in scopes with `anonymousRead` | Nothing |
|
|
53
|
+
| The site's server (Admin SDK) | Everything (security rules do not apply) | Anything, so it must keep the fields below consistent itself |
|
|
54
|
+
|
|
55
|
+
- Firebase's anonymous sign-ins count as visitors who are not signed in, although they have a user id.
|
|
56
|
+
- Two audiences are supported: `private` (only the creator) and `public`. Circles, user lists and links need Acornly Cloud and are refused.
|
|
57
|
+
- Each note is one document in `acornly_annotations`, named by the note's id. Ids are 1 to 128 letters, digits, `_`, `~` or `-` (Acornly's are ULIDs), so a reply's target IRI holds its parent's id unescaped. Notes imported from W3C files keep their ids, so an import whose ids are IRIs (such as Hypothesis URLs) is refused here; give such notes new ids first.
|
|
58
|
+
- The document holds the annotation as JSON (`json`, up to 64 KB) and, beside it, the fields the rules check: the author's `uid`, `creatorId`, `scope`, `source` (`target.source`), `parent`, `threadRoot`, `depth`, `parentRev`, `audience`, `modified`, `rev` and `deleted`. When the adapter reads a note, those fields take precedence over the JSON's copy of them, so nobody can make a note look like it is somewhere or for someone it is not. The rest of the annotation is stored as sent, so validate notes from other users before trusting their shape. A document whose JSON is not an object with `target` and `ext` is skipped.
|
|
59
|
+
- Scopes are `app:`, `web:` or `user:` followed by anything but `/`. `modified` is an ISO 8601 time with a `Z` or an offset.
|
|
60
|
+
- `uid` is the signed-in user who created the note, checked by the rules. Show that as the author: the annotation's own `creator` field is whatever the client sent.
|
|
61
|
+
- `rev` is checked by the rules: a note starts at 1 and each update adds one. Every write is a Firestore transaction that reads the note first, so of two concurrent edits one wins and the other gets a `ConflictError`.
|
|
62
|
+
- Context on context (replies, tags, reactions, highlights on a note) targets its parent: `target.source` is `urn:contexthive:annotation:<parent id>`, and it carries `ext.threadRoot` and `ext.depth`. Add it with `adapter.threads.addChild` (or core's `addContext`). The rules check it against the stored parent: the parent must exist, be readable by the replier and not be deleted, and the child must have its scope, thread root, the next depth, and a parent rev the parent has had. The rules cannot see quotes inside the JSON, so only the adapter checks that `ext.parentRev` is set exactly when the child quotes its parent. A child can narrow its parent's audience (a private reply under a public note) but never widen it, on create or on edit. Children nest to any depth.
|
|
63
|
+
- Each note keeps the ids of its direct children in `kids`, changed only in the same write that adds or removes that child, which the rules check. That is how a delete knows a note still has children, even ones the deleter cannot read. It also means anyone who can read a note can read the ids of all its direct children, private ones included, and ULIDs carry their creation time: a private reply's existence and time are not secret, only its content and author.
|
|
64
|
+
- Plain updates keep the tree: no note moves to another scope, document or thread, `ext.depth`, `ext.parentRev` and the target stay, `ext.deleted` changes only through a delete, a deleted placeholder cannot be edited, and a note with replies keeps its `creator.id`. Plain removes refuse replies and notes with replies; the rules refuse removing a note that has children from any client.
|
|
65
|
+
- Delete with `adapter.threads.deleteNode` (or core's `deleteContext`). Only the author can delete a note (and anyone signed in can remove a placeholder with no children left). One with any reply, even one the author cannot see, becomes a placeholder: no body, author name or edited mark, `ext.deleted: true`. One without replies is removed, and so is each placeholder above it left with none, whoever wrote it: the rules let anyone remove a placeholder whose last child goes in the same write. A long chain of placeholders goes six at a time, each step its own transaction, since Firestore allows the rules of one write 20 document lookups; should a later step fail, the placeholders it leaves have no children and readers never see them.
|
|
66
|
+
- **Replies under a narrowed note stay readable.** Firestore rules check each note on its own and cannot follow a reply's parents, so when a note is narrowed from public to private, other people's public replies below it stay readable to anyone signed in who asks for them, by id or by loading the thread. Core's `assembleThread`, which the SDK and the built-in UI use, hides them; the private note itself stays private. Delete or narrow such replies when that is not enough.
|
|
67
|
+
- Edit history lives in `acornly_versions` (`adapter.versions`), one document per note holding its saved versions. Only the note's author can save, read or clear them; removing a note or turning it into a placeholder drops them. A Firestore document holds 1 MiB, so when a note's versions outgrow that, the oldest are dropped.
|
|
68
|
+
- Deleting a Firebase Auth user leaves their notes. Remove them from the site's server with the Admin SDK, which the rules do not check: delete a user's notes without replies, and turn ones with replies into placeholders, taking each removed note out of its parent's `kids`.
|
|
69
|
+
- `modified` comes from the client and is what `list({ since })` compares, so a client with a wrong clock (or one that writes a time far ahead) can make its edit invisible, or always present, to other clients' delta syncs. Treat delta sync as a shortcut and resync with a full `list` now and then.
|
|
70
|
+
- Ids are chosen by the client, so creating an id that already exists fails even when the existing note is hidden. Use random ids (Acornly's are ULIDs) so this reveals nothing. Reading an id behaves the same way: Firestore refuses a hidden note but reports a missing one as missing, so a probe can tell that a hidden id exists.
|
|
71
|
+
- Edits need the scope to still be listed (or be the author's own `user:` scope), as on Supabase; deletes do not, so a delisted scope's notes can still be removed by their authors.
|
|
72
|
+
- A parent's `kids` lives in its document, which Firestore caps at 1 MiB: a note can take tens of thousands of direct children, not more.
|
|
73
|
+
- Each reply, tag or reaction also writes its parent's document (its `kids`). Firestore sustains about one write a second to one document and a transaction gives up after five retries, so a burst of reactions on one note can fail some of them.
|
|
74
|
+
- Writes need a connection: Firestore transactions fail offline, whatever the site's cache settings.
|
|
75
|
+
|
|
76
|
+
## What it costs
|
|
77
|
+
|
|
78
|
+
Firestore bills each document a query returns or a rule looks up, not each request, so a page's notes cost reads in proportion to how many there are:
|
|
79
|
+
|
|
80
|
+
- `list` runs two queries for someone signed in (public notes, and their own) and one for a visitor, and is billed one read per note returned, at least one per query. A delta sync (`since`) reads only the notes changed since.
|
|
81
|
+
- Loading a thread reads its root and every node in it. Counting replies (`threads.countChildren`) reads each parent and runs two count queries, billed one read per 1,000 replies counted.
|
|
82
|
+
- `subscribe` keeps two snapshot listeners open per page, billed one read per note when they start and one per note changed while they run. A listener that reconnects after being offline for more than 30 minutes is billed as if it started again.
|
|
83
|
+
- Each write is a transaction that reads the note (and a reply's parent, a delete's placeholders above it), and the rules' lookups of other documents are billed as reads too: a few per write.
|
|
84
|
+
|
|
85
|
+
A page with many notes and many visitors can add up. Firestore's own usage tab shows the reads; budget alerts in the Google Cloud console warn before a bill grows.
|
|
86
|
+
|
|
87
|
+
## Live updates
|
|
88
|
+
|
|
89
|
+
`subscribe` listens to Firestore snapshots of the page's public notes and of the caller's own notes, which the same rules filter. A subscriber hears about its own adapter's writes at once and does not hear them again when the snapshot echoes them. Unlike Supabase Realtime, a note narrowed from public to private, or deleted, is told to other readers as removed.
|
|
90
|
+
|
|
91
|
+
- **Replies are not followed.** A subscription follows one page, and replies target their parent rather than the page, so subscribers hear about a note turning into a placeholder but not about replies being added or edited. Load threads with `threads.listThread` when they are opened.
|
|
92
|
+
- **Who is signed in is read when subscribing.** After someone signs in or out, subscribe again. Reloading the document with the SDK's `setDocument`, as the getting-started guide does when the user changes, does that.
|
|
93
|
+
- **A listener that fails stops.** A visitor subscribing to a scope without `anonymousRead`, or rules that are not deployed, end the subscription; `onListenError` (default `console.error`) is told why.
|
|
94
|
+
|
|
95
|
+
## Develop
|
|
96
|
+
|
|
97
|
+
The rules and the adapter are tested against the Firestore and Auth emulators, with the shared contract suites and the access-rule tests in `test/`. The emulators need Java 21; `firebase-tools` comes from `pnpm dlx`, at the version pinned in the `test:emulator` script, and downloads the Firestore emulator on first use.
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
cd packages/adapter-firebase
|
|
101
|
+
pnpm test:emulator # starts the emulators from firebase.json, runs the tests, stops them
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`scripts/ci-local.sh adapter-firebase` runs the same.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { Auth } from "firebase/auth";
|
|
2
|
+
import { type Firestore, type FirestoreError } from "firebase/firestore";
|
|
3
|
+
import { type Annotation, type ChangeEvent, type ListQuery, type StorageAdapter, type ThreadStore, type Unsubscribe, type VersionStore } from "@acornly/core";
|
|
4
|
+
export interface FirebaseAdapterOptions {
|
|
5
|
+
/** The site's own Firestore, with the shipped `firestore.rules` deployed to it. */
|
|
6
|
+
firestore: Firestore;
|
|
7
|
+
/**
|
|
8
|
+
* The site's Firebase Auth. The adapter reads and writes as whoever is signed in on it, or as a
|
|
9
|
+
* visitor when nobody is. Anonymous sign-ins count as visitors: they read, but cannot write.
|
|
10
|
+
*/
|
|
11
|
+
auth: Auth;
|
|
12
|
+
/**
|
|
13
|
+
* Called when a subscription's snapshot listener fails, for example when a visitor who is not
|
|
14
|
+
* signed in subscribes to a scope without `anonymousRead`, or the rules are not deployed.
|
|
15
|
+
* Firestore stops a failed listener; subscribe again to retry. Defaults to `console.error`.
|
|
16
|
+
*/
|
|
17
|
+
onListenError?: (error: FirestoreError) => void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Keeps annotations in a site's own Firestore, checked by the security rules this package ships
|
|
21
|
+
* (`firestore.rules`). Each write is a Firestore transaction that reads what it depends on, so
|
|
22
|
+
* concurrent edits conflict instead of overwriting each other, and the rules check every write.
|
|
23
|
+
*
|
|
24
|
+
* Subscribers hear about this adapter's own writes at once, and about everyone else's through
|
|
25
|
+
* Firestore snapshot listeners, which the same rules filter.
|
|
26
|
+
*/
|
|
27
|
+
export declare class FirebaseAdapter implements StorageAdapter {
|
|
28
|
+
readonly capabilities: {
|
|
29
|
+
realtime: boolean;
|
|
30
|
+
search: boolean;
|
|
31
|
+
offline: boolean;
|
|
32
|
+
};
|
|
33
|
+
private readonly db;
|
|
34
|
+
private readonly auth;
|
|
35
|
+
private readonly notes;
|
|
36
|
+
private readonly listeners;
|
|
37
|
+
private readonly onListenError;
|
|
38
|
+
constructor(options: FirebaseAdapterOptions);
|
|
39
|
+
/** Earlier versions, in `acornly_versions`: only the note's author can save or read them. */
|
|
40
|
+
readonly versions: VersionStore;
|
|
41
|
+
/**
|
|
42
|
+
* Reply threads. Each write is one transaction, and the rules check each new child against
|
|
43
|
+
* its parent and keep every note's list of children in step, so a delete knows whether a note
|
|
44
|
+
* still has children, even ones the deleter cannot read.
|
|
45
|
+
*/
|
|
46
|
+
readonly threads: ThreadStore;
|
|
47
|
+
/**
|
|
48
|
+
* Removes `start` in `tx`, then each placeholder above it left with no children, up to
|
|
49
|
+
* `CHAIN_STEP` notes, and takes the last one out of the children of the note above it. `next`
|
|
50
|
+
* is that note when it is a placeholder left with no children, still to remove.
|
|
51
|
+
*/
|
|
52
|
+
private removeUp;
|
|
53
|
+
list(q: ListQuery): Promise<Annotation[]>;
|
|
54
|
+
get(id: string): Promise<Annotation | null>;
|
|
55
|
+
create(a: Annotation): Promise<Annotation>;
|
|
56
|
+
update(a: Annotation, expectedRev: number): Promise<Annotation>;
|
|
57
|
+
remove(id: string, expectedRev: number): Promise<void>;
|
|
58
|
+
subscribe(q: Omit<ListQuery, "since">, onChange: (e: ChangeEvent) => void): Unsubscribe;
|
|
59
|
+
/** Who is signed in: `member` when with an account, not an anonymous sign-in. */
|
|
60
|
+
private who;
|
|
61
|
+
/** The signed-in account a write is made as. */
|
|
62
|
+
private writer;
|
|
63
|
+
private noteRef;
|
|
64
|
+
private versionsRef;
|
|
65
|
+
/** The stored note, or null when it is missing or the caller cannot read it. */
|
|
66
|
+
private readDoc;
|
|
67
|
+
/**
|
|
68
|
+
* The notes matching `constraints` that the caller can read: the public ones, and their own.
|
|
69
|
+
* The rules refuse any query that could return a note the caller cannot read, so each query
|
|
70
|
+
* asks for one of those two kinds.
|
|
71
|
+
*/
|
|
72
|
+
private readAll;
|
|
73
|
+
/**
|
|
74
|
+
* Runs a query. A visitor is refused a scope without `anonymousRead` as a whole query, which
|
|
75
|
+
* reads as finding nothing, as it would under row-level security.
|
|
76
|
+
*/
|
|
77
|
+
private fetch;
|
|
78
|
+
private count;
|
|
79
|
+
/**
|
|
80
|
+
* Runs `body` in a Firestore transaction, which Firestore may retry when what it read changed
|
|
81
|
+
* meanwhile. The rules refusing a write becomes a plain "not allowed".
|
|
82
|
+
*/
|
|
83
|
+
private transact;
|
|
84
|
+
/**
|
|
85
|
+
* Plain updates keep the tree: no note moves to another scope, document or thread, `deleted`
|
|
86
|
+
* changes only through `deleteNode`, a placeholder is never edited, a note with children keeps
|
|
87
|
+
* its author, and a child keeps its parent rev and gets no wider than its parent. The rules
|
|
88
|
+
* check the same, but cannot say which rule a write broke.
|
|
89
|
+
*/
|
|
90
|
+
private checkTreeKept;
|
|
91
|
+
private onRemote;
|
|
92
|
+
/** Tells this adapter's subscribers about one of its own writes. */
|
|
93
|
+
private emit;
|
|
94
|
+
private tell;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,EAeL,KAAK,SAAS,EACd,KAAK,cAAc,EAIpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAUL,KAAK,UAAU,EACf,KAAK,WAAW,EAEhB,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,YAAY,EAClB,MAAM,eAAe,CAAC;AAEvB,MAAM,WAAW,sBAAsB;IACrC,mFAAmF;IACnF,SAAS,EAAE,SAAS,CAAC;IACrB;;;OAGG;IACH,IAAI,EAAE,IAAI,CAAC;IACX;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;CACjD;AA0ED;;;;;;;GAOG;AACH,qBAAa,eAAgB,YAAW,cAAc;IACpD,QAAQ,CAAC,YAAY;;;;MAAqD;IAC1E,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAY;IAC/B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAO;IAC5B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsB;IAC5C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAuB;IACjD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkC;gBAEpD,OAAO,EAAE,sBAAsB;IAO3C,6FAA6F;IAC7F,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAyC7B;IAEF;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,WAAW,CAsG3B;IAEF;;;;OAIG;YACW,QAAQ;IAiChB,IAAI,CAAC,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAMzC,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAK3C,MAAM,CAAC,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IAyB1C,MAAM,CAAC,CAAC,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAa/D,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiB5D,SAAS,CAAC,CAAC,EAAE,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,WAAW,KAAK,IAAI,GAAG,WAAW;IAgEvF,iFAAiF;YACnE,GAAG;IAMjB,gDAAgD;YAClC,MAAM;IAMpB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,WAAW;IAInB,gFAAgF;YAClE,OAAO;IAWrB;;;;OAIG;YACW,OAAO;IAgBrB;;;OAGG;YACW,KAAK;YAUL,KAAK;IASnB;;;OAGG;YACW,QAAQ;IAStB;;;;;OAKG;YACW,aAAa;IAiC3B,OAAO,CAAC,QAAQ;IA2BhB,oEAAoE;IACpE,OAAO,CAAC,IAAI;IAoBZ,OAAO,CAAC,IAAI;CAUb"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
import { arrayRemove, arrayUnion, collection, doc, getCountFromServer, getDoc, getDocs, onSnapshot, query, runTransaction, where, } from "firebase/firestore";
|
|
2
|
+
import { afterAnnotationPrefix, AudienceError, ConflictError, NotFoundError, ThreadError, checkChild, isWithinAudience, parentIdOf, placeholderOf, } from "@acornly/core";
|
|
3
|
+
/** Collections the shipped rules cover. */
|
|
4
|
+
const NOTES = "acornly_annotations";
|
|
5
|
+
const VERSIONS = "acornly_versions";
|
|
6
|
+
/** Ids the rules accept: no character needs escaping in a target IRI or a document path. */
|
|
7
|
+
const ID = /^[A-Za-z0-9_~-]{1,128}$/;
|
|
8
|
+
/** The rules' limit on a note's JSON. */
|
|
9
|
+
const MAX_JSON = 65_536;
|
|
10
|
+
/** What the saved versions of one note may take, below Firestore's 1 MiB per document. */
|
|
11
|
+
const MAX_VERSION_BYTES = 1_000_000;
|
|
12
|
+
/**
|
|
13
|
+
* Notes one transaction removes at most when a delete takes placeholders above it along. The
|
|
14
|
+
* rules look up a few documents for each, and Firestore allows a write 20 lookups.
|
|
15
|
+
*/
|
|
16
|
+
const CHAIN_STEP = 6;
|
|
17
|
+
/** How long to wait for the snapshot echo of an own remove before giving up on it. */
|
|
18
|
+
const ECHO_WAIT_MS = 30_000;
|
|
19
|
+
/**
|
|
20
|
+
* Keeps annotations in a site's own Firestore, checked by the security rules this package ships
|
|
21
|
+
* (`firestore.rules`). Each write is a Firestore transaction that reads what it depends on, so
|
|
22
|
+
* concurrent edits conflict instead of overwriting each other, and the rules check every write.
|
|
23
|
+
*
|
|
24
|
+
* Subscribers hear about this adapter's own writes at once, and about everyone else's through
|
|
25
|
+
* Firestore snapshot listeners, which the same rules filter.
|
|
26
|
+
*/
|
|
27
|
+
export class FirebaseAdapter {
|
|
28
|
+
capabilities = { realtime: true, search: false, offline: false };
|
|
29
|
+
db;
|
|
30
|
+
auth;
|
|
31
|
+
notes;
|
|
32
|
+
listeners = new Set();
|
|
33
|
+
onListenError;
|
|
34
|
+
constructor(options) {
|
|
35
|
+
this.db = options.firestore;
|
|
36
|
+
this.auth = options.auth;
|
|
37
|
+
this.notes = collection(this.db, NOTES);
|
|
38
|
+
this.onListenError = options.onListenError ?? ((error) => console.error(error));
|
|
39
|
+
}
|
|
40
|
+
/** Earlier versions, in `acornly_versions`: only the note's author can save or read them. */
|
|
41
|
+
versions = {
|
|
42
|
+
saveVersion: async (a) => {
|
|
43
|
+
const me = await this.writer();
|
|
44
|
+
const ref = this.versionsRef(a.id);
|
|
45
|
+
await this.transact(a.id, async (tx) => {
|
|
46
|
+
const note = await readIn(tx, this.noteRef(a.id));
|
|
47
|
+
// Gone or deleted meanwhile (an edit that raced a delete): there is nothing to keep.
|
|
48
|
+
if (typeof note === "string" || note.doc.deleted)
|
|
49
|
+
return;
|
|
50
|
+
if (note.doc.uid !== me.uid) {
|
|
51
|
+
throw new Error(`Not allowed to keep versions of annotation ${a.id}`);
|
|
52
|
+
}
|
|
53
|
+
const stored = await tx.get(ref);
|
|
54
|
+
const items = { ...(stored.data()?.items ?? {}) };
|
|
55
|
+
const key = String(a.ext.rev);
|
|
56
|
+
items[key] = JSON.stringify(a);
|
|
57
|
+
// Firestore documents hold 1 MiB: the oldest versions make way.
|
|
58
|
+
const revs = Object.keys(items)
|
|
59
|
+
.filter((k) => k !== key)
|
|
60
|
+
.sort((x, y) => Number(x) - Number(y));
|
|
61
|
+
while (revs.length && versionBytes(items) > MAX_VERSION_BYTES)
|
|
62
|
+
delete items[revs.shift()];
|
|
63
|
+
tx.set(ref, { uid: me.uid, items });
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
listVersions: async (id) => {
|
|
67
|
+
let stored;
|
|
68
|
+
try {
|
|
69
|
+
stored = await getDoc(this.versionsRef(id));
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
if (isDenied(err))
|
|
73
|
+
return [];
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
const items = stored.data()?.items ?? {};
|
|
77
|
+
return Object.keys(items)
|
|
78
|
+
.sort((x, y) => Number(x) - Number(y))
|
|
79
|
+
.map((k) => JSON.parse(items[k]));
|
|
80
|
+
},
|
|
81
|
+
clearVersions: async (id) => {
|
|
82
|
+
await this.transact(id, async (tx) => {
|
|
83
|
+
tx.delete(this.versionsRef(id));
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Reply threads. Each write is one transaction, and the rules check each new child against
|
|
89
|
+
* its parent and keep every note's list of children in step, so a delete knows whether a note
|
|
90
|
+
* still has children, even ones the deleter cannot read.
|
|
91
|
+
*/
|
|
92
|
+
threads = {
|
|
93
|
+
listThread: async (q) => {
|
|
94
|
+
const [root, below] = await Promise.all([
|
|
95
|
+
this.get(q.threadRoot),
|
|
96
|
+
this.readAll([where("scope", "==", q.scope), where("threadRoot", "==", q.threadRoot)]),
|
|
97
|
+
]);
|
|
98
|
+
// Only a root starts a thread; a reply's id finds nothing, as on other backends.
|
|
99
|
+
return root && root.ext.scope === q.scope && root.ext.threadRoot == null
|
|
100
|
+
? [root, ...below]
|
|
101
|
+
: below;
|
|
102
|
+
},
|
|
103
|
+
countChildren: async (ids) => {
|
|
104
|
+
const counts = Object.fromEntries(ids.map((id) => [id, 0]));
|
|
105
|
+
const me = await this.who();
|
|
106
|
+
await Promise.all([...new Set(ids)].map(async (id) => {
|
|
107
|
+
// Children share the parent's scope; a query names it so the rules can check a visitor
|
|
108
|
+
// against that scope's `anonymousRead`.
|
|
109
|
+
const parent = await this.readDoc(id);
|
|
110
|
+
if (!parent)
|
|
111
|
+
return;
|
|
112
|
+
const under = [where("scope", "==", parent.scope), where("parent", "==", id)];
|
|
113
|
+
const counted = [query(this.notes, ...under, where("audience", "==", "public"))];
|
|
114
|
+
if (me?.member) {
|
|
115
|
+
counted.push(query(this.notes, ...under, where("uid", "==", me.uid), where("audience", "==", "private")));
|
|
116
|
+
}
|
|
117
|
+
const totals = await Promise.all(counted.map((c) => this.count(c, me?.member ?? false)));
|
|
118
|
+
counts[id] = totals.reduce((sum, n) => sum + n, 0);
|
|
119
|
+
}));
|
|
120
|
+
return counts;
|
|
121
|
+
},
|
|
122
|
+
addChild: async (child) => {
|
|
123
|
+
const parentId = parentIdOf(child);
|
|
124
|
+
if (parentId === null)
|
|
125
|
+
throw new Error(`Annotation ${child.id} is not context on context`);
|
|
126
|
+
const me = await this.writer();
|
|
127
|
+
const stored = storedAt(child, 1);
|
|
128
|
+
const fields = fieldsOf(stored, me.uid, [], null);
|
|
129
|
+
await this.transact(child.id, async (tx) => {
|
|
130
|
+
const parent = await readIn(tx, this.noteRef(parentId));
|
|
131
|
+
if (typeof parent === "string")
|
|
132
|
+
throw new NotFoundError(parentId);
|
|
133
|
+
const parentNote = toAnnotation(parentId, parent.doc);
|
|
134
|
+
if (!parentNote)
|
|
135
|
+
throw new ThreadError(`Annotation ${parentId} cannot be read`);
|
|
136
|
+
checkChild(child, parentNote);
|
|
137
|
+
if ((await readIn(tx, this.noteRef(child.id))) !== "missing") {
|
|
138
|
+
throw new Error(`Annotation ${child.id} already exists`);
|
|
139
|
+
}
|
|
140
|
+
tx.set(this.noteRef(child.id), fields);
|
|
141
|
+
tx.update(this.noteRef(parentId), { kids: arrayUnion(child.id), kidChange: child.id });
|
|
142
|
+
});
|
|
143
|
+
this.emit({ type: "created", annotation: stored });
|
|
144
|
+
return structuredClone(stored);
|
|
145
|
+
},
|
|
146
|
+
deleteNode: async (id, expectedRev, now) => {
|
|
147
|
+
const me = await this.writer();
|
|
148
|
+
const result = await this.transact(id, async (tx) => {
|
|
149
|
+
const target = current(id, expectedRev, await readIn(tx, this.noteRef(id)), me.uid);
|
|
150
|
+
if (target.deleted)
|
|
151
|
+
throw new ThreadError(`Annotation ${id} is already deleted`);
|
|
152
|
+
if (target.kids.length) {
|
|
153
|
+
const note = toAnnotation(id, target);
|
|
154
|
+
if (!note)
|
|
155
|
+
throw new Error(`Annotation ${id} is not stored as an annotation`);
|
|
156
|
+
const placeholder = placeholderOf(note, now);
|
|
157
|
+
const stored = storedAt(placeholder, target.rev + 1);
|
|
158
|
+
tx.set(this.noteRef(id), fieldsOf(stored, target.uid, target.kids, target.kidChange));
|
|
159
|
+
tx.delete(this.versionsRef(id));
|
|
160
|
+
return { placeholder: stored };
|
|
161
|
+
}
|
|
162
|
+
// Reads come before writes in a transaction.
|
|
163
|
+
const removal = await this.removeUp(tx, id, target);
|
|
164
|
+
tx.delete(this.versionsRef(id));
|
|
165
|
+
return removal;
|
|
166
|
+
});
|
|
167
|
+
if ("placeholder" in result) {
|
|
168
|
+
this.emit({ type: "updated", annotation: result.placeholder });
|
|
169
|
+
return { placeholder: structuredClone(result.placeholder), removed: [] };
|
|
170
|
+
}
|
|
171
|
+
// A long chain of placeholders goes a few at a time, each step its own transaction. Should
|
|
172
|
+
// a later step fail, the placeholders it left have no children, which readers never see.
|
|
173
|
+
const removed = [...result.removed];
|
|
174
|
+
let step = result;
|
|
175
|
+
while (step.next !== null) {
|
|
176
|
+
const top = step.next;
|
|
177
|
+
step = await this.transact(top, async (tx) => {
|
|
178
|
+
const found = await readIn(tx, this.noteRef(top));
|
|
179
|
+
if (typeof found === "string" || !found.doc.deleted || found.doc.kids.length) {
|
|
180
|
+
return { removed: [], next: null };
|
|
181
|
+
}
|
|
182
|
+
return this.removeUp(tx, top, found.doc);
|
|
183
|
+
}).catch(() => ({ removed: [], next: null }));
|
|
184
|
+
removed.push(...step.removed);
|
|
185
|
+
}
|
|
186
|
+
for (const r of removed) {
|
|
187
|
+
this.emit({ type: "removed", id: r.id }, { scope: r.doc.scope, document: r.doc.source });
|
|
188
|
+
}
|
|
189
|
+
return { removed: removed.map((r) => r.id) };
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* Removes `start` in `tx`, then each placeholder above it left with no children, up to
|
|
194
|
+
* `CHAIN_STEP` notes, and takes the last one out of the children of the note above it. `next`
|
|
195
|
+
* is that note when it is a placeholder left with no children, still to remove.
|
|
196
|
+
*/
|
|
197
|
+
async removeUp(tx, start, startDoc) {
|
|
198
|
+
const removed = [{ id: start, doc: startDoc }];
|
|
199
|
+
let parentId = startDoc.parent;
|
|
200
|
+
let parent = "missing";
|
|
201
|
+
while (parentId !== null) {
|
|
202
|
+
parent = await readIn(tx, this.noteRef(parentId));
|
|
203
|
+
const below = removed[removed.length - 1].id;
|
|
204
|
+
if (typeof parent === "string" ||
|
|
205
|
+
!parent.doc.deleted ||
|
|
206
|
+
parent.doc.kids.length !== 1 ||
|
|
207
|
+
parent.doc.kids[0] !== below ||
|
|
208
|
+
removed.length === CHAIN_STEP) {
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
removed.push({ id: parentId, doc: parent.doc });
|
|
212
|
+
parentId = parent.doc.parent;
|
|
213
|
+
}
|
|
214
|
+
for (const r of removed)
|
|
215
|
+
tx.delete(this.noteRef(r.id));
|
|
216
|
+
if (parentId === null || parent === "missing")
|
|
217
|
+
return { removed, next: null };
|
|
218
|
+
// The note above loses the last removed one from its children. One the deleter cannot read
|
|
219
|
+
// (narrowed since) still exists, and the rules let anyone make this change.
|
|
220
|
+
const last = removed[removed.length - 1].id;
|
|
221
|
+
tx.update(this.noteRef(parentId), { kids: arrayRemove(last), kidChange: last });
|
|
222
|
+
const emptied = typeof parent !== "string" &&
|
|
223
|
+
parent.doc.deleted &&
|
|
224
|
+
parent.doc.kids.length === 1 &&
|
|
225
|
+
parent.doc.kids[0] === last;
|
|
226
|
+
return { removed, next: emptied ? parentId : null };
|
|
227
|
+
}
|
|
228
|
+
async list(q) {
|
|
229
|
+
const constraints = [where("scope", "==", q.scope), where("source", "==", q.documentId)];
|
|
230
|
+
if (q.since !== undefined)
|
|
231
|
+
constraints.push(where("modified", ">", q.since));
|
|
232
|
+
return this.readAll(constraints);
|
|
233
|
+
}
|
|
234
|
+
async get(id) {
|
|
235
|
+
const found = await this.readDoc(id);
|
|
236
|
+
return found ? toAnnotation(id, found) : null;
|
|
237
|
+
}
|
|
238
|
+
async create(a) {
|
|
239
|
+
if (isAnnotationTarget(a.target.source)) {
|
|
240
|
+
throw new ThreadError(`Annotation ${a.id} targets an annotation; use threads.addChild`);
|
|
241
|
+
}
|
|
242
|
+
if ((a.ext.threadRoot ?? null) !== null ||
|
|
243
|
+
(a.ext.depth ?? 0) !== 0 ||
|
|
244
|
+
a.ext.parentRev !== undefined) {
|
|
245
|
+
throw new ThreadError(`Annotation ${a.id} has thread fields but targets a page`);
|
|
246
|
+
}
|
|
247
|
+
const me = await this.writer();
|
|
248
|
+
const stored = storedAt(a, 1);
|
|
249
|
+
const fields = fieldsOf(stored, me.uid, [], null);
|
|
250
|
+
await this.transact(a.id, async (tx) => {
|
|
251
|
+
// A note the caller cannot read is "hidden", which is taken too.
|
|
252
|
+
if ((await readIn(tx, this.noteRef(a.id))) !== "missing") {
|
|
253
|
+
throw new Error(`Annotation ${a.id} already exists`);
|
|
254
|
+
}
|
|
255
|
+
tx.set(this.noteRef(a.id), fields);
|
|
256
|
+
});
|
|
257
|
+
this.emit({ type: "created", annotation: stored });
|
|
258
|
+
return structuredClone(stored);
|
|
259
|
+
}
|
|
260
|
+
async update(a, expectedRev) {
|
|
261
|
+
const me = await this.writer();
|
|
262
|
+
const stored = await this.transact(a.id, async (tx) => {
|
|
263
|
+
const existing = current(a.id, expectedRev, await readIn(tx, this.noteRef(a.id)), me.uid);
|
|
264
|
+
await this.checkTreeKept(tx, a, existing);
|
|
265
|
+
const next = storedAt(a, existing.rev + 1);
|
|
266
|
+
tx.set(this.noteRef(a.id), fieldsOf(next, existing.uid, existing.kids, existing.kidChange));
|
|
267
|
+
return next;
|
|
268
|
+
});
|
|
269
|
+
this.emit({ type: "updated", annotation: stored });
|
|
270
|
+
return structuredClone(stored);
|
|
271
|
+
}
|
|
272
|
+
async remove(id, expectedRev) {
|
|
273
|
+
const me = await this.writer();
|
|
274
|
+
const removed = await this.transact(id, async (tx) => {
|
|
275
|
+
const existing = current(id, expectedRev, await readIn(tx, this.noteRef(id)), me.uid);
|
|
276
|
+
if (existing.kids.length) {
|
|
277
|
+
throw new ThreadError(`Annotation ${id} has children; use threads.deleteNode`);
|
|
278
|
+
}
|
|
279
|
+
if (existing.parent !== null) {
|
|
280
|
+
throw new ThreadError(`Annotation ${id} is context on context; use threads.deleteNode`);
|
|
281
|
+
}
|
|
282
|
+
tx.delete(this.noteRef(id));
|
|
283
|
+
tx.delete(this.versionsRef(id));
|
|
284
|
+
return existing;
|
|
285
|
+
});
|
|
286
|
+
this.emit({ type: "removed", id }, { scope: removed.scope, document: removed.source });
|
|
287
|
+
}
|
|
288
|
+
subscribe(q, onChange) {
|
|
289
|
+
const listener = {
|
|
290
|
+
documentId: q.documentId,
|
|
291
|
+
scope: q.scope,
|
|
292
|
+
onChange,
|
|
293
|
+
told: new Map(),
|
|
294
|
+
removing: new Map(),
|
|
295
|
+
stopped: false,
|
|
296
|
+
};
|
|
297
|
+
this.listeners.add(listener);
|
|
298
|
+
const stops = [];
|
|
299
|
+
void this.who().then((me) => {
|
|
300
|
+
if (listener.stopped)
|
|
301
|
+
return;
|
|
302
|
+
const base = [where("scope", "==", q.scope), where("source", "==", q.documentId)];
|
|
303
|
+
// Public notes, and the caller's own of any audience. The own feed tells about the
|
|
304
|
+
// caller's notes, so the public feed skips them.
|
|
305
|
+
const feeds = [
|
|
306
|
+
{
|
|
307
|
+
feed: query(this.notes, ...base, where("audience", "==", "public")),
|
|
308
|
+
skipUid: me?.member ? me.uid : null,
|
|
309
|
+
},
|
|
310
|
+
];
|
|
311
|
+
if (me?.member) {
|
|
312
|
+
feeds.push({ feed: query(this.notes, ...base, where("uid", "==", me.uid)), skipUid: null });
|
|
313
|
+
}
|
|
314
|
+
for (const { feed, skipUid } of feeds) {
|
|
315
|
+
// The first snapshot is what already exists, not a change: its revs are only noted, so
|
|
316
|
+
// later snapshots (the server's, when the first came from the cache) tell what differs.
|
|
317
|
+
// A server snapshot that changes nothing raises no callback, so none is waited for.
|
|
318
|
+
let first = true;
|
|
319
|
+
stops.push(onSnapshot(feed, (snap) => {
|
|
320
|
+
if (listener.stopped)
|
|
321
|
+
return;
|
|
322
|
+
if (first) {
|
|
323
|
+
first = false;
|
|
324
|
+
for (const d of snap.docs) {
|
|
325
|
+
const data = d.data();
|
|
326
|
+
if (skipUid !== null && data.uid === skipUid)
|
|
327
|
+
continue;
|
|
328
|
+
if (!listener.told.has(d.id))
|
|
329
|
+
listener.told.set(d.id, data.rev);
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
for (const change of snap.docChanges()) {
|
|
334
|
+
const d = change.doc.data();
|
|
335
|
+
if (skipUid !== null && d.uid === skipUid)
|
|
336
|
+
continue;
|
|
337
|
+
this.onRemote(listener, change.type, change.doc.id, d);
|
|
338
|
+
}
|
|
339
|
+
}, (error) => {
|
|
340
|
+
if (!listener.stopped)
|
|
341
|
+
this.onListenError(error);
|
|
342
|
+
}));
|
|
343
|
+
}
|
|
344
|
+
}, this.onListenError);
|
|
345
|
+
return () => {
|
|
346
|
+
listener.stopped = true;
|
|
347
|
+
this.listeners.delete(listener);
|
|
348
|
+
for (const stop of stops)
|
|
349
|
+
stop();
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
/** Who is signed in: `member` when with an account, not an anonymous sign-in. */
|
|
353
|
+
async who() {
|
|
354
|
+
await this.auth.authStateReady();
|
|
355
|
+
const user = this.auth.currentUser;
|
|
356
|
+
return user ? { uid: user.uid, member: !user.isAnonymous } : null;
|
|
357
|
+
}
|
|
358
|
+
/** The signed-in account a write is made as. */
|
|
359
|
+
async writer() {
|
|
360
|
+
const me = await this.who();
|
|
361
|
+
if (!me?.member)
|
|
362
|
+
throw new Error("Sign in with an account to write notes");
|
|
363
|
+
return me;
|
|
364
|
+
}
|
|
365
|
+
noteRef(id) {
|
|
366
|
+
return doc(this.notes, checkId(id));
|
|
367
|
+
}
|
|
368
|
+
versionsRef(id) {
|
|
369
|
+
return doc(this.db, VERSIONS, checkId(id));
|
|
370
|
+
}
|
|
371
|
+
/** The stored note, or null when it is missing or the caller cannot read it. */
|
|
372
|
+
async readDoc(id) {
|
|
373
|
+
if (!ID.test(id))
|
|
374
|
+
return null;
|
|
375
|
+
try {
|
|
376
|
+
const snap = await getDoc(this.noteRef(id));
|
|
377
|
+
return snap.exists() ? snap.data() : null;
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
if (isDenied(err))
|
|
381
|
+
return null;
|
|
382
|
+
throw err;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* The notes matching `constraints` that the caller can read: the public ones, and their own.
|
|
387
|
+
* The rules refuse any query that could return a note the caller cannot read, so each query
|
|
388
|
+
* asks for one of those two kinds.
|
|
389
|
+
*/
|
|
390
|
+
async readAll(constraints) {
|
|
391
|
+
const me = await this.who();
|
|
392
|
+
const queries = [query(this.notes, ...constraints, where("audience", "==", "public"))];
|
|
393
|
+
if (me?.member)
|
|
394
|
+
queries.push(query(this.notes, ...constraints, where("uid", "==", me.uid)));
|
|
395
|
+
const byId = new Map();
|
|
396
|
+
for (const snap of await Promise.all(queries.map((q) => this.fetch(q, me?.member ?? false)))) {
|
|
397
|
+
for (const d of snap) {
|
|
398
|
+
const a = toAnnotation(d.id, d.data);
|
|
399
|
+
if (a)
|
|
400
|
+
byId.set(d.id, a);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return [...byId.values()].sort((x, y) => x.modified === y.modified ? (x.id < y.id ? -1 : 1) : x.modified < y.modified ? -1 : 1);
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Runs a query. A visitor is refused a scope without `anonymousRead` as a whole query, which
|
|
407
|
+
* reads as finding nothing, as it would under row-level security.
|
|
408
|
+
*/
|
|
409
|
+
async fetch(q, member) {
|
|
410
|
+
try {
|
|
411
|
+
const snap = await getDocs(q);
|
|
412
|
+
return snap.docs.map((d) => ({ id: d.id, data: d.data() }));
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
if (!member && isDenied(err))
|
|
416
|
+
return [];
|
|
417
|
+
throw err;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async count(q, member) {
|
|
421
|
+
try {
|
|
422
|
+
return (await getCountFromServer(q)).data().count;
|
|
423
|
+
}
|
|
424
|
+
catch (err) {
|
|
425
|
+
if (!member && isDenied(err))
|
|
426
|
+
return 0;
|
|
427
|
+
throw err;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Runs `body` in a Firestore transaction, which Firestore may retry when what it read changed
|
|
432
|
+
* meanwhile. The rules refusing a write becomes a plain "not allowed".
|
|
433
|
+
*/
|
|
434
|
+
async transact(id, body) {
|
|
435
|
+
try {
|
|
436
|
+
return await runTransaction(this.db, body);
|
|
437
|
+
}
|
|
438
|
+
catch (err) {
|
|
439
|
+
if (isDenied(err))
|
|
440
|
+
throw new Error(`Not allowed to change annotation ${id}`, { cause: err });
|
|
441
|
+
throw err;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Plain updates keep the tree: no note moves to another scope, document or thread, `deleted`
|
|
446
|
+
* changes only through `deleteNode`, a placeholder is never edited, a note with children keeps
|
|
447
|
+
* its author, and a child keeps its parent rev and gets no wider than its parent. The rules
|
|
448
|
+
* check the same, but cannot say which rule a write broke.
|
|
449
|
+
*/
|
|
450
|
+
async checkTreeKept(tx, next, existing) {
|
|
451
|
+
if ((next.ext.deleted ?? false) !== existing.deleted) {
|
|
452
|
+
throw new ThreadError(`Annotation ${next.id} is deleted or restored only by deleteNode`);
|
|
453
|
+
}
|
|
454
|
+
if (existing.deleted)
|
|
455
|
+
throw new ThreadError(`Annotation ${next.id} is deleted`);
|
|
456
|
+
if (next.ext.scope !== existing.scope || next.target.source !== existing.source) {
|
|
457
|
+
throw new ThreadError(`Annotation ${next.id} cannot move to another scope or document`);
|
|
458
|
+
}
|
|
459
|
+
if (existing.kids.length && (next.creator?.id ?? null) !== existing.creatorId) {
|
|
460
|
+
throw new ThreadError(`Annotation ${next.id} has replies; its author stays`);
|
|
461
|
+
}
|
|
462
|
+
if ((next.ext.threadRoot ?? null) !== existing.threadRoot ||
|
|
463
|
+
(next.ext.depth ?? 0) !== existing.depth ||
|
|
464
|
+
(next.ext.parentRev ?? null) !== existing.parentRev) {
|
|
465
|
+
throw new ThreadError(`Annotation ${next.id} cannot move in its thread`);
|
|
466
|
+
}
|
|
467
|
+
checkAudience(next);
|
|
468
|
+
// An unchanged or narrower audience is fine even if the parent was narrowed meanwhile.
|
|
469
|
+
if (existing.parent === null || next.ext.audience.kind === existing.audience)
|
|
470
|
+
return;
|
|
471
|
+
if (next.ext.audience.kind === "private")
|
|
472
|
+
return;
|
|
473
|
+
const parent = await readIn(tx, this.noteRef(existing.parent));
|
|
474
|
+
// A parent the caller cannot read is private to someone else, and the rules let a child widen
|
|
475
|
+
// only under a stored public parent.
|
|
476
|
+
if (typeof parent === "string" ||
|
|
477
|
+
!isWithinAudience(next.ext.audience, { kind: parent.doc.audience })) {
|
|
478
|
+
throw new AudienceError(`Annotation ${next.id} cannot be wider than its parent`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
onRemote(listener, type, id, d) {
|
|
482
|
+
let removing = listener.removing.get(id);
|
|
483
|
+
if (removing !== undefined && Date.now() > removing) {
|
|
484
|
+
// The echo never came (the listener never had the note); stop waiting for it.
|
|
485
|
+
listener.removing.delete(id);
|
|
486
|
+
removing = undefined;
|
|
487
|
+
}
|
|
488
|
+
if (type === "removed") {
|
|
489
|
+
listener.removing.delete(id);
|
|
490
|
+
if (listener.told.get(id) === "removed")
|
|
491
|
+
return;
|
|
492
|
+
listener.told.set(id, "removed");
|
|
493
|
+
this.tell(listener, { type: "removed", id });
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
// A snapshot from before this adapter's own remove of the note.
|
|
497
|
+
if (removing !== undefined)
|
|
498
|
+
return;
|
|
499
|
+
const annotation = toAnnotation(id, d);
|
|
500
|
+
if (!annotation)
|
|
501
|
+
return;
|
|
502
|
+
const last = listener.told.get(id);
|
|
503
|
+
if (typeof last === "number" && last >= d.rev)
|
|
504
|
+
return;
|
|
505
|
+
listener.told.set(id, d.rev);
|
|
506
|
+
this.tell(listener, {
|
|
507
|
+
type: type === "added" || last === "removed" ? "created" : "updated",
|
|
508
|
+
annotation,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
/** Tells this adapter's subscribers about one of its own writes. */
|
|
512
|
+
emit(event, where) {
|
|
513
|
+
const document = where?.document ?? event.annotation.target.source;
|
|
514
|
+
const scope = where?.scope ?? event.annotation.ext.scope;
|
|
515
|
+
for (const l of this.listeners) {
|
|
516
|
+
if (l.documentId !== document || l.scope !== scope)
|
|
517
|
+
continue;
|
|
518
|
+
if (event.type === "removed") {
|
|
519
|
+
l.removing.set(event.id, Date.now() + ECHO_WAIT_MS);
|
|
520
|
+
if (l.told.get(event.id) === "removed")
|
|
521
|
+
continue;
|
|
522
|
+
l.told.set(event.id, "removed");
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
const last = l.told.get(event.annotation.id);
|
|
526
|
+
// The snapshot echo beat the response: already told.
|
|
527
|
+
if (typeof last === "number" && last >= event.annotation.ext.rev)
|
|
528
|
+
continue;
|
|
529
|
+
l.told.set(event.annotation.id, event.annotation.ext.rev);
|
|
530
|
+
}
|
|
531
|
+
this.tell(l, event);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
tell(listener, event) {
|
|
535
|
+
// A throwing listener must not fail a write that has already committed.
|
|
536
|
+
try {
|
|
537
|
+
listener.onChange(structuredClone(event));
|
|
538
|
+
}
|
|
539
|
+
catch (err) {
|
|
540
|
+
queueMicrotask(() => {
|
|
541
|
+
throw err;
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/** `a` as stored at `rev`, a copy the caller cannot change. */
|
|
547
|
+
function storedAt(a, rev) {
|
|
548
|
+
return structuredClone({ ...a, ext: { ...a.ext, rev } });
|
|
549
|
+
}
|
|
550
|
+
/** The document for `a`, which is already at its stored rev. */
|
|
551
|
+
function fieldsOf(a, uid, kids, kidChange) {
|
|
552
|
+
checkId(a.id);
|
|
553
|
+
checkAudience(a);
|
|
554
|
+
for (const [name, value] of [
|
|
555
|
+
["ext.scope", a.ext.scope],
|
|
556
|
+
["target.source", a.target.source],
|
|
557
|
+
["modified", a.modified],
|
|
558
|
+
]) {
|
|
559
|
+
if (typeof value !== "string")
|
|
560
|
+
throw new TypeError(`Annotation ${a.id}: ${name} must be text`);
|
|
561
|
+
}
|
|
562
|
+
const creatorId = a.creator?.id ?? null;
|
|
563
|
+
if (creatorId !== null && typeof creatorId !== "string") {
|
|
564
|
+
throw new TypeError(`Annotation ${a.id}: creator.id must be text`);
|
|
565
|
+
}
|
|
566
|
+
const json = JSON.stringify(a);
|
|
567
|
+
if (json.length > MAX_JSON)
|
|
568
|
+
throw new Error(`Annotation ${a.id} is larger than 64 KB`);
|
|
569
|
+
return {
|
|
570
|
+
uid,
|
|
571
|
+
creatorId,
|
|
572
|
+
scope: a.ext.scope,
|
|
573
|
+
source: a.target.source,
|
|
574
|
+
parent: parentIdOf(a),
|
|
575
|
+
threadRoot: a.ext.threadRoot ?? null,
|
|
576
|
+
depth: a.ext.depth ?? 0,
|
|
577
|
+
parentRev: a.ext.parentRev ?? null,
|
|
578
|
+
audience: a.ext.audience.kind,
|
|
579
|
+
modified: a.modified,
|
|
580
|
+
rev: a.ext.rev,
|
|
581
|
+
deleted: a.ext.deleted ?? false,
|
|
582
|
+
kids,
|
|
583
|
+
kidChange,
|
|
584
|
+
json,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* The annotation a document holds, with the fields the rules check taking precedence over the
|
|
589
|
+
* JSON's copy of them, or null when the JSON is not an annotation.
|
|
590
|
+
*/
|
|
591
|
+
function toAnnotation(id, d) {
|
|
592
|
+
let a;
|
|
593
|
+
try {
|
|
594
|
+
a = JSON.parse(d.json);
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
if (!isObject(a) || !isObject(a.target) || !isObject(a.ext))
|
|
600
|
+
return null;
|
|
601
|
+
a.id = id;
|
|
602
|
+
a.modified = d.modified;
|
|
603
|
+
a.target.source = d.source;
|
|
604
|
+
const ext = a.ext;
|
|
605
|
+
ext.scope = d.scope;
|
|
606
|
+
ext.rev = d.rev;
|
|
607
|
+
if (!isObject(ext.audience) || ext.audience.kind !== d.audience) {
|
|
608
|
+
ext.audience = { kind: d.audience };
|
|
609
|
+
}
|
|
610
|
+
if ((ext.threadRoot ?? null) !== d.threadRoot) {
|
|
611
|
+
if (d.threadRoot === null)
|
|
612
|
+
delete ext.threadRoot;
|
|
613
|
+
else
|
|
614
|
+
ext.threadRoot = d.threadRoot;
|
|
615
|
+
}
|
|
616
|
+
if ((ext.depth ?? 0) !== d.depth) {
|
|
617
|
+
if (d.depth === 0)
|
|
618
|
+
delete ext.depth;
|
|
619
|
+
else
|
|
620
|
+
ext.depth = d.depth;
|
|
621
|
+
}
|
|
622
|
+
if ((ext.parentRev ?? null) !== d.parentRev) {
|
|
623
|
+
if (d.parentRev === null)
|
|
624
|
+
delete ext.parentRev;
|
|
625
|
+
else
|
|
626
|
+
ext.parentRev = d.parentRev;
|
|
627
|
+
}
|
|
628
|
+
if ((ext.deleted ?? false) !== d.deleted) {
|
|
629
|
+
if (d.deleted)
|
|
630
|
+
ext.deleted = true;
|
|
631
|
+
else
|
|
632
|
+
delete ext.deleted;
|
|
633
|
+
}
|
|
634
|
+
if ((a.creator?.id ?? null) !== d.creatorId) {
|
|
635
|
+
if (d.creatorId === null)
|
|
636
|
+
delete a.creator;
|
|
637
|
+
else
|
|
638
|
+
a.creator = { id: d.creatorId };
|
|
639
|
+
}
|
|
640
|
+
return a;
|
|
641
|
+
}
|
|
642
|
+
/** The note a transaction read, if it exists at `expectedRev`. */
|
|
643
|
+
function current(id, expectedRev, found, uid) {
|
|
644
|
+
// A note the caller cannot read is missing to them, as under row-level security.
|
|
645
|
+
if (typeof found === "string")
|
|
646
|
+
throw new NotFoundError(id);
|
|
647
|
+
if (found.doc.uid !== uid)
|
|
648
|
+
throw new Error(`Not allowed to change annotation ${id}`);
|
|
649
|
+
if (found.doc.rev !== expectedRev)
|
|
650
|
+
throw new ConflictError(id, expectedRev, found.doc.rev);
|
|
651
|
+
return found.doc;
|
|
652
|
+
}
|
|
653
|
+
async function readIn(tx, ref) {
|
|
654
|
+
try {
|
|
655
|
+
const snap = await tx.get(ref);
|
|
656
|
+
return snap.exists() ? { doc: snap.data() } : "missing";
|
|
657
|
+
}
|
|
658
|
+
catch (err) {
|
|
659
|
+
if (isDenied(err))
|
|
660
|
+
return "hidden";
|
|
661
|
+
throw err;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
function checkId(id) {
|
|
665
|
+
if (typeof id !== "string" || !ID.test(id)) {
|
|
666
|
+
throw new Error(`Annotation id ${JSON.stringify(id)} is not 1 to 128 letters, digits, "_", "~" or "-"`);
|
|
667
|
+
}
|
|
668
|
+
return id;
|
|
669
|
+
}
|
|
670
|
+
/** Only private and public notes are stored; other audiences need Acornly Cloud. */
|
|
671
|
+
function checkAudience(a) {
|
|
672
|
+
const kind = a.ext.audience?.kind;
|
|
673
|
+
if (kind !== "private" && kind !== "public") {
|
|
674
|
+
throw new AudienceError(`Annotation ${a.id}: the Firebase adapter stores private and public notes only`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function versionBytes(items) {
|
|
678
|
+
const encoder = new TextEncoder();
|
|
679
|
+
let total = 0;
|
|
680
|
+
for (const [k, v] of Object.entries(items))
|
|
681
|
+
total += encoder.encode(k + v).length + 2;
|
|
682
|
+
return total;
|
|
683
|
+
}
|
|
684
|
+
function isDenied(err) {
|
|
685
|
+
// By its code: Firestore's own errors are not always `instanceof` the exported class.
|
|
686
|
+
return err?.code === "permission-denied";
|
|
687
|
+
}
|
|
688
|
+
function isObject(x) {
|
|
689
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
690
|
+
}
|
|
691
|
+
function isAnnotationTarget(source) {
|
|
692
|
+
if (typeof source !== "string")
|
|
693
|
+
return false;
|
|
694
|
+
return afterAnnotationPrefix(source) !== undefined;
|
|
695
|
+
}
|
|
696
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"indexes": [
|
|
3
|
+
{
|
|
4
|
+
"collectionGroup": "acornly_annotations",
|
|
5
|
+
"queryScope": "COLLECTION",
|
|
6
|
+
"fields": [
|
|
7
|
+
{ "fieldPath": "scope", "order": "ASCENDING" },
|
|
8
|
+
{ "fieldPath": "source", "order": "ASCENDING" },
|
|
9
|
+
{ "fieldPath": "audience", "order": "ASCENDING" },
|
|
10
|
+
{ "fieldPath": "modified", "order": "ASCENDING" }
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"collectionGroup": "acornly_annotations",
|
|
15
|
+
"queryScope": "COLLECTION",
|
|
16
|
+
"fields": [
|
|
17
|
+
{ "fieldPath": "scope", "order": "ASCENDING" },
|
|
18
|
+
{ "fieldPath": "source", "order": "ASCENDING" },
|
|
19
|
+
{ "fieldPath": "uid", "order": "ASCENDING" },
|
|
20
|
+
{ "fieldPath": "modified", "order": "ASCENDING" }
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"fieldOverrides": [
|
|
25
|
+
{
|
|
26
|
+
"collectionGroup": "acornly_annotations",
|
|
27
|
+
"fieldPath": "json",
|
|
28
|
+
"indexes": []
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"collectionGroup": "acornly_annotations",
|
|
32
|
+
"fieldPath": "kids",
|
|
33
|
+
"indexes": []
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"collectionGroup": "acornly_versions",
|
|
37
|
+
"fieldPath": "items",
|
|
38
|
+
"indexes": []
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
package/firestore.rules
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
rules_version = '2';
|
|
2
|
+
|
|
3
|
+
// Who may read and write Acornly notes in a site's own Firestore (@acornly/adapter-firebase).
|
|
4
|
+
// Deploy them as they are, or merge the `acornly_` matches into the site's own rules. The
|
|
5
|
+
// package's README explains each rule.
|
|
6
|
+
//
|
|
7
|
+
// acornly_annotations/{id}: one note. `json` is the annotation as the client wrote it; the
|
|
8
|
+
// other fields are what these rules check, and the adapter overrides the annotation's copy of them
|
|
9
|
+
// with these when it reads. `kids` lists the ids of the note's direct children (replies, tags,
|
|
10
|
+
// reactions), kept in step with them by the same write, so deletes can tell whether a note still
|
|
11
|
+
// has children, even ones the deleter cannot read. `kidChange` names the child the last change to
|
|
12
|
+
// `kids` added or removed.
|
|
13
|
+
// acornly_versions/{id}: the author's saved earlier versions of note `id`.
|
|
14
|
+
// acornly_scopes/{scope}: the scopes signed-in users may write in, written by the site's
|
|
15
|
+
// server or the console only. `anonymousRead: true` lets visitors who are not signed in read the
|
|
16
|
+
// public notes in that scope.
|
|
17
|
+
service cloud.firestore {
|
|
18
|
+
match /databases/{database}/documents {
|
|
19
|
+
function notePath(id) {
|
|
20
|
+
return /databases/$(database)/documents/acornly_annotations/$(id);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function scopePath(scope) {
|
|
24
|
+
return /databases/$(database)/documents/acornly_scopes/$(scope);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Signed in with an account. Firebase's anonymous sign-ins count as visitors.
|
|
28
|
+
function member() {
|
|
29
|
+
return request.auth != null
|
|
30
|
+
&& request.auth.token.firebase.sign_in_provider != 'anonymous';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isMe(uid) {
|
|
34
|
+
return request.auth != null && uid == request.auth.uid;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function canRead(n) {
|
|
38
|
+
return isMe(n.uid)
|
|
39
|
+
|| (n.audience == 'public'
|
|
40
|
+
&& (member() || get(scopePath(n.scope)).data.get('anonymousRead', false) == true));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function canWriteIn(scope) {
|
|
44
|
+
return scope == 'user:' + request.auth.uid || exists(scopePath(scope));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The fields every stored note has, with their types. Ids are limited to characters that
|
|
48
|
+
// need no escaping in a target IRI, so a child's `source` is the prefix and its parent's id.
|
|
49
|
+
function wellFormed(id, n) {
|
|
50
|
+
return id.matches('[A-Za-z0-9_~-]{1,128}')
|
|
51
|
+
&& n.keys().hasOnly(['uid', 'creatorId', 'scope', 'source', 'parent', 'threadRoot',
|
|
52
|
+
'depth', 'parentRev', 'audience', 'modified', 'rev', 'deleted', 'kids', 'kidChange',
|
|
53
|
+
'json'])
|
|
54
|
+
&& n.keys().hasAll(['uid', 'creatorId', 'scope', 'source', 'parent', 'threadRoot',
|
|
55
|
+
'depth', 'parentRev', 'audience', 'modified', 'rev', 'deleted', 'kids', 'kidChange',
|
|
56
|
+
'json'])
|
|
57
|
+
&& n.uid is string
|
|
58
|
+
&& (n.creatorId == null || n.creatorId is string)
|
|
59
|
+
&& n.scope is string && n.scope.size() <= 512 && n.scope.matches('(app|web|user):[^/]+')
|
|
60
|
+
&& n.source is string && n.source.size() > 0 && n.source.size() <= 1500
|
|
61
|
+
&& n.audience in ['private', 'public']
|
|
62
|
+
&& n.modified is string
|
|
63
|
+
&& n.modified.matches('[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}([.][0-9]{1,9})?(Z|[+-][0-9]{2}:[0-9]{2})')
|
|
64
|
+
&& n.rev is int
|
|
65
|
+
&& n.deleted is bool
|
|
66
|
+
&& n.kids is list
|
|
67
|
+
&& (n.kidChange == null || n.kidChange is string)
|
|
68
|
+
&& n.json is string && n.json.size() <= 65536
|
|
69
|
+
&& n.depth is int
|
|
70
|
+
&& (n.parentRev == null || n.parentRev is int)
|
|
71
|
+
&& ((n.parent == null && n.threadRoot == null && n.depth == 0 && n.parentRev == null
|
|
72
|
+
&& !n.source.lower().matches('(?s)urn:contexthive:annotation:.*'))
|
|
73
|
+
|| (n.parent is string && n.source == 'urn:contexthive:annotation:' + n.parent
|
|
74
|
+
&& n.threadRoot is string && n.depth >= 1));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// A new child, checked against its parent as stored: the parent exists, is not deleted and
|
|
78
|
+
// is readable by the writer, and the child carries its scope, thread, the next depth, a
|
|
79
|
+
// parent rev the parent has had, and an audience no wider than the parent's. The same write
|
|
80
|
+
// adds the child to the parent's `kids`.
|
|
81
|
+
function childFits(id, n) {
|
|
82
|
+
let parent = notePath(n.parent);
|
|
83
|
+
return exists(parent)
|
|
84
|
+
&& !get(parent).data.deleted
|
|
85
|
+
&& canRead(get(parent).data)
|
|
86
|
+
&& n.scope == get(parent).data.scope
|
|
87
|
+
&& n.threadRoot == (get(parent).data.threadRoot == null
|
|
88
|
+
? n.parent : get(parent).data.threadRoot)
|
|
89
|
+
&& n.depth == get(parent).data.depth + 1
|
|
90
|
+
&& (n.parentRev == null || (n.parentRev >= 1 && n.parentRev <= get(parent).data.rev))
|
|
91
|
+
&& (get(parent).data.audience == 'public' || n.audience == 'private')
|
|
92
|
+
&& getAfter(parent).data.kids == get(parent).data.kids.concat([id])
|
|
93
|
+
&& getAfter(parent).data.kidChange == id;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// A change to `kids` alone, made by the same write that creates or removes that child.
|
|
97
|
+
function kidsFollowChild(id) {
|
|
98
|
+
let n = request.resource.data;
|
|
99
|
+
let o = resource.data;
|
|
100
|
+
let kid = n.kidChange;
|
|
101
|
+
return n.diff(o).affectedKeys().hasOnly(['kids', 'kidChange'])
|
|
102
|
+
&& kid is string
|
|
103
|
+
&& ((n.kids == o.kids.concat([kid])
|
|
104
|
+
&& !exists(notePath(kid))
|
|
105
|
+
&& existsAfter(notePath(kid))
|
|
106
|
+
&& getAfter(notePath(kid)).data.parent == id)
|
|
107
|
+
|| (kid in o.kids
|
|
108
|
+
&& n.kids == o.kids.removeAll([kid])
|
|
109
|
+
&& exists(notePath(kid))
|
|
110
|
+
&& !existsAfter(notePath(kid))
|
|
111
|
+
&& get(notePath(kid)).data.parent == id));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The author's own edit, in a scope they may still write in: the next rev, nothing that
|
|
115
|
+
// places the note in its thread changes,
|
|
116
|
+
// a note with children keeps its author, and a child gets no wider than its parent.
|
|
117
|
+
function authorEdits(id) {
|
|
118
|
+
let n = request.resource.data;
|
|
119
|
+
let o = resource.data;
|
|
120
|
+
return isMe(o.uid)
|
|
121
|
+
&& !o.deleted
|
|
122
|
+
&& !n.deleted
|
|
123
|
+
&& wellFormed(id, n)
|
|
124
|
+
&& canWriteIn(n.scope)
|
|
125
|
+
&& n.diff(o).unchangedKeys().hasAll(['uid', 'scope', 'source', 'parent', 'threadRoot',
|
|
126
|
+
'depth', 'parentRev', 'kids', 'kidChange'])
|
|
127
|
+
&& n.rev == o.rev + 1
|
|
128
|
+
&& (o.kids.size() == 0 || n.creatorId == o.creatorId)
|
|
129
|
+
&& (n.parent == null
|
|
130
|
+
|| n.audience == o.audience
|
|
131
|
+
|| n.audience == 'private'
|
|
132
|
+
|| (exists(notePath(n.parent)) && get(notePath(n.parent)).data.audience == 'public'));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// The author deletes a note that has children: it stays as a placeholder.
|
|
136
|
+
function authorLeavesPlaceholder(id) {
|
|
137
|
+
let n = request.resource.data;
|
|
138
|
+
let o = resource.data;
|
|
139
|
+
return isMe(o.uid)
|
|
140
|
+
&& !o.deleted
|
|
141
|
+
&& n.deleted
|
|
142
|
+
&& o.kids.size() > 0
|
|
143
|
+
&& wellFormed(id, n)
|
|
144
|
+
&& n.diff(o).unchangedKeys().hasAll(['uid', 'creatorId', 'scope', 'source', 'parent',
|
|
145
|
+
'threadRoot', 'depth', 'parentRev', 'audience', 'kids', 'kidChange'])
|
|
146
|
+
&& n.rev == o.rev + 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// A note that goes is taken out of its parent's `kids` by the same write, or its parent goes
|
|
150
|
+
// with it.
|
|
151
|
+
function leavesParent(id, n) {
|
|
152
|
+
return n.parent == null
|
|
153
|
+
|| !existsAfter(notePath(n.parent))
|
|
154
|
+
|| (getAfter(notePath(n.parent)).data.kidChange == id
|
|
155
|
+
&& getAfter(notePath(n.parent)).data.kids
|
|
156
|
+
== get(notePath(n.parent)).data.kids.removeAll([id]));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
match /acornly_annotations/{id} {
|
|
160
|
+
allow get: if resource == null || canRead(resource.data);
|
|
161
|
+
allow list: if canRead(resource.data);
|
|
162
|
+
|
|
163
|
+
allow create: if member()
|
|
164
|
+
&& wellFormed(id, request.resource.data)
|
|
165
|
+
&& isMe(request.resource.data.uid)
|
|
166
|
+
&& request.resource.data.rev == 1
|
|
167
|
+
&& !request.resource.data.deleted
|
|
168
|
+
&& request.resource.data.kids.size() == 0
|
|
169
|
+
&& request.resource.data.kidChange == null
|
|
170
|
+
&& canWriteIn(request.resource.data.scope)
|
|
171
|
+
&& (request.resource.data.parent == null || childFits(id, request.resource.data));
|
|
172
|
+
|
|
173
|
+
allow update: if member()
|
|
174
|
+
&& (kidsFollowChild(id) || authorEdits(id) || authorLeavesPlaceholder(id));
|
|
175
|
+
|
|
176
|
+
// The author removes a note without children, or anyone removes a placeholder whose last
|
|
177
|
+
// child goes in the same write.
|
|
178
|
+
allow delete: if member()
|
|
179
|
+
&& leavesParent(id, resource.data)
|
|
180
|
+
&& ((isMe(resource.data.uid) && !resource.data.deleted && resource.data.kids.size() == 0)
|
|
181
|
+
|| (resource.data.deleted
|
|
182
|
+
&& (resource.data.kids.size() == 0
|
|
183
|
+
|| (resource.data.kids.size() == 1
|
|
184
|
+
&& !existsAfter(notePath(resource.data.kids[0]))))));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
match /acornly_versions/{id} {
|
|
188
|
+
allow get: if resource == null || isMe(resource.data.uid);
|
|
189
|
+
allow create, update: if member()
|
|
190
|
+
&& request.resource.data.keys().hasOnly(['uid', 'items'])
|
|
191
|
+
&& isMe(request.resource.data.uid)
|
|
192
|
+
&& request.resource.data.items is map
|
|
193
|
+
&& (resource == null || isMe(resource.data.uid))
|
|
194
|
+
&& exists(notePath(id))
|
|
195
|
+
&& isMe(get(notePath(id)).data.uid)
|
|
196
|
+
&& !get(notePath(id)).data.deleted;
|
|
197
|
+
allow delete: if resource == null || isMe(resource.data.uid);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acornly/adapter-firebase",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Firebase StorageAdapter: notes in a site's own Firestore, with the security rules to run there",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/free-solo/contexthive.git",
|
|
9
|
+
"directory": "packages/adapter-firebase"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"firestore.rules",
|
|
22
|
+
"firestore.indexes.json"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@acornly/core": "0.1.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"firebase": "^12.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.20.5",
|
|
32
|
+
"firebase": "^12.19.0",
|
|
33
|
+
"vitest": "^3.2.4"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"test:emulator": "pnpm dlx firebase-tools@15.32.1 emulators:exec --only firestore,auth --project demo-acornly 'vitest run --config test/vitest.config.ts'"
|
|
40
|
+
}
|
|
6
41
|
}
|