@hatk/hatk 0.0.1-alpha.100 → 0.0.1-alpha.102
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/server.js +1 -1
- package/dist/spaces/blob.js +1 -1
- package/dist/spaces/credential.d.ts +25 -5
- package/dist/spaces/credential.d.ts.map +1 -1
- package/dist/spaces/credential.js +74 -38
- package/dist/spaces/engine.d.ts.map +1 -1
- package/dist/spaces/engine.js +9 -5
- package/dist/spaces/index.d.ts +2 -1
- package/dist/spaces/index.d.ts.map +1 -1
- package/dist/spaces/index.js +2 -1
- package/dist/spaces/notify.d.ts +10 -1
- package/dist/spaces/notify.d.ts.map +1 -1
- package/dist/spaces/notify.js +10 -3
- package/dist/spaces/signature.d.ts +36 -0
- package/dist/spaces/signature.d.ts.map +1 -0
- package/dist/spaces/signature.js +83 -0
- package/dist/spaces/viewer.d.ts +1 -1
- package/dist/spaces/viewer.js +1 -1
- package/dist/xrpc.d.ts +21 -0
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +12 -0
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -1252,7 +1252,7 @@ export function createHandler(config) {
|
|
|
1252
1252
|
return withCors(jsonError(404, 'This instance receives no space notices', acceptEncoding));
|
|
1253
1253
|
const notice = parseWriteNotice(await request.json().catch(() => null));
|
|
1254
1254
|
if (!notice)
|
|
1255
|
-
return withCors(jsonError(400, 'Expected space, repo and
|
|
1255
|
+
return withCors(jsonError(400, 'Expected space, repo and repoRev', acceptEncoding));
|
|
1256
1256
|
try {
|
|
1257
1257
|
await verifyNotice(request.headers.get('authorization'), {
|
|
1258
1258
|
iss: parseSpaceRef(notice.space).authority,
|
package/dist/spaces/blob.js
CHANGED
|
@@ -34,7 +34,7 @@ const SERVABLE = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/avif',
|
|
|
34
34
|
* a viewer who has just lost access still sees what they had. A minute is
|
|
35
35
|
* small against what the surrounding design already accepts: the readable set
|
|
36
36
|
* is cached for five, and the space host lets a minted credential outlive a
|
|
37
|
-
* revocation by up to
|
|
37
|
+
* revocation by up to ten minutes unless the authority revokes it early.
|
|
38
38
|
*/
|
|
39
39
|
const MAX_AGE_S = 60;
|
|
40
40
|
/**
|
|
@@ -13,12 +13,13 @@
|
|
|
13
13
|
* the reader's PDS signs a delegation token (60s, single-use, addressed to
|
|
14
14
|
* the authority's space host)
|
|
15
15
|
* the authority checks it, checks the reader against the space's policy, and
|
|
16
|
-
* returns a credential bound to the key that signed
|
|
16
|
+
* returns a credential bound to the key that signed the exchange (10 min)
|
|
17
17
|
*
|
|
18
18
|
* — and the credential is then presented to every writer's host in the space.
|
|
19
19
|
* It carries no audience, which is why it must be bound to a key rather than
|
|
20
|
-
* held as a bearer token:
|
|
21
|
-
*
|
|
20
|
+
* held as a bearer token: each request signs the credential together with the
|
|
21
|
+
* DID it is addressed to, so a host handed one to serve its own repo cannot
|
|
22
|
+
* replay it against any other account. See signature.ts.
|
|
22
23
|
*
|
|
23
24
|
* The consequence to be honest about: a space is only readable while somebody
|
|
24
25
|
* who can read it has a live session here. That is the same bound the data has
|
|
@@ -40,7 +41,10 @@ export interface SpaceCredential {
|
|
|
40
41
|
/** The account whose delegation bought it. */
|
|
41
42
|
readerDid: string;
|
|
42
43
|
expiresAt: number;
|
|
43
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* A fetch that presents the credential, signed for whoever the request is
|
|
46
|
+
* addressed to: the `repo` it names, or else the space's authority.
|
|
47
|
+
*/
|
|
44
48
|
fetch: (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
45
49
|
}
|
|
46
50
|
/** Mint a credential for `space` using `readerDid`'s stored session. */
|
|
@@ -57,6 +61,22 @@ export declare function getSpaceCredential(oauthConfig: OAuthConfig, space: stri
|
|
|
57
61
|
refresh?: boolean;
|
|
58
62
|
}): Promise<SpaceCredential>;
|
|
59
63
|
export declare function forgetSpaceCredential(space: string): void;
|
|
60
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* `readerDid`'s own credential for `space`, minted with their stored session.
|
|
66
|
+
*
|
|
67
|
+
* Throws a {@link SpaceCredentialError} when they may not read it, carrying the
|
|
68
|
+
* authority's status and code, and caches only successes — a refusal is asked
|
|
69
|
+
* again next time rather than remembered. `refresh` mints anew, for a read the
|
|
70
|
+
* host refused despite a credential that had not expired by our clock.
|
|
71
|
+
*/
|
|
72
|
+
export declare function readerSpaceCredential(oauthConfig: OAuthConfig, readerDid: string, space: string, opts?: {
|
|
73
|
+
refresh?: boolean;
|
|
74
|
+
}): Promise<SpaceCredential>;
|
|
75
|
+
/**
|
|
76
|
+
* Drop `readerDid`'s cached credential for `space`, after something that may
|
|
77
|
+
* have changed their standing in it — leaving it, say. The next read mints anew.
|
|
78
|
+
*/
|
|
79
|
+
export declare function forgetReaderSpaceCredential(readerDid: string, space: string): void;
|
|
80
|
+
/** For tests: drop every cached credential. */
|
|
61
81
|
export declare function resetSpaceCredentials(): void;
|
|
62
82
|
//# sourceMappingURL=credential.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"credential.d.ts","sourceRoot":"","sources":["../../src/spaces/credential.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"credential.d.ts","sourceRoot":"","sources":["../../src/spaces/credential.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAa/C,qBAAa,oBAAqB,SAAQ,KAAK;IAE3C,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM;gBADZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM;CAIlB;AAED,iFAAiF;AACjF,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAErD;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAEjD;AAED,MAAM,WAAW,eAAe;IAC9B,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAA;IACb,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;CACtE;AA4CD,wEAAwE;AACxE,wBAAsB,mBAAmB,CACvC,WAAW,EAAE,WAAW,EACxB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CAwC1B;AAUD;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,WAAW,EACxB,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,EAAE,EACpB,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAC/B,OAAO,CAAC,eAAe,CAAC,CAwB1B;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAEzD;AAgBD;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CACzC,WAAW,EAAE,WAAW,EACxB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAC/B,OAAO,CAAC,eAAe,CAAC,CAgB1B;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAElF;AAED,+CAA+C;AAC/C,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C"}
|
|
@@ -13,36 +13,29 @@
|
|
|
13
13
|
* the reader's PDS signs a delegation token (60s, single-use, addressed to
|
|
14
14
|
* the authority's space host)
|
|
15
15
|
* the authority checks it, checks the reader against the space's policy, and
|
|
16
|
-
* returns a credential bound to the key that signed
|
|
16
|
+
* returns a credential bound to the key that signed the exchange (10 min)
|
|
17
17
|
*
|
|
18
18
|
* — and the credential is then presented to every writer's host in the space.
|
|
19
19
|
* It carries no audience, which is why it must be bound to a key rather than
|
|
20
|
-
* held as a bearer token:
|
|
21
|
-
*
|
|
20
|
+
* held as a bearer token: each request signs the credential together with the
|
|
21
|
+
* DID it is addressed to, so a host handed one to serve its own repo cannot
|
|
22
|
+
* replay it against any other account. See signature.ts.
|
|
22
23
|
*
|
|
23
24
|
* The consequence to be honest about: a space is only readable while somebody
|
|
24
25
|
* who can read it has a live session here. That is the same bound the data has
|
|
25
26
|
* anyway — nobody outside the space's audience was ever entitled to this — but
|
|
26
27
|
* it does mean indexing lapses when the last member logs out.
|
|
27
28
|
*/
|
|
28
|
-
import { computeJwkThumbprint, generateKeyPair } from "../oauth/crypto.js";
|
|
29
|
-
import { createDpopProof } from "../oauth/dpop.js";
|
|
30
29
|
import { pdsXrpc } from "../pds-proxy.js";
|
|
31
30
|
import { emit } from "../logger.js";
|
|
32
31
|
import { spaceHostEndpoint } from "./identity.js";
|
|
32
|
+
import { generateSpaceSigKey, spaceSigHeaders } from "./signature.js";
|
|
33
33
|
import { parseSpaceRef } from "./uri.js";
|
|
34
|
-
/** Re-mint this long before the credential's own expiry rather than racing it. */
|
|
35
|
-
const RENEW_LEAD_MS = 5 * 60 * 1000;
|
|
36
34
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* mint fresh credentials and nothing else.
|
|
35
|
+
* Re-mint this long before the credential's own expiry rather than racing it.
|
|
36
|
+
* Credentials live ten minutes, so this keeps nine of them.
|
|
40
37
|
*/
|
|
41
|
-
|
|
42
|
-
function getBindingKey() {
|
|
43
|
-
bindingKey ??= generateKeyPair();
|
|
44
|
-
return bindingKey;
|
|
45
|
-
}
|
|
38
|
+
const RENEW_LEAD_MS = 60 * 1000;
|
|
46
39
|
export class SpaceCredentialError extends Error {
|
|
47
40
|
status;
|
|
48
41
|
code;
|
|
@@ -71,28 +64,19 @@ function expiryOf(jwt) {
|
|
|
71
64
|
}
|
|
72
65
|
}
|
|
73
66
|
/**
|
|
74
|
-
* Exchange a delegation token for a credential at the authority
|
|
75
|
-
*
|
|
76
|
-
* The proof sent here carries no `ath`: a delegation token is an authorization
|
|
77
|
-
* grant rather than an access token, so there is nothing to hash yet, and a
|
|
78
|
-
* server that checks will refuse a proof that includes one. `dpopJkt` is sent
|
|
79
|
-
* alongside because implementations differ on which they read — the reference
|
|
80
|
-
* reads the proof, others read the field — and they agree here by construction.
|
|
67
|
+
* Exchange a delegation token for a credential at the authority, binding it to
|
|
68
|
+
* `key` by signing the token with it.
|
|
81
69
|
*/
|
|
82
|
-
async function exchange(authorityHost, space, delegationToken) {
|
|
83
|
-
const { privateJwk, publicJwk } = await getBindingKey();
|
|
70
|
+
async function exchange(authorityHost, space, delegationToken, key) {
|
|
84
71
|
const url = `${authorityHost}/xrpc/com.atproto.space.getSpaceCredential`;
|
|
85
|
-
const proof = await createDpopProof(privateJwk, publicJwk, 'POST', url);
|
|
86
|
-
const dpopJkt = await computeJwkThumbprint(publicJwk);
|
|
87
72
|
const res = await fetch(url, {
|
|
88
73
|
method: 'POST',
|
|
89
74
|
headers: {
|
|
90
75
|
'Content-Type': 'application/json',
|
|
91
76
|
Accept: 'application/json',
|
|
92
|
-
|
|
93
|
-
DPoP: proof,
|
|
77
|
+
...(await spaceSigHeaders(key, `Bearer ${delegationToken}`)),
|
|
94
78
|
},
|
|
95
|
-
body: JSON.stringify({ space
|
|
79
|
+
body: JSON.stringify({ space }),
|
|
96
80
|
});
|
|
97
81
|
const body = (await res.json().catch(() => ({})));
|
|
98
82
|
if (!res.ok || !body.credential) {
|
|
@@ -120,17 +104,25 @@ export async function mintSpaceCredential(oauthConfig, space, readerDid) {
|
|
|
120
104
|
catch (err) {
|
|
121
105
|
throw new SpaceCredentialError(err?.status ?? 502, 'DelegationFailed', err?.message ?? 'delegation failed');
|
|
122
106
|
}
|
|
107
|
+
// A key per credential, held only as long as the credential is.
|
|
108
|
+
const key = await generateSpaceSigKey();
|
|
123
109
|
const authorityHost = await spaceHostEndpoint(ref.authority);
|
|
124
|
-
const credential = await exchange(authorityHost, space, token);
|
|
125
|
-
|
|
110
|
+
const credential = await exchange(authorityHost, space, token, key);
|
|
111
|
+
// Signatures depend only on the credential and the audience, so each is made
|
|
112
|
+
// once and reused for as long as the credential lives.
|
|
113
|
+
const signed = new Map();
|
|
126
114
|
const credentialFetch = async (input, init) => {
|
|
127
115
|
const url = typeof input === 'string' ? input : input.toString();
|
|
128
|
-
const
|
|
129
|
-
|
|
116
|
+
const audience = new URL(url).searchParams.get('repo') ?? ref.authority;
|
|
117
|
+
let sig = signed.get(audience);
|
|
118
|
+
if (!sig) {
|
|
119
|
+
sig = spaceSigHeaders(key, `Atproto-Space ${credential}`, audience);
|
|
120
|
+
signed.set(audience, sig);
|
|
121
|
+
}
|
|
130
122
|
const headers = new Headers(init?.headers);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return fetch(url, { ...init,
|
|
123
|
+
for (const [name, value] of Object.entries(await sig))
|
|
124
|
+
headers.set(name, value);
|
|
125
|
+
return fetch(url, { ...init, headers });
|
|
134
126
|
};
|
|
135
127
|
return { space, readerDid, expiresAt: expiryOf(credential), fetch: credentialFetch };
|
|
136
128
|
}
|
|
@@ -177,8 +169,52 @@ export async function getSpaceCredential(oauthConfig, space, candidates, opts =
|
|
|
177
169
|
export function forgetSpaceCredential(space) {
|
|
178
170
|
cache.delete(space);
|
|
179
171
|
}
|
|
180
|
-
|
|
172
|
+
// --- Per reader ---
|
|
173
|
+
/**
|
|
174
|
+
* Credentials minted as one particular reader, for reads made on their behalf.
|
|
175
|
+
*
|
|
176
|
+
* Kept apart from the cache above, which holds whichever session works for a
|
|
177
|
+
* space and is right for indexing it. A read made for a viewer has to be made
|
|
178
|
+
* as that viewer: the authority's answer is the authorization.
|
|
179
|
+
*/
|
|
180
|
+
const readerCache = new Map();
|
|
181
|
+
/** Pairs held at once. Past this the least recently used goes. */
|
|
182
|
+
const MAX_READER_CREDENTIALS = 5000;
|
|
183
|
+
/**
|
|
184
|
+
* `readerDid`'s own credential for `space`, minted with their stored session.
|
|
185
|
+
*
|
|
186
|
+
* Throws a {@link SpaceCredentialError} when they may not read it, carrying the
|
|
187
|
+
* authority's status and code, and caches only successes — a refusal is asked
|
|
188
|
+
* again next time rather than remembered. `refresh` mints anew, for a read the
|
|
189
|
+
* host refused despite a credential that had not expired by our clock.
|
|
190
|
+
*/
|
|
191
|
+
export async function readerSpaceCredential(oauthConfig, readerDid, space, opts = {}) {
|
|
192
|
+
const key = `${readerDid} ${space}`;
|
|
193
|
+
const cached = readerCache.get(key);
|
|
194
|
+
readerCache.delete(key);
|
|
195
|
+
if (!opts.refresh && fresh(cached)) {
|
|
196
|
+
readerCache.set(key, cached);
|
|
197
|
+
return cached;
|
|
198
|
+
}
|
|
199
|
+
const credential = await mintSpaceCredential(oauthConfig, space, readerDid);
|
|
200
|
+
readerCache.set(key, credential);
|
|
201
|
+
while (readerCache.size > MAX_READER_CREDENTIALS) {
|
|
202
|
+
const oldest = readerCache.keys().next();
|
|
203
|
+
if (oldest.done)
|
|
204
|
+
break;
|
|
205
|
+
readerCache.delete(oldest.value);
|
|
206
|
+
}
|
|
207
|
+
return credential;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Drop `readerDid`'s cached credential for `space`, after something that may
|
|
211
|
+
* have changed their standing in it — leaving it, say. The next read mints anew.
|
|
212
|
+
*/
|
|
213
|
+
export function forgetReaderSpaceCredential(readerDid, space) {
|
|
214
|
+
readerCache.delete(`${readerDid} ${space}`);
|
|
215
|
+
}
|
|
216
|
+
/** For tests: drop every cached credential. */
|
|
181
217
|
export function resetSpaceCredentials() {
|
|
182
218
|
cache.clear();
|
|
183
|
-
|
|
219
|
+
readerCache.clear();
|
|
184
220
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/spaces/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAQ/C,OAAO,EAA0D,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAE9G,OAAO,EAUL,KAAK,UAAU,EAChB,MAAM,YAAY,CAAA;AAMnB,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,WAAW,CAAA;IAClB,4EAA4E;IAC5E,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IAClB,mFAAmF;IACnF,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAaD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI,CAEnE;AAOD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAQnE;
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/spaces/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAQ/C,OAAO,EAA0D,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAE9G,OAAO,EAUL,KAAK,UAAU,EAChB,MAAM,YAAY,CAAA;AAMnB,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,WAAW,CAAA;IAClB,4EAA4E;IAC5E,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IAClB,mFAAmF;IACnF,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAaD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI,CAEnE;AAOD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAQnE;AAqUD,iFAAiF;AACjF,wBAAsB,aAAa,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBjH;AAqED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAwCrE;AAID,oDAAoD;AACpD,wBAAsB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAUnE;AAED,kEAAkE;AAClE,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/D;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAKjG;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAclD"}
|
package/dist/spaces/engine.js
CHANGED
|
@@ -128,9 +128,10 @@ function codeOf(err) {
|
|
|
128
128
|
/**
|
|
129
129
|
* Run a credentialed read, re-minting once on a 401.
|
|
130
130
|
*
|
|
131
|
-
* A credential lives
|
|
132
|
-
*
|
|
133
|
-
*
|
|
131
|
+
* A credential lives ten minutes and a sweep can outlive one; an authority can
|
|
132
|
+
* also revoke one early. Re-minting costs a round trip; treating the refusal as
|
|
133
|
+
* a failure costs the whole sync and leaves the space looking unreadable when
|
|
134
|
+
* nothing about the membership changed.
|
|
134
135
|
*/
|
|
135
136
|
async function withCredential(watch, operation) {
|
|
136
137
|
let credential = await credentialFor(watch);
|
|
@@ -430,14 +431,17 @@ async function listWriters(watch, credential) {
|
|
|
430
431
|
cursor,
|
|
431
432
|
limit: 1000,
|
|
432
433
|
});
|
|
434
|
+
// Ordered by space revision, and a repo updated mid-walk can turn up twice;
|
|
435
|
+
// the later entry is the newer state. The walk ends on an empty page, the
|
|
436
|
+
// only one without a cursor.
|
|
433
437
|
for (const repo of (out.repos ?? [])) {
|
|
434
|
-
writers.push({ did: repo.did, rev: typeof repo.
|
|
438
|
+
writers.push({ did: repo.did, rev: typeof repo.repoRev === 'string' ? repo.repoRev : null });
|
|
435
439
|
}
|
|
436
440
|
cursor = typeof out.cursor === 'string' ? out.cursor : undefined;
|
|
437
441
|
if (!cursor)
|
|
438
442
|
break;
|
|
439
443
|
}
|
|
440
|
-
return writers;
|
|
444
|
+
return [...new Map(writers.map((w) => [w.did, w])).values()];
|
|
441
445
|
}
|
|
442
446
|
/**
|
|
443
447
|
* Compare the authority's writer set against what the index holds, and close
|
package/dist/spaces/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import type { OAuthConfig, SpacesConfig } from '../config.ts';
|
|
9
9
|
import { unwatchSpace, watchSpace } from './engine.ts';
|
|
10
10
|
export { collectionsForSpaceType, reconcileAll, reconcileSpace, syncSpaceRepo, unwatchSpace, watchSpace, } from './engine.ts';
|
|
11
|
-
export { getSpaceCredential, mintSpaceCredential, isNotAuthorized, isSpaceGone } from './credential.ts';
|
|
11
|
+
export { getSpaceCredential, mintSpaceCredential, readerSpaceCredential, forgetReaderSpaceCredential, isNotAuthorized, isSpaceGone, SpaceCredentialError, type SpaceCredential, } from './credential.ts';
|
|
12
12
|
export { listSpaceRepos, listSpaceWatches, type SpaceWatch } from './store.ts';
|
|
13
13
|
export { isSpaceReadable, readableSpaces, withReadableSpaces } from './visibility.ts';
|
|
14
14
|
export { UngatedSpaceQueryError, spaceBackedCollections } from './guard.ts';
|
|
@@ -21,6 +21,7 @@ export { forgetViewerSpaces, readableSpacesFor, viewerCredential } from './viewe
|
|
|
21
21
|
export declare function serverDpopProof(method: string, url: string, accessToken?: string, nonce?: string): Promise<string>;
|
|
22
22
|
export { handleWriteNotice } from './engine.ts';
|
|
23
23
|
export { clearPendingNotices, NoticeError, parseWriteNotice, scheduleNoticeSync, verifyNotice, type WriteNotice, } from './notify.ts';
|
|
24
|
+
export { generateSpaceSigKey, spaceSigHeaders, type SpaceSigKey } from './signature.ts';
|
|
24
25
|
export * from './uri.ts';
|
|
25
26
|
export { asPreset, blobCid, resetSpaceBlobCache, type SpaceBlobPreset, spaceBlobUrl } from './blob.ts';
|
|
26
27
|
export { records, requireSpace, spaceRecords } from './records.ts';
|
|
@@ -1 +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,
|
|
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,EACL,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,EACrB,2BAA2B,EAC3B,eAAe,EACf,WAAW,EACX,oBAAoB,EACpB,KAAK,eAAe,GACrB,MAAM,iBAAiB,CAAA;AACxB,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,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAA;AACvF,cAAc,UAAU,CAAA;AACxB,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,mBAAmB,EAAE,KAAK,eAAe,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACtG,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"}
|
package/dist/spaces/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import { clearPendingNotices } from "./notify.js";
|
|
|
14
14
|
import { getServerKey } from "../oauth/db.js";
|
|
15
15
|
import { createDpopProof } from "../oauth/dpop.js";
|
|
16
16
|
export { collectionsForSpaceType, reconcileAll, reconcileSpace, syncSpaceRepo, unwatchSpace, watchSpace, } from "./engine.js";
|
|
17
|
-
export { getSpaceCredential, mintSpaceCredential, isNotAuthorized, isSpaceGone } from "./credential.js";
|
|
17
|
+
export { getSpaceCredential, mintSpaceCredential, readerSpaceCredential, forgetReaderSpaceCredential, isNotAuthorized, isSpaceGone, SpaceCredentialError, } from "./credential.js";
|
|
18
18
|
export { listSpaceRepos, listSpaceWatches } from "./store.js";
|
|
19
19
|
export { isSpaceReadable, readableSpaces, withReadableSpaces } from "./visibility.js";
|
|
20
20
|
export { UngatedSpaceQueryError, spaceBackedCollections } from "./guard.js";
|
|
@@ -32,6 +32,7 @@ export async function serverDpopProof(method, url, accessToken, nonce) {
|
|
|
32
32
|
}
|
|
33
33
|
export { handleWriteNotice } from "./engine.js";
|
|
34
34
|
export { clearPendingNotices, NoticeError, parseWriteNotice, scheduleNoticeSync, verifyNotice, } from "./notify.js";
|
|
35
|
+
export { generateSpaceSigKey, spaceSigHeaders } from "./signature.js";
|
|
35
36
|
export * from "./uri.js";
|
|
36
37
|
export { asPreset, blobCid, resetSpaceBlobCache, spaceBlobUrl } from "./blob.js";
|
|
37
38
|
export { records, requireSpace, spaceRecords } from "./records.js";
|
package/dist/spaces/notify.d.ts
CHANGED
|
@@ -42,8 +42,17 @@ export declare function verifyNotice(authorization: string | null, expected: {
|
|
|
42
42
|
export interface WriteNotice {
|
|
43
43
|
space: string;
|
|
44
44
|
repo: string;
|
|
45
|
-
|
|
45
|
+
/** The revision the repo reached. */
|
|
46
|
+
repoRev: string;
|
|
47
|
+
/** Where the authority sequenced the write across the whole space. */
|
|
48
|
+
spaceRev?: string;
|
|
46
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* A forwarded notice is `{ space, repo, repoRev, hash, spaceRev, prevSpaceRev? }`.
|
|
52
|
+
* An authority on a release before the space-revision split sends `rev` in
|
|
53
|
+
* place of `repoRev` and no `spaceRev`; that is read too, since nothing here
|
|
54
|
+
* depends on either beyond knowing which repo to look at.
|
|
55
|
+
*/
|
|
47
56
|
export declare function parseWriteNotice(body: unknown): WriteNotice | null;
|
|
48
57
|
export declare function scheduleNoticeSync(key: string, run: () => Promise<void>, delay?: number): void;
|
|
49
58
|
/** Drop every pending sync. For shutdown, and for tests. */
|
|
@@ -1 +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,
|
|
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,qCAAqC;IACrC,OAAO,EAAE,MAAM,CAAA;IACf,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,WAAW,GAAG,IAAI,CAOlE;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"}
|
package/dist/spaces/notify.js
CHANGED
|
@@ -82,15 +82,22 @@ export async function verifyNotice(authorization, expected) {
|
|
|
82
82
|
throw new NoticeError(403, 'Notice signature does not verify');
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* A forwarded notice is `{ space, repo, repoRev, hash, spaceRev, prevSpaceRev? }`.
|
|
87
|
+
* An authority on a release before the space-revision split sends `rev` in
|
|
88
|
+
* place of `repoRev` and no `spaceRev`; that is read too, since nothing here
|
|
89
|
+
* depends on either beyond knowing which repo to look at.
|
|
90
|
+
*/
|
|
85
91
|
export function parseWriteNotice(body) {
|
|
86
92
|
if (!body || typeof body !== 'object')
|
|
87
93
|
return null;
|
|
88
|
-
const { space, repo, rev } = body;
|
|
89
|
-
|
|
94
|
+
const { space, repo, repoRev, rev, spaceRev } = body;
|
|
95
|
+
const at = typeof repoRev === 'string' ? repoRev : rev;
|
|
96
|
+
if (typeof space !== 'string' || typeof repo !== 'string' || typeof at !== 'string')
|
|
90
97
|
return null;
|
|
91
98
|
if (!parseSpaceRef(space) || !repo.startsWith('did:'))
|
|
92
99
|
return null;
|
|
93
|
-
return { space, repo,
|
|
100
|
+
return { space, repo, repoRev: at, ...(typeof spaceRev === 'string' ? { spaceRev } : {}) };
|
|
94
101
|
}
|
|
95
102
|
/**
|
|
96
103
|
* Collapse a burst of notices for the same repo into one sync.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HTTP message signature a space credential travels with.
|
|
3
|
+
*
|
|
4
|
+
* RFC 9421, under the label `atproto-space`, signed ecdsa-p256-sha256 by a key
|
|
5
|
+
* made for one credential and named by its did:key. Two shapes:
|
|
6
|
+
*
|
|
7
|
+
* exchange the delegation token is signed alone, and the signature names
|
|
8
|
+
* its key — that key is what the authority binds the credential
|
|
9
|
+
* to (`cnf.kid`)
|
|
10
|
+
* use the credential is signed together with the DID the request is
|
|
11
|
+
* addressed to, so a credential lifted off one request cannot be
|
|
12
|
+
* replayed at another account by anyone without the key
|
|
13
|
+
*
|
|
14
|
+
* The signature is the raw 64-byte r || s, normalized to low-S: the reference
|
|
15
|
+
* verifier accepts either, but a stricter one (noble's default) only low-S.
|
|
16
|
+
*/
|
|
17
|
+
/** Encode base58btc. The inverse of `decodeBase58btc` in verify.ts. */
|
|
18
|
+
export declare function encodeBase58btc(bytes: Uint8Array): string;
|
|
19
|
+
export interface SpaceSigKey {
|
|
20
|
+
/** The public key as a P-256 did:key. */
|
|
21
|
+
did: string;
|
|
22
|
+
sign: (data: Uint8Array) => Promise<Uint8Array>;
|
|
23
|
+
}
|
|
24
|
+
/** A fresh P-256 key, held in memory only and never exported. */
|
|
25
|
+
export declare function generateSpaceSigKey(): Promise<SpaceSigKey>;
|
|
26
|
+
/**
|
|
27
|
+
* The bytes a signature covers: one line per covered field, then the
|
|
28
|
+
* signature parameters, joined by LF with no trailing newline.
|
|
29
|
+
*/
|
|
30
|
+
export declare function signatureBase(authorization: string, params: string, audience?: string): Uint8Array;
|
|
31
|
+
/**
|
|
32
|
+
* Authorization, audience and signature headers for a space request. Without
|
|
33
|
+
* an audience this is the exchange; with one, a use of the credential.
|
|
34
|
+
*/
|
|
35
|
+
export declare function spaceSigHeaders(key: SpaceSigKey, authorization: string, audience?: string): Promise<Record<string, string>>;
|
|
36
|
+
//# sourceMappingURL=signature.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signature.d.ts","sourceRoot":"","sources":["../../src/spaces/signature.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAQH,uEAAuE;AACvE,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAqBzD;AAED,MAAM,WAAW,WAAW;IAC1B,yCAAyC;IACzC,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,OAAO,CAAC,UAAU,CAAC,CAAA;CAChD;AAED,iEAAiE;AACjE,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,WAAW,CAAC,CAYhE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,CAKlG;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,WAAW,EAChB,aAAa,EAAE,MAAM,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAUjC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HTTP message signature a space credential travels with.
|
|
3
|
+
*
|
|
4
|
+
* RFC 9421, under the label `atproto-space`, signed ecdsa-p256-sha256 by a key
|
|
5
|
+
* made for one credential and named by its did:key. Two shapes:
|
|
6
|
+
*
|
|
7
|
+
* exchange the delegation token is signed alone, and the signature names
|
|
8
|
+
* its key — that key is what the authority binds the credential
|
|
9
|
+
* to (`cnf.kid`)
|
|
10
|
+
* use the credential is signed together with the DID the request is
|
|
11
|
+
* addressed to, so a credential lifted off one request cannot be
|
|
12
|
+
* replayed at another account by anyone without the key
|
|
13
|
+
*
|
|
14
|
+
* The signature is the raw 64-byte r || s, normalized to low-S: the reference
|
|
15
|
+
* verifier accepts either, but a stricter one (noble's default) only low-S.
|
|
16
|
+
*/
|
|
17
|
+
import { signEs256 } from "../oauth/crypto.js";
|
|
18
|
+
const LABEL = 'atproto-space';
|
|
19
|
+
const B58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
20
|
+
/** Encode base58btc. The inverse of `decodeBase58btc` in verify.ts. */
|
|
21
|
+
export function encodeBase58btc(bytes) {
|
|
22
|
+
const digits = [0];
|
|
23
|
+
for (const byte of bytes) {
|
|
24
|
+
let carry = byte;
|
|
25
|
+
for (let i = 0; i < digits.length; i++) {
|
|
26
|
+
carry += digits[i] << 8;
|
|
27
|
+
digits[i] = carry % 58;
|
|
28
|
+
carry = (carry / 58) | 0;
|
|
29
|
+
}
|
|
30
|
+
while (carry) {
|
|
31
|
+
digits.push(carry % 58);
|
|
32
|
+
carry = (carry / 58) | 0;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
let out = '';
|
|
36
|
+
for (const byte of bytes) {
|
|
37
|
+
if (byte !== 0)
|
|
38
|
+
break;
|
|
39
|
+
out += '1';
|
|
40
|
+
}
|
|
41
|
+
for (let i = digits.length - 1; i >= 0; i--)
|
|
42
|
+
out += B58[digits[i]];
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
/** A fresh P-256 key, held in memory only and never exported. */
|
|
46
|
+
export async function generateSpaceSigKey() {
|
|
47
|
+
const pair = await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign', 'verify']);
|
|
48
|
+
// 0x04 || x || y. A did:key carries the compressed point behind the
|
|
49
|
+
// p256-pub multicodec prefix (0x1200, varint 0x80 0x24).
|
|
50
|
+
const raw = new Uint8Array(await crypto.subtle.exportKey('raw', pair.publicKey));
|
|
51
|
+
const compressed = new Uint8Array(33);
|
|
52
|
+
compressed[0] = raw[64] & 1 ? 0x03 : 0x02;
|
|
53
|
+
compressed.set(raw.subarray(1, 33), 1);
|
|
54
|
+
return {
|
|
55
|
+
did: `did:key:z${encodeBase58btc(new Uint8Array([0x80, 0x24, ...compressed]))}`,
|
|
56
|
+
sign: (data) => signEs256(pair.privateKey, data),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The bytes a signature covers: one line per covered field, then the
|
|
61
|
+
* signature parameters, joined by LF with no trailing newline.
|
|
62
|
+
*/
|
|
63
|
+
export function signatureBase(authorization, params, audience) {
|
|
64
|
+
const lines = [`"authorization": ${authorization}`];
|
|
65
|
+
if (audience !== undefined)
|
|
66
|
+
lines.push(`"atproto-space-audience": ${audience}`);
|
|
67
|
+
lines.push(`"@signature-params": ${params}`);
|
|
68
|
+
return new TextEncoder().encode(lines.join('\n'));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Authorization, audience and signature headers for a space request. Without
|
|
72
|
+
* an audience this is the exchange; with one, a use of the credential.
|
|
73
|
+
*/
|
|
74
|
+
export async function spaceSigHeaders(key, authorization, audience) {
|
|
75
|
+
const params = audience === undefined ? `("authorization");keyid="${key.did}"` : '("authorization" "atproto-space-audience")';
|
|
76
|
+
const signature = await key.sign(signatureBase(authorization, params, audience));
|
|
77
|
+
return {
|
|
78
|
+
Authorization: authorization,
|
|
79
|
+
...(audience !== undefined ? { 'Atproto-Space-Audience': audience } : {}),
|
|
80
|
+
'Signature-Input': `${LABEL}=${params}`,
|
|
81
|
+
Signature: `${LABEL}=:${Buffer.from(signature).toString('base64')}:`,
|
|
82
|
+
};
|
|
83
|
+
}
|
package/dist/spaces/viewer.d.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* somebody ejected from a community may still be served that community's rows
|
|
21
21
|
* for a few minutes. The alternative is two round trips on every request, and
|
|
22
22
|
* the space host itself already lets a minted credential outlive a revocation
|
|
23
|
-
* by up to
|
|
23
|
+
* by up to ten minutes.
|
|
24
24
|
*/
|
|
25
25
|
import type { OAuthConfig } from '../config.ts';
|
|
26
26
|
import { type SpaceCredential } from './credential.ts';
|
package/dist/spaces/viewer.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* somebody ejected from a community may still be served that community's rows
|
|
21
21
|
* for a few minutes. The alternative is two round trips on every request, and
|
|
22
22
|
* the space host itself already lets a minted credential outlive a revocation
|
|
23
|
-
* by up to
|
|
23
|
+
* by up to ten minutes.
|
|
24
24
|
*/
|
|
25
25
|
import { emit } from "../logger.js";
|
|
26
26
|
import { mintSpaceCredential, isSpaceGone } from "./credential.js";
|
package/dist/xrpc.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SpaceCredential } from './spaces/credential.ts';
|
|
1
2
|
import type { BaseContext } from './hydrate.ts';
|
|
2
3
|
import type { Row, FlatRow } from './lex-types.ts';
|
|
3
4
|
import type { OAuthConfig, CdnConfig } from './config.ts';
|
|
@@ -82,6 +83,26 @@ export interface XrpcContext<P = Record<string, string>, Records extends Record<
|
|
|
82
83
|
* the granted scopes still apply. See {@link pdsXrpc}.
|
|
83
84
|
*/
|
|
84
85
|
pds: (nsid: string, options?: PdsXrpcOptions) => Promise<Record<string, unknown>>;
|
|
86
|
+
/**
|
|
87
|
+
* The viewer's own credential for a space, minted with their session and
|
|
88
|
+
* cached until shortly before it expires. Its `fetch` presents it to any
|
|
89
|
+
* host in the space, signed for the repo a request names (or the authority).
|
|
90
|
+
*
|
|
91
|
+
* For reads an app makes itself, as the viewer, that the index does not
|
|
92
|
+
* serve. Throws a `SpaceCredentialError` — `status` and `code` are the
|
|
93
|
+
* authority's — when the viewer may not read the space, and with status 401
|
|
94
|
+
* when there is no viewer. Pass `refresh` after a host refuses a credential
|
|
95
|
+
* that had not yet expired.
|
|
96
|
+
*/
|
|
97
|
+
spaceCredential: (space: string, opts?: {
|
|
98
|
+
refresh?: boolean;
|
|
99
|
+
}) => Promise<SpaceCredential>;
|
|
100
|
+
/**
|
|
101
|
+
* Drop the viewer's cached credential for a space, after something that may
|
|
102
|
+
* have changed their standing in it, such as leaving. Does nothing without a
|
|
103
|
+
* viewer.
|
|
104
|
+
*/
|
|
105
|
+
forgetSpaceCredential: (space: string) => void;
|
|
85
106
|
/**
|
|
86
107
|
* The record helpers and `pds` above, acting as another account this app
|
|
87
108
|
* holds a session for — one it obtained with {@link obtainSession}, say.
|
package/dist/xrpc.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xrpc.d.ts","sourceRoot":"","sources":["../src/xrpc.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"xrpc.d.ts","sourceRoot":"","sources":["../src/xrpc.ts"],"names":[],"mappings":"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"}
|
package/dist/xrpc.js
CHANGED
|
@@ -27,6 +27,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
29
29
|
import { guardedQuerySQL, unfilteredQuerySQL } from "./spaces/guard.js";
|
|
30
|
+
import { forgetReaderSpaceCredential, readerSpaceCredential, SpaceCredentialError, } from "./spaces/credential.js";
|
|
30
31
|
import { resolve, relative } from 'node:path';
|
|
31
32
|
import { readdirSync, statSync } from 'node:fs';
|
|
32
33
|
import { log, emit, timer } from "./logger.js";
|
|
@@ -138,6 +139,17 @@ export function buildXrpcContext(params, cursor, limit, viewer, input) {
|
|
|
138
139
|
return uri !== null;
|
|
139
140
|
},
|
|
140
141
|
...accountHelpers(viewer),
|
|
142
|
+
spaceCredential: async (space, opts) => {
|
|
143
|
+
if (!_oauthConfig)
|
|
144
|
+
throw new Error('No OAuth config — cannot read a space');
|
|
145
|
+
if (!viewer)
|
|
146
|
+
throw new SpaceCredentialError(401, 'AuthRequired', 'Authentication required to read a space');
|
|
147
|
+
return readerSpaceCredential(_oauthConfig, viewer.did, space, opts);
|
|
148
|
+
},
|
|
149
|
+
forgetSpaceCredential: (space) => {
|
|
150
|
+
if (viewer)
|
|
151
|
+
forgetReaderSpaceCredential(viewer.did, space);
|
|
152
|
+
},
|
|
141
153
|
asAccount: (did) => accountHelpers({ did }),
|
|
142
154
|
obtainSession: async (url, body, opts) => {
|
|
143
155
|
if (!_oauthConfig)
|