@hatk/hatk 0.0.1-alpha.85 → 0.0.1-alpha.87
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/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +2 -0
- package/dist/cli.js +5 -1
- package/dist/config.d.ts +47 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +13 -0
- package/dist/database/db.d.ts +11 -0
- package/dist/database/db.d.ts.map +1 -1
- package/dist/database/db.js +157 -22
- package/dist/database/index.d.ts +1 -1
- package/dist/database/index.d.ts.map +1 -1
- package/dist/database/index.js +1 -1
- package/dist/database/schema.d.ts +2 -0
- package/dist/database/schema.d.ts.map +1 -1
- package/dist/database/schema.js +40 -3
- package/dist/dev-entry.d.ts.map +1 -1
- package/dist/dev-entry.js +2 -2
- package/dist/feeds.d.ts +14 -0
- package/dist/feeds.d.ts.map +1 -1
- package/dist/feeds.js +24 -13
- package/dist/hooks.d.ts +9 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +2 -2
- package/dist/hydrate.d.ts +42 -0
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +18 -5
- package/dist/indexer.d.ts +12 -0
- package/dist/indexer.d.ts.map +1 -1
- package/dist/indexer.js +20 -0
- package/dist/lex-types.d.ts +2 -0
- package/dist/lex-types.d.ts.map +1 -1
- package/dist/main.js +26 -2
- package/dist/oauth/db.d.ts +9 -0
- package/dist/oauth/db.d.ts.map +1 -1
- package/dist/oauth/db.js +12 -0
- package/dist/private-collections.d.ts +7 -2
- package/dist/private-collections.d.ts.map +1 -1
- package/dist/private-collections.js +9 -4
- package/dist/server.d.ts +8 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +99 -10
- package/dist/spaces/blob.d.ts +51 -0
- package/dist/spaces/blob.d.ts.map +1 -0
- package/dist/spaces/blob.js +111 -0
- package/dist/spaces/credential.d.ts +62 -0
- package/dist/spaces/credential.d.ts.map +1 -0
- package/dist/spaces/credential.js +184 -0
- package/dist/spaces/engine.d.ts +97 -0
- package/dist/spaces/engine.d.ts.map +1 -0
- package/dist/spaces/engine.js +557 -0
- package/dist/spaces/guard.d.ts +41 -0
- package/dist/spaces/guard.d.ts.map +1 -0
- package/dist/spaces/guard.js +79 -0
- package/dist/spaces/identity.d.ts +37 -0
- package/dist/spaces/identity.d.ts.map +1 -0
- package/dist/spaces/identity.js +88 -0
- package/dist/spaces/index.d.ts +55 -0
- package/dist/spaces/index.d.ts.map +1 -0
- package/dist/spaces/index.js +133 -0
- package/dist/spaces/notify.d.ts +51 -0
- package/dist/spaces/notify.d.ts.map +1 -0
- package/dist/spaces/notify.js +121 -0
- package/dist/spaces/records.d.ts +37 -0
- package/dist/spaces/records.d.ts.map +1 -0
- package/dist/spaces/records.js +63 -0
- package/dist/spaces/store.d.ts +51 -0
- package/dist/spaces/store.d.ts.map +1 -0
- package/dist/spaces/store.js +99 -0
- package/dist/spaces/uri.d.ts +60 -0
- package/dist/spaces/uri.d.ts.map +1 -0
- package/dist/spaces/uri.js +97 -0
- package/dist/spaces/verify.d.ts +39 -0
- package/dist/spaces/verify.d.ts.map +1 -0
- package/dist/spaces/verify.js +87 -0
- package/dist/spaces/viewer.d.ts +51 -0
- package/dist/spaces/viewer.d.ts.map +1 -0
- package/dist/spaces/viewer.js +167 -0
- package/dist/spaces/visibility.d.ts +57 -0
- package/dist/spaces/visibility.d.ts.map +1 -0
- package/dist/spaces/visibility.js +81 -0
- package/dist/templates/feed.tpl +4 -0
- package/dist/templates/xrpc.tpl +13 -1
- package/dist/test.d.ts +2 -0
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +8 -5
- package/dist/xrpc.d.ts +19 -3
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +61 -31
- package/package.json +5 -3
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving the services a space is reached through.
|
|
3
|
+
*
|
|
4
|
+
* Two different endpoints matter, and they are usually but not always the same
|
|
5
|
+
* host:
|
|
6
|
+
*
|
|
7
|
+
* the authority's space host — issues credentials, enumerates writers
|
|
8
|
+
* each writer's own PDS — holds that writer's records and blobs
|
|
9
|
+
*
|
|
10
|
+
* A space is the aggregation of one repo per writer, each on the writer's own
|
|
11
|
+
* server, so reading one means talking to every writer's host with a credential
|
|
12
|
+
* the authority signed. There is no single server that has all of it.
|
|
13
|
+
*/
|
|
14
|
+
import { type PublicKey } from './verify.ts';
|
|
15
|
+
export declare function configureSpaceIdentity(url: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Where to send credential and writer-set requests for a space.
|
|
18
|
+
*
|
|
19
|
+
* The authority may publish a dedicated `#atproto_space_host`; one that does
|
|
20
|
+
* not is still reached at its `#atproto_pds`, which is the case for every
|
|
21
|
+
* community host built on the reference implementation.
|
|
22
|
+
*/
|
|
23
|
+
export declare function spaceHostEndpoint(authority: string): Promise<string>;
|
|
24
|
+
/** Where a writer's repo in a space lives: their own PDS, wherever that is. */
|
|
25
|
+
export declare function repoEndpoint(did: string): Promise<string>;
|
|
26
|
+
/** Drop cached DID documents. For tests, and for a host that has just moved. */
|
|
27
|
+
export declare function clearSpaceIdentityCache(): void;
|
|
28
|
+
/**
|
|
29
|
+
* An account's atproto signing key, as its DID document publishes it.
|
|
30
|
+
*
|
|
31
|
+
* Used to check that a write notice really came from the space authority it
|
|
32
|
+
* claims to. `#atproto` is the entry every atproto DID document carries;
|
|
33
|
+
* `#atproto_space` is the optional dedicated credential-signing key, preferred
|
|
34
|
+
* when an authority publishes one.
|
|
35
|
+
*/
|
|
36
|
+
export declare function atprotoSigningKey(did: string): Promise<PublicKey | null>;
|
|
37
|
+
//# sourceMappingURL=identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/spaces/identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,aAAa,CAAA;AAS/D,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAExD;AA2BD;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAK1E;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEzD;AAED,gFAAgF;AAChF,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAW9E"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving the services a space is reached through.
|
|
3
|
+
*
|
|
4
|
+
* Two different endpoints matter, and they are usually but not always the same
|
|
5
|
+
* host:
|
|
6
|
+
*
|
|
7
|
+
* the authority's space host — issues credentials, enumerates writers
|
|
8
|
+
* each writer's own PDS — holds that writer's records and blobs
|
|
9
|
+
*
|
|
10
|
+
* A space is the aggregation of one repo per writer, each on the writer's own
|
|
11
|
+
* server, so reading one means talking to every writer's host with a credential
|
|
12
|
+
* the authority signed. There is no single server that has all of it.
|
|
13
|
+
*/
|
|
14
|
+
import { pdsFor } from "../backfill.js";
|
|
15
|
+
import { parseMultibaseKey } from "./verify.js";
|
|
16
|
+
let plcUrl = 'https://plc.directory';
|
|
17
|
+
export function configureSpaceIdentity(url) {
|
|
18
|
+
plcUrl = url;
|
|
19
|
+
}
|
|
20
|
+
const docCache = new Map();
|
|
21
|
+
function fetchDidDoc(did) {
|
|
22
|
+
const cached = docCache.get(did);
|
|
23
|
+
if (cached)
|
|
24
|
+
return cached;
|
|
25
|
+
const pending = (async () => {
|
|
26
|
+
try {
|
|
27
|
+
const url = did.startsWith('did:web:')
|
|
28
|
+
? `https://${decodeURIComponent(did.slice('did:web:'.length)).replace(/:/g, '/')}/.well-known/did.json`
|
|
29
|
+
: `${plcUrl}/${did}`;
|
|
30
|
+
const res = await fetch(url);
|
|
31
|
+
if (!res.ok)
|
|
32
|
+
return null;
|
|
33
|
+
return (await res.json());
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
})();
|
|
39
|
+
docCache.set(did, pending);
|
|
40
|
+
return pending;
|
|
41
|
+
}
|
|
42
|
+
function serviceEndpoint(doc, id) {
|
|
43
|
+
return doc?.service?.find((s) => s.id === id || s.id?.endsWith(id))?.serviceEndpoint;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Where to send credential and writer-set requests for a space.
|
|
47
|
+
*
|
|
48
|
+
* The authority may publish a dedicated `#atproto_space_host`; one that does
|
|
49
|
+
* not is still reached at its `#atproto_pds`, which is the case for every
|
|
50
|
+
* community host built on the reference implementation.
|
|
51
|
+
*/
|
|
52
|
+
export async function spaceHostEndpoint(authority) {
|
|
53
|
+
const doc = await fetchDidDoc(authority);
|
|
54
|
+
const endpoint = serviceEndpoint(doc, '#atproto_space_host') ?? serviceEndpoint(doc, '#atproto_pds');
|
|
55
|
+
if (!endpoint)
|
|
56
|
+
throw new Error(`No space host or PDS endpoint in DID document for ${authority}`);
|
|
57
|
+
return endpoint;
|
|
58
|
+
}
|
|
59
|
+
/** Where a writer's repo in a space lives: their own PDS, wherever that is. */
|
|
60
|
+
export function repoEndpoint(did) {
|
|
61
|
+
return pdsFor(did);
|
|
62
|
+
}
|
|
63
|
+
/** Drop cached DID documents. For tests, and for a host that has just moved. */
|
|
64
|
+
export function clearSpaceIdentityCache() {
|
|
65
|
+
docCache.clear();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* An account's atproto signing key, as its DID document publishes it.
|
|
69
|
+
*
|
|
70
|
+
* Used to check that a write notice really came from the space authority it
|
|
71
|
+
* claims to. `#atproto` is the entry every atproto DID document carries;
|
|
72
|
+
* `#atproto_space` is the optional dedicated credential-signing key, preferred
|
|
73
|
+
* when an authority publishes one.
|
|
74
|
+
*/
|
|
75
|
+
export async function atprotoSigningKey(did) {
|
|
76
|
+
const doc = await fetchDidDoc(did);
|
|
77
|
+
const methods = doc?.verificationMethod ?? [];
|
|
78
|
+
const named = (id) => methods.find((m) => m.id === id || m.id?.endsWith(id));
|
|
79
|
+
const method = named('#atproto_space') ?? named('#atproto');
|
|
80
|
+
if (!method?.publicKeyMultibase)
|
|
81
|
+
return null;
|
|
82
|
+
try {
|
|
83
|
+
return parseMultibaseKey(method.publicKeyMultibase);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permissioned-space indexing, as a whole.
|
|
3
|
+
*
|
|
4
|
+
* Off unless `spaces` is configured. With it off nothing in here runs, no
|
|
5
|
+
* space row can be written, and the read gate serves none — which is the state
|
|
6
|
+
* every deployment that predates this is in.
|
|
7
|
+
*/
|
|
8
|
+
import type { OAuthConfig, SpacesConfig } from '../config.ts';
|
|
9
|
+
import { unwatchSpace, watchSpace } from './engine.ts';
|
|
10
|
+
export { collectionsForSpaceType, reconcileAll, reconcileSpace, syncSpaceRepo, unwatchSpace, watchSpace, } from './engine.ts';
|
|
11
|
+
export { getSpaceCredential, mintSpaceCredential, isNotAuthorized, isSpaceGone } from './credential.ts';
|
|
12
|
+
export { listSpaceRepos, listSpaceWatches, type SpaceWatch } from './store.ts';
|
|
13
|
+
export { isSpaceReadable, readableSpaces, withReadableSpaces } from './visibility.ts';
|
|
14
|
+
export { UngatedSpaceQueryError, spaceBackedCollections } from './guard.ts';
|
|
15
|
+
export { forgetViewerSpaces, readableSpacesFor, viewerCredential } from './viewer.ts';
|
|
16
|
+
/**
|
|
17
|
+
* A DPoP proof signed by this instance's own key, for an app that has to hold
|
|
18
|
+
* a DPoP-bound token itself — a community host minting one for a member to act
|
|
19
|
+
* as the community, say. The key is the one hatk already presents to PDSes.
|
|
20
|
+
*/
|
|
21
|
+
export declare function serverDpopProof(method: string, url: string, accessToken?: string, nonce?: string): Promise<string>;
|
|
22
|
+
export { handleWriteNotice } from './engine.ts';
|
|
23
|
+
export { clearPendingNotices, NoticeError, parseWriteNotice, scheduleNoticeSync, verifyNotice, type WriteNotice, } from './notify.ts';
|
|
24
|
+
export * from './uri.ts';
|
|
25
|
+
export { blobCid, spaceBlobUrl } from './blob.ts';
|
|
26
|
+
export { records, requireSpace, spaceRecords } from './records.ts';
|
|
27
|
+
export declare function spaceServiceId(): string | null;
|
|
28
|
+
/**
|
|
29
|
+
* The DID document an authority resolves to find where to deliver notices.
|
|
30
|
+
*
|
|
31
|
+
* It publishes a service entry and nothing else. A syncer signs nothing — it
|
|
32
|
+
* verifies inbound notices and presents credentials bound to an ephemeral key
|
|
33
|
+
* — so there is no verification method to publish and no key here to steal.
|
|
34
|
+
*/
|
|
35
|
+
export declare function spaceDidDocument(did: string, endpoint: string, fragment: string): Record<string, unknown>;
|
|
36
|
+
export interface StartSpacesOptions {
|
|
37
|
+
spaces: SpacesConfig;
|
|
38
|
+
oauth: OAuthConfig | null;
|
|
39
|
+
plc: string;
|
|
40
|
+
/** Every collection hatk has a table for. */
|
|
41
|
+
collections: Set<string>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Configure the engine and start the reconcile sweep.
|
|
45
|
+
*
|
|
46
|
+
* Returns without starting anything if the instance has no OAuth: reading a
|
|
47
|
+
* space begins with a delegation token from a member's own PDS, so an instance
|
|
48
|
+
* that holds no sessions has no way to obtain one and would sweep forever
|
|
49
|
+
* finding nothing.
|
|
50
|
+
*/
|
|
51
|
+
export declare function startSpaces(opts: StartSpacesOptions): void;
|
|
52
|
+
export declare function stopSpaces(): void;
|
|
53
|
+
/** Re-exported so an app's on-login hook can start following a space it just learned about. */
|
|
54
|
+
export { watchSpace as followSpace, unwatchSpace as unfollowSpace };
|
|
55
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/spaces/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAG7D,OAAO,EAA+D,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAOnH,OAAO,EACL,uBAAuB,EACvB,YAAY,EACZ,cAAc,EACd,aAAa,EACb,YAAY,EACZ,UAAU,GACX,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AACvG,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAA;AAC9E,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACrF,OAAO,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAA;AAC3E,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAErF;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,WAAW,CAAC,EAAE,MAAM,EACpB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAIjB;AACD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EACL,mBAAmB,EACnB,WAAW,EACX,gBAAgB,EAChB,kBAAkB,EAClB,YAAY,EACZ,KAAK,WAAW,GACjB,MAAM,aAAa,CAAA;AACpB,cAAc,UAAU,CAAA;AACxB,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACjD,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAWlE,wBAAgB,cAAc,IAAI,MAAM,GAAG,IAAI,CAE9C;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAYzG;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,YAAY,CAAA;IACpB,KAAK,EAAE,WAAW,GAAG,IAAI,CAAA;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,6CAA6C;IAC7C,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CACzB;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI,CAqD1D;AAED,wBAAgB,UAAU,IAAI,IAAI,CAMjC;AAED,+FAA+F;AAC/F,OAAO,EAAE,UAAU,IAAI,WAAW,EAAE,YAAY,IAAI,aAAa,EAAE,CAAA"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permissioned-space indexing, as a whole.
|
|
3
|
+
*
|
|
4
|
+
* Off unless `spaces` is configured. With it off nothing in here runs, no
|
|
5
|
+
* space row can be written, and the read gate serves none — which is the state
|
|
6
|
+
* every deployment that predates this is in.
|
|
7
|
+
*/
|
|
8
|
+
import { emit } from "../logger.js";
|
|
9
|
+
import { log } from "../logger.js";
|
|
10
|
+
import { collectionsForSpaceType, configureSpaceEngine, reconcileAll, unwatchSpace, watchSpace } from "./engine.js";
|
|
11
|
+
import { setSpaceBackedCollections } from "./guard.js";
|
|
12
|
+
import { configureSpaceIdentity } from "./identity.js";
|
|
13
|
+
import { clearPendingNotices } from "./notify.js";
|
|
14
|
+
import { getServerKey } from "../oauth/db.js";
|
|
15
|
+
import { createDpopProof } from "../oauth/dpop.js";
|
|
16
|
+
export { collectionsForSpaceType, reconcileAll, reconcileSpace, syncSpaceRepo, unwatchSpace, watchSpace, } from "./engine.js";
|
|
17
|
+
export { getSpaceCredential, mintSpaceCredential, isNotAuthorized, isSpaceGone } from "./credential.js";
|
|
18
|
+
export { listSpaceRepos, listSpaceWatches } from "./store.js";
|
|
19
|
+
export { isSpaceReadable, readableSpaces, withReadableSpaces } from "./visibility.js";
|
|
20
|
+
export { UngatedSpaceQueryError, spaceBackedCollections } from "./guard.js";
|
|
21
|
+
export { forgetViewerSpaces, readableSpacesFor, viewerCredential } from "./viewer.js";
|
|
22
|
+
/**
|
|
23
|
+
* A DPoP proof signed by this instance's own key, for an app that has to hold
|
|
24
|
+
* a DPoP-bound token itself — a community host minting one for a member to act
|
|
25
|
+
* as the community, say. The key is the one hatk already presents to PDSes.
|
|
26
|
+
*/
|
|
27
|
+
export async function serverDpopProof(method, url, accessToken, nonce) {
|
|
28
|
+
const key = await getServerKey('appview-oauth-key');
|
|
29
|
+
if (!key)
|
|
30
|
+
throw new Error('No server key: is OAuth configured?');
|
|
31
|
+
return createDpopProof(JSON.parse(key.privateKey), JSON.parse(key.publicKey), method, url, accessToken, nonce);
|
|
32
|
+
}
|
|
33
|
+
export { handleWriteNotice } from "./engine.js";
|
|
34
|
+
export { clearPendingNotices, NoticeError, parseWriteNotice, scheduleNoticeSync, verifyNotice, } from "./notify.js";
|
|
35
|
+
export * from "./uri.js";
|
|
36
|
+
export { blobCid, spaceBlobUrl } from "./blob.js";
|
|
37
|
+
export { records, requireSpace, spaceRecords } from "./records.js";
|
|
38
|
+
let sweepTimer = null;
|
|
39
|
+
/**
|
|
40
|
+
* What a notice must be addressed to for this instance to act on it, and what
|
|
41
|
+
* every registration names. Null when no service DID is configured, which is
|
|
42
|
+
* also what makes the inbound routes refuse everything.
|
|
43
|
+
*/
|
|
44
|
+
let serviceId = null;
|
|
45
|
+
export function spaceServiceId() {
|
|
46
|
+
return serviceId;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The DID document an authority resolves to find where to deliver notices.
|
|
50
|
+
*
|
|
51
|
+
* It publishes a service entry and nothing else. A syncer signs nothing — it
|
|
52
|
+
* verifies inbound notices and presents credentials bound to an ephemeral key
|
|
53
|
+
* — so there is no verification method to publish and no key here to steal.
|
|
54
|
+
*/
|
|
55
|
+
export function spaceDidDocument(did, endpoint, fragment) {
|
|
56
|
+
return {
|
|
57
|
+
'@context': ['https://www.w3.org/ns/did/v1'],
|
|
58
|
+
id: did,
|
|
59
|
+
service: [
|
|
60
|
+
{
|
|
61
|
+
id: `#${fragment}`,
|
|
62
|
+
type: 'AtprotoSpaceService',
|
|
63
|
+
serviceEndpoint: endpoint,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Configure the engine and start the reconcile sweep.
|
|
70
|
+
*
|
|
71
|
+
* Returns without starting anything if the instance has no OAuth: reading a
|
|
72
|
+
* space begins with a delegation token from a member's own PDS, so an instance
|
|
73
|
+
* that holds no sessions has no way to obtain one and would sweep forever
|
|
74
|
+
* finding nothing.
|
|
75
|
+
*/
|
|
76
|
+
export function startSpaces(opts) {
|
|
77
|
+
const { spaces, oauth, plc, collections } = opts;
|
|
78
|
+
if (!oauth) {
|
|
79
|
+
log("[spaces] configured but OAuth is not — a space is read with a member's delegation, so nothing to do");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (spaces.types.length === 0) {
|
|
83
|
+
log('[spaces] configured with no space types — nothing to index');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
configureSpaceIdentity(plc);
|
|
87
|
+
serviceId = spaces.serviceDid ? `${spaces.serviceDid}#${spaces.serviceFragment ?? 'atproto_space_syncer'}` : null;
|
|
88
|
+
configureSpaceEngine({
|
|
89
|
+
oauth,
|
|
90
|
+
types: new Set(spaces.types),
|
|
91
|
+
collections,
|
|
92
|
+
...(serviceId ? { serviceId } : {}),
|
|
93
|
+
});
|
|
94
|
+
// The tables a space row can land in, so raw SQL over one of them without
|
|
95
|
+
// the gate is refused by name rather than quietly served to everyone.
|
|
96
|
+
const backed = new Set(spaces.types.flatMap((type) => collectionsForSpaceType(type)));
|
|
97
|
+
setSpaceBackedCollections(backed);
|
|
98
|
+
if (backed.size > 0) {
|
|
99
|
+
log(`[spaces] raw SQL over ${[...backed].join(', ')} must carry ctx.spaceFilter`);
|
|
100
|
+
}
|
|
101
|
+
const intervalMs = Math.max(30, spaces.reconcileInterval ?? 300) * 1000;
|
|
102
|
+
const sweep = async () => {
|
|
103
|
+
for (const space of spaces.watch ?? []) {
|
|
104
|
+
try {
|
|
105
|
+
await watchSpace(space);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
emit('spaces', 'watch_failed', {
|
|
109
|
+
space,
|
|
110
|
+
error: err instanceof Error ? err.message : String(err),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
await reconcileAll();
|
|
115
|
+
};
|
|
116
|
+
// Deferred rather than run inline: the server is already listening by the
|
|
117
|
+
// time this is called, and a first sweep that has to reach several hosts
|
|
118
|
+
// should not sit in front of the first request.
|
|
119
|
+
setTimeout(() => void sweep().catch(() => { }), 1000).unref();
|
|
120
|
+
sweepTimer = setInterval(() => void sweep().catch(() => { }), intervalMs);
|
|
121
|
+
sweepTimer.unref();
|
|
122
|
+
log(`[spaces] indexing ${spaces.types.join(', ')} — reconciling every ${intervalMs / 1000}s`);
|
|
123
|
+
}
|
|
124
|
+
export function stopSpaces() {
|
|
125
|
+
if (sweepTimer)
|
|
126
|
+
clearInterval(sweepTimer);
|
|
127
|
+
sweepTimer = null;
|
|
128
|
+
serviceId = null;
|
|
129
|
+
setSpaceBackedCollections([]);
|
|
130
|
+
clearPendingNotices();
|
|
131
|
+
}
|
|
132
|
+
/** Re-exported so an app's on-login hook can start following a space it just learned about. */
|
|
133
|
+
export { watchSpace as followSpace, unwatchSpace as unfollowSpace };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Receiving write notices from a space authority.
|
|
3
|
+
*
|
|
4
|
+
* A space's writes never reach a firehose, so an authority tells its
|
|
5
|
+
* subscribers directly: register once, and every write into the space arrives
|
|
6
|
+
* as a `notifyWrite` carrying the repo and the revision it reached — and no
|
|
7
|
+
* records. The notice says something moved; reading it is still the syncer's
|
|
8
|
+
* job, with its own credential.
|
|
9
|
+
*
|
|
10
|
+
* This only makes sync prompt, never correct. Delivery is best-effort by
|
|
11
|
+
* design: the reference host forwards each notice once from an in-memory queue
|
|
12
|
+
* and logs the failure, so a missed one is recovered by the reconcile sweep or
|
|
13
|
+
* by the next write. Nothing here may be load-bearing.
|
|
14
|
+
*
|
|
15
|
+
* ## Why this needs its own verifier
|
|
16
|
+
*
|
|
17
|
+
* A forwarded notice does not look like the one a PDS sends its own authority.
|
|
18
|
+
* At origin, a writer's host signs with `iss` = the writer and `aud` = the bare
|
|
19
|
+
* authority DID. Forwarded onward, the authority re-signs with `iss` = itself
|
|
20
|
+
* and `aud` = the registered service identifier *including its fragment*. The
|
|
21
|
+
* reference PDS's own inbound handler rejects exactly that shape — it requires
|
|
22
|
+
* `iss` to equal the claimed writer and `aud` to equal the bare authority — so
|
|
23
|
+
* a receiver cannot borrow it and has to check the forwarded shape itself.
|
|
24
|
+
*/
|
|
25
|
+
export declare class NoticeError extends Error {
|
|
26
|
+
readonly status: number;
|
|
27
|
+
constructor(status: number, message: string);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Check a service-auth JWT that claims to come from `expectedIss`.
|
|
31
|
+
*
|
|
32
|
+
* The audience must match this instance's own service identifier exactly,
|
|
33
|
+
* fragment included: a token addressed to somebody else is not ours to act on
|
|
34
|
+
* however valid its signature. The method is checked for the same reason — a
|
|
35
|
+
* token minted for one call must not be replayable against another.
|
|
36
|
+
*/
|
|
37
|
+
export declare function verifyNotice(authorization: string | null, expected: {
|
|
38
|
+
iss: string;
|
|
39
|
+
aud: string;
|
|
40
|
+
lxm: string;
|
|
41
|
+
}): Promise<void>;
|
|
42
|
+
export interface WriteNotice {
|
|
43
|
+
space: string;
|
|
44
|
+
repo: string;
|
|
45
|
+
rev: string;
|
|
46
|
+
}
|
|
47
|
+
export declare function parseWriteNotice(body: unknown): WriteNotice | null;
|
|
48
|
+
export declare function scheduleNoticeSync(key: string, run: () => Promise<void>, delay?: number): void;
|
|
49
|
+
/** Drop every pending sync. For shutdown, and for tests. */
|
|
50
|
+
export declare function clearPendingNotices(): void;
|
|
51
|
+
//# sourceMappingURL=notify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../src/spaces/notify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAUH,qBAAa,WAAY,SAAQ,KAAK;IAElC,QAAQ,CAAC,MAAM,EAAE,MAAM;gBAAd,MAAM,EAAE,MAAM,EACvB,OAAO,EAAE,MAAM;CAIlB;AAcD;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,aAAa,EAAE,MAAM,GAAG,IAAI,EAC5B,QAAQ,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAClD,OAAO,CAAC,IAAI,CAAC,CAiCf;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,WAAW,GAAG,IAAI,CAMlE;AAcD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,SAAc,GAAG,IAAI,CASnG;AAED,4DAA4D;AAC5D,wBAAgB,mBAAmB,IAAI,IAAI,CAG1C"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Receiving write notices from a space authority.
|
|
3
|
+
*
|
|
4
|
+
* A space's writes never reach a firehose, so an authority tells its
|
|
5
|
+
* subscribers directly: register once, and every write into the space arrives
|
|
6
|
+
* as a `notifyWrite` carrying the repo and the revision it reached — and no
|
|
7
|
+
* records. The notice says something moved; reading it is still the syncer's
|
|
8
|
+
* job, with its own credential.
|
|
9
|
+
*
|
|
10
|
+
* This only makes sync prompt, never correct. Delivery is best-effort by
|
|
11
|
+
* design: the reference host forwards each notice once from an in-memory queue
|
|
12
|
+
* and logs the failure, so a missed one is recovered by the reconcile sweep or
|
|
13
|
+
* by the next write. Nothing here may be load-bearing.
|
|
14
|
+
*
|
|
15
|
+
* ## Why this needs its own verifier
|
|
16
|
+
*
|
|
17
|
+
* A forwarded notice does not look like the one a PDS sends its own authority.
|
|
18
|
+
* At origin, a writer's host signs with `iss` = the writer and `aud` = the bare
|
|
19
|
+
* authority DID. Forwarded onward, the authority re-signs with `iss` = itself
|
|
20
|
+
* and `aud` = the registered service identifier *including its fragment*. The
|
|
21
|
+
* reference PDS's own inbound handler rejects exactly that shape — it requires
|
|
22
|
+
* `iss` to equal the claimed writer and `aud` to equal the bare authority — so
|
|
23
|
+
* a receiver cannot borrow it and has to check the forwarded shape itself.
|
|
24
|
+
*/
|
|
25
|
+
import { emit } from "../logger.js";
|
|
26
|
+
import { atprotoSigningKey } from "./identity.js";
|
|
27
|
+
import { parseSpaceRef } from "./uri.js";
|
|
28
|
+
import { verifySignature } from "./verify.js";
|
|
29
|
+
/** Clock skew tolerated on a notice's expiry. The reference signs them for 60s. */
|
|
30
|
+
const SKEW_SEC = 60;
|
|
31
|
+
export class NoticeError extends Error {
|
|
32
|
+
status;
|
|
33
|
+
constructor(status, message) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.status = status;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function base64UrlToBytes(value) {
|
|
39
|
+
const padded = value.replace(/-/g, '+').replace(/_/g, '/');
|
|
40
|
+
return new Uint8Array(Buffer.from(padded, 'base64'));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Check a service-auth JWT that claims to come from `expectedIss`.
|
|
44
|
+
*
|
|
45
|
+
* The audience must match this instance's own service identifier exactly,
|
|
46
|
+
* fragment included: a token addressed to somebody else is not ours to act on
|
|
47
|
+
* however valid its signature. The method is checked for the same reason — a
|
|
48
|
+
* token minted for one call must not be replayable against another.
|
|
49
|
+
*/
|
|
50
|
+
export async function verifyNotice(authorization, expected) {
|
|
51
|
+
const token = authorization?.match(/^Bearer (.+)$/)?.[1];
|
|
52
|
+
if (!token)
|
|
53
|
+
throw new NoticeError(401, 'Missing service auth');
|
|
54
|
+
const parts = token.split('.');
|
|
55
|
+
if (parts.length !== 3)
|
|
56
|
+
throw new NoticeError(401, 'Malformed service auth');
|
|
57
|
+
let payload;
|
|
58
|
+
try {
|
|
59
|
+
payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw new NoticeError(401, 'Malformed service auth');
|
|
63
|
+
}
|
|
64
|
+
// The issuer is checked against the space named in the body rather than
|
|
65
|
+
// trusted from the token, so a valid token from one authority cannot be used
|
|
66
|
+
// to speak about another authority's space.
|
|
67
|
+
if (payload.iss?.split('#')[0] !== expected.iss) {
|
|
68
|
+
throw new NoticeError(403, 'Notice issuer is not the space authority');
|
|
69
|
+
}
|
|
70
|
+
if (payload.aud !== expected.aud)
|
|
71
|
+
throw new NoticeError(403, 'Notice is addressed elsewhere');
|
|
72
|
+
if (payload.lxm !== expected.lxm)
|
|
73
|
+
throw new NoticeError(403, 'Notice is for a different method');
|
|
74
|
+
if (typeof payload.exp !== 'number' || payload.exp + SKEW_SEC < Date.now() / 1000) {
|
|
75
|
+
throw new NoticeError(401, 'Notice has expired');
|
|
76
|
+
}
|
|
77
|
+
const key = await atprotoSigningKey(expected.iss);
|
|
78
|
+
if (!key)
|
|
79
|
+
throw new NoticeError(502, `Could not resolve a signing key for ${expected.iss}`);
|
|
80
|
+
const message = new TextEncoder().encode(`${parts[0]}.${parts[1]}`);
|
|
81
|
+
if (!verifySignature(key, base64UrlToBytes(parts[2]), message)) {
|
|
82
|
+
throw new NoticeError(403, 'Notice signature does not verify');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function parseWriteNotice(body) {
|
|
86
|
+
if (!body || typeof body !== 'object')
|
|
87
|
+
return null;
|
|
88
|
+
const { space, repo, rev } = body;
|
|
89
|
+
if (typeof space !== 'string' || typeof repo !== 'string' || typeof rev !== 'string')
|
|
90
|
+
return null;
|
|
91
|
+
if (!parseSpaceRef(space) || !repo.startsWith('did:'))
|
|
92
|
+
return null;
|
|
93
|
+
return { space, repo, rev };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Collapse a burst of notices for the same repo into one sync.
|
|
97
|
+
*
|
|
98
|
+
* A member writing a gallery produces a notice per record, and syncing per
|
|
99
|
+
* notice would read the same repo a dozen times over a few seconds. A short
|
|
100
|
+
* trailing delay turns that into one read, which is also what keeps a flood —
|
|
101
|
+
* forged or otherwise — from costing more than one sync per interval.
|
|
102
|
+
*/
|
|
103
|
+
const DEBOUNCE_MS = 750;
|
|
104
|
+
const pending = new Map();
|
|
105
|
+
export function scheduleNoticeSync(key, run, delay = DEBOUNCE_MS) {
|
|
106
|
+
const existing = pending.get(key);
|
|
107
|
+
if (existing)
|
|
108
|
+
clearTimeout(existing);
|
|
109
|
+
const timer = setTimeout(() => {
|
|
110
|
+
pending.delete(key);
|
|
111
|
+
void run().catch((err) => emit('spaces', 'notice_sync_failed', { key, error: err?.message ?? String(err) }));
|
|
112
|
+
}, delay);
|
|
113
|
+
timer.unref?.();
|
|
114
|
+
pending.set(key, timer);
|
|
115
|
+
}
|
|
116
|
+
/** Drop every pending sync. For shutdown, and for tests. */
|
|
117
|
+
export function clearPendingNotices() {
|
|
118
|
+
for (const timer of pending.values())
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
pending.clear();
|
|
121
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reads an app over permissioned spaces needs and should not have to write.
|
|
3
|
+
*
|
|
4
|
+
* Each of these is two things done in the right order — a select that carries
|
|
5
|
+
* the space gate, then resolution into properly shaped rows — and an app that
|
|
6
|
+
* writes them itself has to know both. Getting the first wrong serves a private
|
|
7
|
+
* space to whoever asks. So they are the framework's.
|
|
8
|
+
*/
|
|
9
|
+
import { getRecordsByUris, reshapeRow } from '../database/db.ts';
|
|
10
|
+
import type { Row } from '../lex-types.ts';
|
|
11
|
+
/**
|
|
12
|
+
* Every record of one collection in one space the viewer may read, oldest
|
|
13
|
+
* first.
|
|
14
|
+
*
|
|
15
|
+
* A space outside the viewer's scope matches no rows, so the result is empty
|
|
16
|
+
* rather than refused; `requireSpace` is the explicit refusal for a page that
|
|
17
|
+
* should say "members only" instead of showing nothing.
|
|
18
|
+
*/
|
|
19
|
+
export declare function spaceRecords<R = unknown>(collection: string, space: string): Promise<Row<R>[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Every record of one collection where `field` is one of `values`, oldest
|
|
22
|
+
* first — the many-rows counterpart of `ctx.lookup`, which keeps one per key.
|
|
23
|
+
*
|
|
24
|
+
* Gated like every other read: a space row appears only inside a scope that
|
|
25
|
+
* names its space, and a public row always.
|
|
26
|
+
*/
|
|
27
|
+
export declare function records<R = unknown>(collection: string, field: string, values: string[]): Promise<Row<R>[]>;
|
|
28
|
+
/**
|
|
29
|
+
* Refuse the way the space itself would.
|
|
30
|
+
*
|
|
31
|
+
* A viewer the space's authority does not admit gets `NotAuthorized`, which
|
|
32
|
+
* is the ordinary shape of "not a member" — the same answer their own browser
|
|
33
|
+
* would have had from a refused credential.
|
|
34
|
+
*/
|
|
35
|
+
export declare function requireSpace(space: string): void;
|
|
36
|
+
export { getRecordsByUris, reshapeRow };
|
|
37
|
+
//# sourceMappingURL=records.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"records.d.ts","sourceRoot":"","sources":["../../src/spaces/records.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,gBAAgB,EAAa,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAE3E,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAA;AAK1C;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAQpG;AAED;;;;;;GAMG;AACH,wBAAsB,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAejH;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAEhD;AAGD,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,CAAA"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reads an app over permissioned spaces needs and should not have to write.
|
|
3
|
+
*
|
|
4
|
+
* Each of these is two things done in the right order — a select that carries
|
|
5
|
+
* the space gate, then resolution into properly shaped rows — and an app that
|
|
6
|
+
* writes them itself has to know both. Getting the first wrong serves a private
|
|
7
|
+
* space to whoever asks. So they are the framework's.
|
|
8
|
+
*/
|
|
9
|
+
import { getRecordsByUris, getSchema, reshapeRow } from "../database/db.js";
|
|
10
|
+
import { resolveRecords } from "../hydrate.js";
|
|
11
|
+
import { InvalidRequestError } from "../xrpc.js";
|
|
12
|
+
import { spaceFilterSql, isSpaceReadable } from "./visibility.js";
|
|
13
|
+
import { guardedQuerySQL } from "./guard.js";
|
|
14
|
+
/**
|
|
15
|
+
* Every record of one collection in one space the viewer may read, oldest
|
|
16
|
+
* first.
|
|
17
|
+
*
|
|
18
|
+
* A space outside the viewer's scope matches no rows, so the result is empty
|
|
19
|
+
* rather than refused; `requireSpace` is the explicit refusal for a page that
|
|
20
|
+
* should say "members only" instead of showing nothing.
|
|
21
|
+
*/
|
|
22
|
+
export async function spaceRecords(collection, space) {
|
|
23
|
+
if (!getSchema(collection))
|
|
24
|
+
return [];
|
|
25
|
+
const gate = spaceFilterSql('t', 2);
|
|
26
|
+
const rows = (await guardedQuerySQL(`SELECT t.uri FROM "${collection}" t WHERE t.space = $1 AND ${gate.sql} ORDER BY t.indexed_at ASC`, [space, ...gate.params]));
|
|
27
|
+
return resolveRecords(rows.map((r) => r.uri));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Every record of one collection where `field` is one of `values`, oldest
|
|
31
|
+
* first — the many-rows counterpart of `ctx.lookup`, which keeps one per key.
|
|
32
|
+
*
|
|
33
|
+
* Gated like every other read: a space row appears only inside a scope that
|
|
34
|
+
* names its space, and a public row always.
|
|
35
|
+
*/
|
|
36
|
+
export async function records(collection, field, values) {
|
|
37
|
+
const schema = getSchema(collection);
|
|
38
|
+
if (!schema || values.length === 0)
|
|
39
|
+
return [];
|
|
40
|
+
const known = new Set(schema.columns.map((c) => c.name));
|
|
41
|
+
known.add('did');
|
|
42
|
+
// The field goes into SQL by name; a name the schema does not know is
|
|
43
|
+
// either a typo or an attempt, and both are refused the same way.
|
|
44
|
+
if (!known.has(field))
|
|
45
|
+
throw new InvalidRequestError(`Unknown field ${field} on ${collection}`);
|
|
46
|
+
const placeholders = values.map((_, i) => `$${i + 1}`).join(',');
|
|
47
|
+
const gate = spaceFilterSql('t', values.length + 1);
|
|
48
|
+
const rows = (await guardedQuerySQL(`SELECT t.uri FROM "${collection}" t WHERE t.${field} IN (${placeholders}) AND ${gate.sql} ORDER BY t.indexed_at ASC`, [...values, ...gate.params]));
|
|
49
|
+
return resolveRecords(rows.map((r) => r.uri));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Refuse the way the space itself would.
|
|
53
|
+
*
|
|
54
|
+
* A viewer the space's authority does not admit gets `NotAuthorized`, which
|
|
55
|
+
* is the ordinary shape of "not a member" — the same answer their own browser
|
|
56
|
+
* would have had from a refused credential.
|
|
57
|
+
*/
|
|
58
|
+
export function requireSpace(space) {
|
|
59
|
+
if (!isSpaceReadable(space))
|
|
60
|
+
throw new InvalidRequestError('Not a member of this space', 'NotAuthorized');
|
|
61
|
+
}
|
|
62
|
+
// Re-exported so a handler can shape a row the way a read shaped it.
|
|
63
|
+
export { getRecordsByUris, reshapeRow };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync state for permissioned spaces: which spaces this instance follows, and
|
|
3
|
+
* how far each writer's repo in them has been read.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately separate from `_repos`, which is firehose and backfill
|
|
6
|
+
* bookkeeping keyed by DID alone. A writer's position is per space — the same
|
|
7
|
+
* account can hold a repo in several, at different revisions — so a DID is not
|
|
8
|
+
* a key here.
|
|
9
|
+
*/
|
|
10
|
+
export interface SpaceWatch {
|
|
11
|
+
space: string;
|
|
12
|
+
authority: string;
|
|
13
|
+
spaceType: string;
|
|
14
|
+
/** The account whose session last minted a credential for this space, if any. */
|
|
15
|
+
readerDid: string | null;
|
|
16
|
+
/** Expiry of the current `registerNotify` registration, ISO 8601. */
|
|
17
|
+
registeredUntil: string | null;
|
|
18
|
+
lastError: string | null;
|
|
19
|
+
}
|
|
20
|
+
export interface SpaceRepo {
|
|
21
|
+
space: string;
|
|
22
|
+
did: string;
|
|
23
|
+
/** The PDS holding this writer's repo — their own, not the authority's. */
|
|
24
|
+
pds: string | null;
|
|
25
|
+
/** The revision this writer's repo had been read to. */
|
|
26
|
+
rev: string | null;
|
|
27
|
+
}
|
|
28
|
+
export declare function listSpaceWatches(): Promise<SpaceWatch[]>;
|
|
29
|
+
export declare function getSpaceWatch(space: string): Promise<SpaceWatch | null>;
|
|
30
|
+
/**
|
|
31
|
+
* Start following a space, or refresh what is known about one already followed.
|
|
32
|
+
*
|
|
33
|
+
* Never clears `reader_did` or `registered_until`: a second call to watch an
|
|
34
|
+
* existing space is a re-assertion of interest, not a reset of its progress.
|
|
35
|
+
*/
|
|
36
|
+
export declare function putSpaceWatch(input: {
|
|
37
|
+
space: string;
|
|
38
|
+
authority: string;
|
|
39
|
+
spaceType: string;
|
|
40
|
+
}): Promise<void>;
|
|
41
|
+
export declare function updateSpaceWatch(space: string, patch: {
|
|
42
|
+
readerDid?: string | null;
|
|
43
|
+
registeredUntil?: string | null;
|
|
44
|
+
lastError?: string | null;
|
|
45
|
+
}): Promise<void>;
|
|
46
|
+
export declare function deleteSpaceWatch(space: string): Promise<void>;
|
|
47
|
+
export declare function listSpaceRepos(space: string): Promise<SpaceRepo[]>;
|
|
48
|
+
export declare function getSpaceRepo(space: string, did: string): Promise<SpaceRepo | null>;
|
|
49
|
+
export declare function putSpaceRepo(input: SpaceRepo): Promise<void>;
|
|
50
|
+
export declare function deleteSpaceRepo(space: string, did: string): Promise<void>;
|
|
51
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/spaces/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,iFAAiF;IACjF,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,qEAAqE;IACrE,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,2EAA2E;IAC3E,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,wDAAwD;IACxD,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CACnB;AA6BD,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC,CAG9D;AAED,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAG7E;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBjH;AAED,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAC/F,OAAO,CAAC,IAAI,CAAC,CAqBf;AAED,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGnE;AAED,wBAAsB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAKxE;AAED,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAMxF;AAED,wBAAsB,YAAY,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAQlE;AAED,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/E"}
|