@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,99 @@
|
|
|
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
|
+
import { querySQL, runSQL } from "../database/db.js";
|
|
11
|
+
const toWatch = (row) => ({
|
|
12
|
+
space: row.space,
|
|
13
|
+
authority: row.authority,
|
|
14
|
+
spaceType: row.space_type,
|
|
15
|
+
readerDid: row.reader_did,
|
|
16
|
+
registeredUntil: row.registered_until,
|
|
17
|
+
lastError: row.last_error,
|
|
18
|
+
});
|
|
19
|
+
const WATCH_COLS = 'space, authority, space_type, reader_did, registered_until, last_error';
|
|
20
|
+
export async function listSpaceWatches() {
|
|
21
|
+
const rows = (await querySQL(`SELECT ${WATCH_COLS} FROM _space_watch ORDER BY space`));
|
|
22
|
+
return rows.map(toWatch);
|
|
23
|
+
}
|
|
24
|
+
export async function getSpaceWatch(space) {
|
|
25
|
+
const rows = (await querySQL(`SELECT ${WATCH_COLS} FROM _space_watch WHERE space = $1`, [space]));
|
|
26
|
+
return rows[0] ? toWatch(rows[0]) : null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Start following a space, or refresh what is known about one already followed.
|
|
30
|
+
*
|
|
31
|
+
* Never clears `reader_did` or `registered_until`: a second call to watch an
|
|
32
|
+
* existing space is a re-assertion of interest, not a reset of its progress.
|
|
33
|
+
*/
|
|
34
|
+
export async function putSpaceWatch(input) {
|
|
35
|
+
const existing = await getSpaceWatch(input.space);
|
|
36
|
+
if (existing) {
|
|
37
|
+
await runSQL(`UPDATE _space_watch SET authority = $1, space_type = $2, updated_at = $3 WHERE space = $4`, [
|
|
38
|
+
input.authority,
|
|
39
|
+
input.spaceType,
|
|
40
|
+
new Date().toISOString(),
|
|
41
|
+
input.space,
|
|
42
|
+
]);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
await runSQL(`INSERT INTO _space_watch (space, authority, space_type, reader_did, registered_until, last_error, updated_at)
|
|
46
|
+
VALUES ($1, $2, $3, NULL, NULL, NULL, $4)`, [input.space, input.authority, input.spaceType, new Date().toISOString()]);
|
|
47
|
+
}
|
|
48
|
+
export async function updateSpaceWatch(space, patch) {
|
|
49
|
+
const sets = [];
|
|
50
|
+
const params = [];
|
|
51
|
+
let idx = 1;
|
|
52
|
+
if (patch.readerDid !== undefined) {
|
|
53
|
+
sets.push(`reader_did = $${idx++}`);
|
|
54
|
+
params.push(patch.readerDid);
|
|
55
|
+
}
|
|
56
|
+
if (patch.registeredUntil !== undefined) {
|
|
57
|
+
sets.push(`registered_until = $${idx++}`);
|
|
58
|
+
params.push(patch.registeredUntil);
|
|
59
|
+
}
|
|
60
|
+
if (patch.lastError !== undefined) {
|
|
61
|
+
sets.push(`last_error = $${idx++}`);
|
|
62
|
+
params.push(patch.lastError);
|
|
63
|
+
}
|
|
64
|
+
if (sets.length === 0)
|
|
65
|
+
return;
|
|
66
|
+
sets.push(`updated_at = $${idx++}`);
|
|
67
|
+
params.push(new Date().toISOString());
|
|
68
|
+
params.push(space);
|
|
69
|
+
await runSQL(`UPDATE _space_watch SET ${sets.join(', ')} WHERE space = $${idx}`, params);
|
|
70
|
+
}
|
|
71
|
+
export async function deleteSpaceWatch(space) {
|
|
72
|
+
await runSQL(`DELETE FROM _space_watch WHERE space = $1`, [space]);
|
|
73
|
+
await runSQL(`DELETE FROM _space_repos WHERE space = $1`, [space]);
|
|
74
|
+
}
|
|
75
|
+
export async function listSpaceRepos(space) {
|
|
76
|
+
const rows = (await querySQL(`SELECT space, did, pds, rev FROM _space_repos WHERE space = $1 ORDER BY did`, [
|
|
77
|
+
space,
|
|
78
|
+
]));
|
|
79
|
+
return rows;
|
|
80
|
+
}
|
|
81
|
+
export async function getSpaceRepo(space, did) {
|
|
82
|
+
const rows = (await querySQL(`SELECT space, did, pds, rev FROM _space_repos WHERE space = $1 AND did = $2`, [
|
|
83
|
+
space,
|
|
84
|
+
did,
|
|
85
|
+
]));
|
|
86
|
+
return rows[0] ?? null;
|
|
87
|
+
}
|
|
88
|
+
export async function putSpaceRepo(input) {
|
|
89
|
+
await runSQL(`INSERT OR REPLACE INTO _space_repos (space, did, pds, rev, synced_at) VALUES ($1, $2, $3, $4, $5)`, [
|
|
90
|
+
input.space,
|
|
91
|
+
input.did,
|
|
92
|
+
input.pds,
|
|
93
|
+
input.rev,
|
|
94
|
+
new Date().toISOString(),
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
export async function deleteSpaceRepo(space, did) {
|
|
98
|
+
await runSQL(`DELETE FROM _space_repos WHERE space = $1 AND did = $2`, [space, did]);
|
|
99
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AT-URIs for permissioned spaces (proposal 0016).
|
|
3
|
+
*
|
|
4
|
+
* A space is named by a three-segment ref, and a record inside one carries the
|
|
5
|
+
* writer, collection and rkey after it:
|
|
6
|
+
*
|
|
7
|
+
* at://{authority}/space/{type}/{skey} — the space
|
|
8
|
+
* at://{authority}/space/{type}/{skey}/{writer}/{collection}/{rkey} — a record in it
|
|
9
|
+
*
|
|
10
|
+
* Seven segments where an ordinary record URI has five, and `space` sitting
|
|
11
|
+
* where a collection would. Anything that reads a collection out of a URI by
|
|
12
|
+
* position has to know the difference or it reads back the literal 'space'.
|
|
13
|
+
*
|
|
14
|
+
* The authority is always a DID rather than a handle: a space's membership is
|
|
15
|
+
* keyed on DIDs, so a handle could not be resolved to a member.
|
|
16
|
+
*/
|
|
17
|
+
export interface SpaceRef {
|
|
18
|
+
/** The DID the space is anchored on, and the only party that issues credentials for it. */
|
|
19
|
+
authority: string;
|
|
20
|
+
/** The space type NSID, whose lexicon declares which collections the space holds. */
|
|
21
|
+
type: string;
|
|
22
|
+
skey: string;
|
|
23
|
+
}
|
|
24
|
+
export interface SpaceRecordRef extends SpaceRef {
|
|
25
|
+
/** The three-segment ref of the space this record lives in. */
|
|
26
|
+
space: string;
|
|
27
|
+
/** The account whose repo holds the record. Records stay with their writer. */
|
|
28
|
+
writer: string;
|
|
29
|
+
collection: string;
|
|
30
|
+
rkey: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function spaceRefUri(ref: SpaceRef): string;
|
|
33
|
+
/** Parse `at://{authority}/space/{type}/{skey}` — the space itself, not a record in it. */
|
|
34
|
+
export declare function parseSpaceRef(uri: string): SpaceRef | null;
|
|
35
|
+
/** Whether a URI names a space or anything inside one. */
|
|
36
|
+
export declare function isSpaceUri(uri: string): boolean;
|
|
37
|
+
/** `at://{authority}/space/{type}/{skey}` from its parts; `self` is the skey a singleton space uses. */
|
|
38
|
+
export declare function spaceUri(authority: string, type: string, skey?: string): string;
|
|
39
|
+
/** The record key: the last segment of either URI shape. */
|
|
40
|
+
export declare function rkeyOf(uri: string): string;
|
|
41
|
+
export declare function spaceRecordUri(space: string, writer: string, collection: string, rkey: string): string;
|
|
42
|
+
export declare function parseSpaceRecordUri(uri: string): SpaceRecordRef | null;
|
|
43
|
+
/** The space a record URI belongs to, or undefined for an ordinary repo URI. */
|
|
44
|
+
export declare function spaceFromUri(uri: string): string | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* The collection a record URI names, whichever shape it is.
|
|
47
|
+
*
|
|
48
|
+
* The one place the two URI forms are reconciled; every positional read of a
|
|
49
|
+
* collection goes through here.
|
|
50
|
+
*/
|
|
51
|
+
export declare function collectionFromRecordUri(uri: string): string | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* The audience a delegation token for this space is addressed to.
|
|
54
|
+
*
|
|
55
|
+
* An authority that publishes no `#atproto_space_host` entry is still reached
|
|
56
|
+
* at its `#atproto_pds` endpoint — the fragment names the audience, not the
|
|
57
|
+
* address.
|
|
58
|
+
*/
|
|
59
|
+
export declare function spaceHostAud(authority: string): string;
|
|
60
|
+
//# sourceMappingURL=uri.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uri.d.ts","sourceRoot":"","sources":["../../src/spaces/uri.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,MAAM,WAAW,QAAQ;IACvB,2FAA2F;IAC3F,SAAS,EAAE,MAAM,CAAA;IACjB,qFAAqF;IACrF,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,cAAe,SAAQ,QAAQ;IAC9C,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAA;IACb,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;CACb;AAOD,wBAAgB,WAAW,CAAC,GAAG,EAAE,QAAQ,GAAG,MAAM,CAEjD;AAED,2FAA2F;AAC3F,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAM1D;AAED,0DAA0D;AAC1D,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAG/C;AAED,wGAAwG;AACxG,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,SAAS,GAAG,MAAM,CAE/E;AAED,4DAA4D;AAC5D,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE1C;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtG;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CActE;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE5D;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKvE;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEtD"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AT-URIs for permissioned spaces (proposal 0016).
|
|
3
|
+
*
|
|
4
|
+
* A space is named by a three-segment ref, and a record inside one carries the
|
|
5
|
+
* writer, collection and rkey after it:
|
|
6
|
+
*
|
|
7
|
+
* at://{authority}/space/{type}/{skey} — the space
|
|
8
|
+
* at://{authority}/space/{type}/{skey}/{writer}/{collection}/{rkey} — a record in it
|
|
9
|
+
*
|
|
10
|
+
* Seven segments where an ordinary record URI has five, and `space` sitting
|
|
11
|
+
* where a collection would. Anything that reads a collection out of a URI by
|
|
12
|
+
* position has to know the difference or it reads back the literal 'space'.
|
|
13
|
+
*
|
|
14
|
+
* The authority is always a DID rather than a handle: a space's membership is
|
|
15
|
+
* keyed on DIDs, so a handle could not be resolved to a member.
|
|
16
|
+
*/
|
|
17
|
+
const SPACE_MARKER = 'space';
|
|
18
|
+
const AT_PREFIX = 'at://';
|
|
19
|
+
function segments(uri) {
|
|
20
|
+
if (!uri.startsWith(AT_PREFIX))
|
|
21
|
+
return null;
|
|
22
|
+
return uri.slice(AT_PREFIX.length).split('/');
|
|
23
|
+
}
|
|
24
|
+
export function spaceRefUri(ref) {
|
|
25
|
+
return `${AT_PREFIX}${ref.authority}/${SPACE_MARKER}/${ref.type}/${ref.skey}`;
|
|
26
|
+
}
|
|
27
|
+
/** Parse `at://{authority}/space/{type}/{skey}` — the space itself, not a record in it. */
|
|
28
|
+
export function parseSpaceRef(uri) {
|
|
29
|
+
const parts = segments(uri);
|
|
30
|
+
if (!parts || parts.length !== 4)
|
|
31
|
+
return null;
|
|
32
|
+
const [authority, marker, type, skey] = parts;
|
|
33
|
+
if (marker !== SPACE_MARKER || !authority.startsWith('did:') || !type || !skey)
|
|
34
|
+
return null;
|
|
35
|
+
return { authority, type, skey };
|
|
36
|
+
}
|
|
37
|
+
/** Whether a URI names a space or anything inside one. */
|
|
38
|
+
export function isSpaceUri(uri) {
|
|
39
|
+
const parts = segments(uri);
|
|
40
|
+
return parts != null && parts.length >= 4 && parts[1] === SPACE_MARKER && parts[0].startsWith('did:');
|
|
41
|
+
}
|
|
42
|
+
/** `at://{authority}/space/{type}/{skey}` from its parts; `self` is the skey a singleton space uses. */
|
|
43
|
+
export function spaceUri(authority, type, skey = 'self') {
|
|
44
|
+
return `${AT_PREFIX}${authority}/${SPACE_MARKER}/${type}/${skey}`;
|
|
45
|
+
}
|
|
46
|
+
/** The record key: the last segment of either URI shape. */
|
|
47
|
+
export function rkeyOf(uri) {
|
|
48
|
+
return uri.slice(uri.lastIndexOf('/') + 1);
|
|
49
|
+
}
|
|
50
|
+
export function spaceRecordUri(space, writer, collection, rkey) {
|
|
51
|
+
return `${space}/${writer}/${collection}/${rkey}`;
|
|
52
|
+
}
|
|
53
|
+
export function parseSpaceRecordUri(uri) {
|
|
54
|
+
const parts = segments(uri);
|
|
55
|
+
if (!parts || parts.length !== 7)
|
|
56
|
+
return null;
|
|
57
|
+
const [authority, marker, type, skey, writer, collection, rkey] = parts;
|
|
58
|
+
if (marker !== SPACE_MARKER || !authority.startsWith('did:') || !collection || !rkey)
|
|
59
|
+
return null;
|
|
60
|
+
return {
|
|
61
|
+
authority,
|
|
62
|
+
type,
|
|
63
|
+
skey,
|
|
64
|
+
writer,
|
|
65
|
+
collection,
|
|
66
|
+
rkey,
|
|
67
|
+
space: `${AT_PREFIX}${authority}/${SPACE_MARKER}/${type}/${skey}`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** The space a record URI belongs to, or undefined for an ordinary repo URI. */
|
|
71
|
+
export function spaceFromUri(uri) {
|
|
72
|
+
return parseSpaceRecordUri(uri)?.space ?? undefined;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The collection a record URI names, whichever shape it is.
|
|
76
|
+
*
|
|
77
|
+
* The one place the two URI forms are reconciled; every positional read of a
|
|
78
|
+
* collection goes through here.
|
|
79
|
+
*/
|
|
80
|
+
export function collectionFromRecordUri(uri) {
|
|
81
|
+
const parts = segments(uri);
|
|
82
|
+
if (!parts)
|
|
83
|
+
return undefined;
|
|
84
|
+
if (parts.length === 7 && parts[1] === SPACE_MARKER)
|
|
85
|
+
return parts[5];
|
|
86
|
+
return parts.length >= 3 ? parts[1] : undefined;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The audience a delegation token for this space is addressed to.
|
|
90
|
+
*
|
|
91
|
+
* An authority that publishes no `#atproto_space_host` entry is still reached
|
|
92
|
+
* at its `#atproto_pds` endpoint — the fragment names the audience, not the
|
|
93
|
+
* address.
|
|
94
|
+
*/
|
|
95
|
+
export function spaceHostAud(authority) {
|
|
96
|
+
return `${authority}#atproto_space_host`;
|
|
97
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifying a signature made by somebody else's atproto signing key.
|
|
3
|
+
*
|
|
4
|
+
* Everything hatk signs, it signs with ES256, because it chose its own keys and
|
|
5
|
+
* WebCrypto implements that curve. Everything it has to *verify* here was
|
|
6
|
+
* signed by an account it does not control, and most `did:plc` accounts sign
|
|
7
|
+
* with secp256k1 — a curve WebCrypto does not implement at all. So the two
|
|
8
|
+
* curves atproto allows are handled explicitly, over @noble/curves.
|
|
9
|
+
*
|
|
10
|
+
* A key arrives as a multibase string in a DID document's verification method
|
|
11
|
+
* (`publicKeyMultibase` on a `Multikey`): base58btc, prefixed with a multicodec
|
|
12
|
+
* varint naming the curve, then the compressed point. The prefix is the only
|
|
13
|
+
* thing that says which curve it is, so it is read rather than guessed.
|
|
14
|
+
*/
|
|
15
|
+
export type AtprotoCurve = 'secp256k1' | 'p256';
|
|
16
|
+
export declare class SignatureError extends Error {
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Decode base58btc. Written out rather than pulled in: it is short, it is the
|
|
20
|
+
* only encoding needed, and a wrong answer here fails closed rather than
|
|
21
|
+
* silently accepting something.
|
|
22
|
+
*/
|
|
23
|
+
export declare function decodeBase58btc(input: string): Uint8Array;
|
|
24
|
+
export interface PublicKey {
|
|
25
|
+
curve: AtprotoCurve;
|
|
26
|
+
/** The compressed point, 33 bytes. */
|
|
27
|
+
bytes: Uint8Array;
|
|
28
|
+
}
|
|
29
|
+
/** Read a `publicKeyMultibase` from a DID document's verification method. */
|
|
30
|
+
export declare function parseMultibaseKey(multibase: string): PublicKey;
|
|
31
|
+
/**
|
|
32
|
+
* Verify a raw `r || s` signature over `message`.
|
|
33
|
+
*
|
|
34
|
+
* Low-S is required — atproto mandates it and a malleable signature would let
|
|
35
|
+
* the same assertion be presented twice with different bytes — and is what
|
|
36
|
+
* @noble enforces by default.
|
|
37
|
+
*/
|
|
38
|
+
export declare function verifySignature(key: PublicKey, signature: Uint8Array, message: Uint8Array): boolean;
|
|
39
|
+
//# sourceMappingURL=verify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../src/spaces/verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAYH,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,MAAM,CAAA;AAE/C,qBAAa,cAAe,SAAQ,KAAK;CAAG;AAE5C;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAuBzD;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,YAAY,CAAA;IACnB,sCAAsC;IACtC,KAAK,EAAE,UAAU,CAAA;CAClB;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAO9D;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAUnG"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifying a signature made by somebody else's atproto signing key.
|
|
3
|
+
*
|
|
4
|
+
* Everything hatk signs, it signs with ES256, because it chose its own keys and
|
|
5
|
+
* WebCrypto implements that curve. Everything it has to *verify* here was
|
|
6
|
+
* signed by an account it does not control, and most `did:plc` accounts sign
|
|
7
|
+
* with secp256k1 — a curve WebCrypto does not implement at all. So the two
|
|
8
|
+
* curves atproto allows are handled explicitly, over @noble/curves.
|
|
9
|
+
*
|
|
10
|
+
* A key arrives as a multibase string in a DID document's verification method
|
|
11
|
+
* (`publicKeyMultibase` on a `Multikey`): base58btc, prefixed with a multicodec
|
|
12
|
+
* varint naming the curve, then the compressed point. The prefix is the only
|
|
13
|
+
* thing that says which curve it is, so it is read rather than guessed.
|
|
14
|
+
*/
|
|
15
|
+
import { p256 } from '@noble/curves/nist.js';
|
|
16
|
+
import { secp256k1 } from '@noble/curves/secp256k1.js';
|
|
17
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
18
|
+
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
19
|
+
/** Multicodec varints for the two curves atproto allows. */
|
|
20
|
+
const SECP256K1_PREFIX = [0xe7, 0x01];
|
|
21
|
+
const P256_PREFIX = [0x80, 0x24];
|
|
22
|
+
export class SignatureError extends Error {
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Decode base58btc. Written out rather than pulled in: it is short, it is the
|
|
26
|
+
* only encoding needed, and a wrong answer here fails closed rather than
|
|
27
|
+
* silently accepting something.
|
|
28
|
+
*/
|
|
29
|
+
export function decodeBase58btc(input) {
|
|
30
|
+
if (input.length === 0)
|
|
31
|
+
return new Uint8Array(0);
|
|
32
|
+
const bytes = [0];
|
|
33
|
+
for (const char of input) {
|
|
34
|
+
const value = BASE58_ALPHABET.indexOf(char);
|
|
35
|
+
if (value < 0)
|
|
36
|
+
throw new SignatureError(`Not base58btc: ${char}`);
|
|
37
|
+
let carry = value;
|
|
38
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
39
|
+
carry += bytes[i] * 58;
|
|
40
|
+
bytes[i] = carry & 0xff;
|
|
41
|
+
carry >>= 8;
|
|
42
|
+
}
|
|
43
|
+
while (carry > 0) {
|
|
44
|
+
bytes.push(carry & 0xff);
|
|
45
|
+
carry >>= 8;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Every leading '1' is a leading zero byte that the arithmetic above drops.
|
|
49
|
+
for (const char of input) {
|
|
50
|
+
if (char !== '1')
|
|
51
|
+
break;
|
|
52
|
+
bytes.push(0);
|
|
53
|
+
}
|
|
54
|
+
return new Uint8Array(bytes.reverse());
|
|
55
|
+
}
|
|
56
|
+
/** Read a `publicKeyMultibase` from a DID document's verification method. */
|
|
57
|
+
export function parseMultibaseKey(multibase) {
|
|
58
|
+
if (!multibase.startsWith('z'))
|
|
59
|
+
throw new SignatureError('Key is not base58btc multibase');
|
|
60
|
+
const decoded = decodeBase58btc(multibase.slice(1));
|
|
61
|
+
const starts = (prefix) => prefix.every((b, i) => decoded[i] === b);
|
|
62
|
+
if (starts(SECP256K1_PREFIX))
|
|
63
|
+
return { curve: 'secp256k1', bytes: decoded.slice(SECP256K1_PREFIX.length) };
|
|
64
|
+
if (starts(P256_PREFIX))
|
|
65
|
+
return { curve: 'p256', bytes: decoded.slice(P256_PREFIX.length) };
|
|
66
|
+
throw new SignatureError('Key is neither secp256k1 nor p256');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Verify a raw `r || s` signature over `message`.
|
|
70
|
+
*
|
|
71
|
+
* Low-S is required — atproto mandates it and a malleable signature would let
|
|
72
|
+
* the same assertion be presented twice with different bytes — and is what
|
|
73
|
+
* @noble enforces by default.
|
|
74
|
+
*/
|
|
75
|
+
export function verifySignature(key, signature, message) {
|
|
76
|
+
if (signature.length !== 64)
|
|
77
|
+
return false;
|
|
78
|
+
const digest = sha256(message);
|
|
79
|
+
try {
|
|
80
|
+
const curve = key.curve === 'secp256k1' ? secp256k1 : p256;
|
|
81
|
+
return curve.verify(signature, digest, key.bytes);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// A malformed point or scalar is a failed verification, not a crash.
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which spaces a given viewer may be shown.
|
|
3
|
+
*
|
|
4
|
+
* The question has exactly one authoritative answer, and hatk is not the one
|
|
5
|
+
* holding it: a space's authority decides who may read it, at the moment it
|
|
6
|
+
* mints a credential, from records and rules that live on the community's own
|
|
7
|
+
* host. Re-deriving that here — reading the access and membership records out
|
|
8
|
+
* of the index and evaluating them — would be a second implementation of
|
|
9
|
+
* somebody else's authorization, drifting from it in exactly the cases that
|
|
10
|
+
* matter. Ejected members keep their content readable in the alpha; an invites
|
|
11
|
+
* space is write-for-anyone and read-for-owner; a host may change its rules
|
|
12
|
+
* without telling us.
|
|
13
|
+
*
|
|
14
|
+
* So the check is the real thing: ask the authority to mint a credential as
|
|
15
|
+
* this viewer, and take the answer. It succeeds precisely when the viewer may
|
|
16
|
+
* read the space, because that is the same call their own browser would make.
|
|
17
|
+
*
|
|
18
|
+
* What that costs is a round trip per space, so the answer is cached — which
|
|
19
|
+
* means revocation lags by up to the cache's lifetime. Worth stating plainly:
|
|
20
|
+
* somebody ejected from a community may still be served that community's rows
|
|
21
|
+
* for a few minutes. The alternative is two round trips on every request, and
|
|
22
|
+
* the space host itself already lets a minted credential outlive a revocation
|
|
23
|
+
* by up to two hours.
|
|
24
|
+
*/
|
|
25
|
+
import type { OAuthConfig } from '../config.ts';
|
|
26
|
+
import { type SpaceCredential } from './credential.ts';
|
|
27
|
+
/**
|
|
28
|
+
* The spaces this viewer may be shown, as the scope for a request.
|
|
29
|
+
*
|
|
30
|
+
* Empty for a signed-out visitor, always: obtaining a credential begins with a
|
|
31
|
+
* delegation token from the reader's own PDS, so there is no anonymous read
|
|
32
|
+
* path into a space at all. `readableBy: ["public"]` means any authenticated
|
|
33
|
+
* reader, never anyone.
|
|
34
|
+
*/
|
|
35
|
+
export declare function readableSpacesFor(oauth: OAuthConfig | null, viewer: {
|
|
36
|
+
did: string;
|
|
37
|
+
} | null): Promise<string[]>;
|
|
38
|
+
/**
|
|
39
|
+
* This viewer's own credential for a space, or null if they may not read it.
|
|
40
|
+
*
|
|
41
|
+
* For the reads that cannot go through the index — a blob, which has no public
|
|
42
|
+
* URL by design and must be fetched from the repo that holds it with the same
|
|
43
|
+
* credential as the record naming it.
|
|
44
|
+
*/
|
|
45
|
+
export declare function viewerCredential(oauth: OAuthConfig | null, viewer: {
|
|
46
|
+
did: string;
|
|
47
|
+
} | null, space: string): Promise<SpaceCredential | null>;
|
|
48
|
+
/** Drop a viewer's cached answer — after they sign out, or to stop honouring it early. */
|
|
49
|
+
export declare function forgetViewerSpaces(viewerDid: string): void;
|
|
50
|
+
export declare function resetViewerSpaces(): void;
|
|
51
|
+
//# sourceMappingURL=viewer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"viewer.d.ts","sourceRoot":"","sources":["../../src/spaces/viewer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE/C,OAAO,EAAoC,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAsGxF;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,EAAE,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAIpH;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,WAAW,GAAG,IAAI,EACzB,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,EAC9B,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAkBjC;AAED,0FAA0F;AAC1F,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAE1D;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAGxC"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which spaces a given viewer may be shown.
|
|
3
|
+
*
|
|
4
|
+
* The question has exactly one authoritative answer, and hatk is not the one
|
|
5
|
+
* holding it: a space's authority decides who may read it, at the moment it
|
|
6
|
+
* mints a credential, from records and rules that live on the community's own
|
|
7
|
+
* host. Re-deriving that here — reading the access and membership records out
|
|
8
|
+
* of the index and evaluating them — would be a second implementation of
|
|
9
|
+
* somebody else's authorization, drifting from it in exactly the cases that
|
|
10
|
+
* matter. Ejected members keep their content readable in the alpha; an invites
|
|
11
|
+
* space is write-for-anyone and read-for-owner; a host may change its rules
|
|
12
|
+
* without telling us.
|
|
13
|
+
*
|
|
14
|
+
* So the check is the real thing: ask the authority to mint a credential as
|
|
15
|
+
* this viewer, and take the answer. It succeeds precisely when the viewer may
|
|
16
|
+
* read the space, because that is the same call their own browser would make.
|
|
17
|
+
*
|
|
18
|
+
* What that costs is a round trip per space, so the answer is cached — which
|
|
19
|
+
* means revocation lags by up to the cache's lifetime. Worth stating plainly:
|
|
20
|
+
* somebody ejected from a community may still be served that community's rows
|
|
21
|
+
* for a few minutes. The alternative is two round trips on every request, and
|
|
22
|
+
* the space host itself already lets a minted credential outlive a revocation
|
|
23
|
+
* by up to two hours.
|
|
24
|
+
*/
|
|
25
|
+
import { emit } from "../logger.js";
|
|
26
|
+
import { mintSpaceCredential, isSpaceGone } from "./credential.js";
|
|
27
|
+
import { listSpaceWatches } from "./store.js";
|
|
28
|
+
/** How long a viewer's readable set is trusted before it is checked again. */
|
|
29
|
+
const TTL_MS = 5 * 60 * 1000;
|
|
30
|
+
/** Enough for a community's worth of spaces without opening a socket per space. */
|
|
31
|
+
const CONCURRENCY = 6;
|
|
32
|
+
/**
|
|
33
|
+
* Viewers held at once. Past this the oldest entry goes — a cache that grows
|
|
34
|
+
* with every account that has ever signed in is a leak, and the cost of a miss
|
|
35
|
+
* is one round trip.
|
|
36
|
+
*/
|
|
37
|
+
const MAX_VIEWERS = 1000;
|
|
38
|
+
const cache = new Map();
|
|
39
|
+
/**
|
|
40
|
+
* Resolutions in flight, one per viewer. A page fires several requests at
|
|
41
|
+
* once and every one of them misses the same cold cache; without this each
|
|
42
|
+
* would mint its own credential per space, and a member landing on the home
|
|
43
|
+
* page cost six times what it needed to.
|
|
44
|
+
*/
|
|
45
|
+
const inFlight = new Map();
|
|
46
|
+
function entryFor(viewerDid) {
|
|
47
|
+
const entry = cache.get(viewerDid);
|
|
48
|
+
if (!entry)
|
|
49
|
+
return undefined;
|
|
50
|
+
if (entry.expiresAt <= Date.now()) {
|
|
51
|
+
cache.delete(viewerDid);
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
// Re-inserted so iteration order is least-recently-used first.
|
|
55
|
+
cache.delete(viewerDid);
|
|
56
|
+
cache.set(viewerDid, entry);
|
|
57
|
+
return entry;
|
|
58
|
+
}
|
|
59
|
+
function store(viewerDid, credentials) {
|
|
60
|
+
const entry = { expiresAt: Date.now() + TTL_MS, credentials };
|
|
61
|
+
cache.set(viewerDid, entry);
|
|
62
|
+
while (cache.size > MAX_VIEWERS) {
|
|
63
|
+
const oldest = cache.keys().next();
|
|
64
|
+
if (oldest.done)
|
|
65
|
+
break;
|
|
66
|
+
cache.delete(oldest.value);
|
|
67
|
+
}
|
|
68
|
+
return entry;
|
|
69
|
+
}
|
|
70
|
+
async function inBatches(items, size, fn) {
|
|
71
|
+
const out = [];
|
|
72
|
+
for (let i = 0; i < items.length; i += size) {
|
|
73
|
+
out.push(...(await Promise.all(items.slice(i, i + size).map(fn))));
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
function resolve(oauth, viewerDid) {
|
|
78
|
+
const pending = inFlight.get(viewerDid);
|
|
79
|
+
if (pending)
|
|
80
|
+
return pending;
|
|
81
|
+
const task = resolveNow(oauth, viewerDid).finally(() => {
|
|
82
|
+
if (inFlight.get(viewerDid) === task)
|
|
83
|
+
inFlight.delete(viewerDid);
|
|
84
|
+
});
|
|
85
|
+
inFlight.set(viewerDid, task);
|
|
86
|
+
return task;
|
|
87
|
+
}
|
|
88
|
+
async function resolveNow(oauth, viewerDid) {
|
|
89
|
+
const watches = await listSpaceWatches();
|
|
90
|
+
const credentials = new Map();
|
|
91
|
+
if (watches.length === 0)
|
|
92
|
+
return store(viewerDid, credentials);
|
|
93
|
+
const elapsed = Date.now();
|
|
94
|
+
await inBatches(watches, CONCURRENCY, async (watch) => {
|
|
95
|
+
try {
|
|
96
|
+
credentials.set(watch.space, await mintSpaceCredential(oauth, watch.space, viewerDid));
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
// Every refusal is a no, and a refusal is the ordinary shape of "not a
|
|
100
|
+
// member" rather than a fault worth reporting. A space that has been
|
|
101
|
+
// deleted is a no for everybody and the sweep will stop following it.
|
|
102
|
+
credentials.set(watch.space, null);
|
|
103
|
+
if (isSpaceGone(err))
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
const readable = [...credentials].filter(([, c]) => c !== null).length;
|
|
108
|
+
emit('spaces', 'viewer_resolved', {
|
|
109
|
+
viewer_did: viewerDid,
|
|
110
|
+
checked: watches.length,
|
|
111
|
+
readable,
|
|
112
|
+
duration_ms: Date.now() - elapsed,
|
|
113
|
+
});
|
|
114
|
+
return store(viewerDid, credentials);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The spaces this viewer may be shown, as the scope for a request.
|
|
118
|
+
*
|
|
119
|
+
* Empty for a signed-out visitor, always: obtaining a credential begins with a
|
|
120
|
+
* delegation token from the reader's own PDS, so there is no anonymous read
|
|
121
|
+
* path into a space at all. `readableBy: ["public"]` means any authenticated
|
|
122
|
+
* reader, never anyone.
|
|
123
|
+
*/
|
|
124
|
+
export async function readableSpacesFor(oauth, viewer) {
|
|
125
|
+
if (!oauth || !viewer)
|
|
126
|
+
return [];
|
|
127
|
+
const entry = entryFor(viewer.did) ?? (await resolve(oauth, viewer.did));
|
|
128
|
+
return [...entry.credentials].filter(([, c]) => c !== null).map(([space]) => space);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* This viewer's own credential for a space, or null if they may not read it.
|
|
132
|
+
*
|
|
133
|
+
* For the reads that cannot go through the index — a blob, which has no public
|
|
134
|
+
* URL by design and must be fetched from the repo that holds it with the same
|
|
135
|
+
* credential as the record naming it.
|
|
136
|
+
*/
|
|
137
|
+
export async function viewerCredential(oauth, viewer, space) {
|
|
138
|
+
if (!oauth || !viewer)
|
|
139
|
+
return null;
|
|
140
|
+
const entry = entryFor(viewer.did) ?? (await resolve(oauth, viewer.did));
|
|
141
|
+
const cached = entry.credentials.get(space);
|
|
142
|
+
if (cached !== undefined) {
|
|
143
|
+
// A credential can expire inside the entry's own lifetime; the answer to
|
|
144
|
+
// "may they read it" outlives the token that proved it.
|
|
145
|
+
if (cached === null)
|
|
146
|
+
return null;
|
|
147
|
+
if (cached.expiresAt > Date.now())
|
|
148
|
+
return cached;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const credential = await mintSpaceCredential(oauth, space, viewer.did);
|
|
152
|
+
entry.credentials.set(space, credential);
|
|
153
|
+
return credential;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
entry.credentials.set(space, null);
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** Drop a viewer's cached answer — after they sign out, or to stop honouring it early. */
|
|
161
|
+
export function forgetViewerSpaces(viewerDid) {
|
|
162
|
+
cache.delete(viewerDid);
|
|
163
|
+
}
|
|
164
|
+
export function resetViewerSpaces() {
|
|
165
|
+
cache.clear();
|
|
166
|
+
inFlight.clear();
|
|
167
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which permissioned spaces the read in flight is allowed to serve.
|
|
3
|
+
*
|
|
4
|
+
* Space records are not public data. A space's authority decides who may read
|
|
5
|
+
* it, and it decides that one caller at a time — so a row indexed out of a
|
|
6
|
+
* space carries an audience, and serving it to a request that has not proven
|
|
7
|
+
* it belongs to that audience discloses private data no matter how the query
|
|
8
|
+
* was written.
|
|
9
|
+
*
|
|
10
|
+
* hatk has no other per-row visibility concept. The takedown join is
|
|
11
|
+
* viewer-independent and `privateCollections` is all-or-nothing, so neither
|
|
12
|
+
* could carry this. The rule here is instead:
|
|
13
|
+
*
|
|
14
|
+
* a row with `space IS NULL` is public repo data and always readable;
|
|
15
|
+
* a row with a space is readable only inside a scope that names that space.
|
|
16
|
+
*
|
|
17
|
+
* The default is an empty scope, which serves no space rows at all. That is
|
|
18
|
+
* deliberate: a read path nobody remembered to scope fails closed and shows a
|
|
19
|
+
* member nothing, rather than failing open and showing a stranger everything.
|
|
20
|
+
* Every query over a record table applies `spaceFilterSql`; the scope is
|
|
21
|
+
* established per request by whoever established the viewer.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Run `fn` with exactly these spaces readable.
|
|
25
|
+
*
|
|
26
|
+
* Replaces rather than extends any enclosing scope: widening what a nested read
|
|
27
|
+
* may see is never what a caller means, and a scope that only ever narrows is
|
|
28
|
+
* one that can be reasoned about locally.
|
|
29
|
+
*/
|
|
30
|
+
export declare function withReadableSpaces<T>(spaces: Iterable<string>, fn: () => T): T;
|
|
31
|
+
/**
|
|
32
|
+
* Set the readable spaces for the rest of the current async context.
|
|
33
|
+
*
|
|
34
|
+
* `withReadableSpaces` wants a callback, and a request handler is a long body
|
|
35
|
+
* rather than something to wrap — restructuring it around a closure to hold a
|
|
36
|
+
* scope would be the tail wagging the dog. A server handler already runs in its
|
|
37
|
+
* own async context per request, which is the case `enterWith` exists for.
|
|
38
|
+
*/
|
|
39
|
+
export declare function enterReadableSpaces(spaces: Iterable<string>): void;
|
|
40
|
+
/** The spaces this read may serve. Empty outside any scope. */
|
|
41
|
+
export declare function readableSpaces(): ReadonlySet<string>;
|
|
42
|
+
/** Whether a space would be served to the read in flight. */
|
|
43
|
+
export declare function isSpaceReadable(space: string | null | undefined): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* The space gate as a SQL predicate, for a table aliased `alias` (pass '' for
|
|
46
|
+
* an unaliased table).
|
|
47
|
+
*
|
|
48
|
+
* `startIdx` is the next free `$N` placeholder; the returned `nextIdx` is the
|
|
49
|
+
* next one after this predicate's own. With nothing readable the predicate
|
|
50
|
+
* binds no parameters at all, which is the shape almost every request takes.
|
|
51
|
+
*/
|
|
52
|
+
export declare function spaceFilterSql(alias: string, startIdx: number): {
|
|
53
|
+
sql: string;
|
|
54
|
+
params: string[];
|
|
55
|
+
nextIdx: number;
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=visibility.d.ts.map
|