@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 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 rev', acceptEncoding));
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,
@@ -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 two hours.
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 our DPoP proof (2h)
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: a host handed a bearer credential to serve its own
21
- * repo could replay it against every other host in the space.
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
- /** A fetch that presents the credential with a fresh DPoP proof per request. */
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
- /** For tests: drop every cached credential and the process binding key. */
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;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAsB/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,gFAAgF;IAChF,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;CACtE;AAiDD,wEAAwE;AACxE,wBAAsB,mBAAmB,CACvC,WAAW,EAAE,WAAW,EACxB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CAiC1B;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;AAED,2EAA2E;AAC3E,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C"}
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 our DPoP proof (2h)
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: a host handed a bearer credential to serve its own
21
- * repo could replay it against every other host in the space.
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
- * The key credentials are bound to, generated once per process and never
38
- * persisted. It is registered nowhere, so a restart costs one round trip to
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
- let bindingKey = null;
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
- Authorization: `Bearer ${delegationToken}`,
93
- DPoP: proof,
77
+ ...(await spaceSigHeaders(key, `Bearer ${delegationToken}`)),
94
78
  },
95
- body: JSON.stringify({ space, dpopJkt }),
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
- const { privateJwk, publicJwk } = await getBindingKey();
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 method = (init?.method ?? 'GET').toUpperCase();
129
- const proof = await createDpopProof(privateJwk, publicJwk, method, url, credential);
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
- headers.set('Authorization', `DPoP ${credential}`);
132
- headers.set('DPoP', proof);
133
- return fetch(url, { ...init, method, headers });
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
- /** For tests: drop every cached credential and the process binding key. */
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
- bindingKey = null;
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;AAoUD,iFAAiF;AACjF,wBAAsB,aAAa,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBjH;AAkED;;;;;;;;;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"}
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"}
@@ -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 two hours and a sweep can outlive one. Re-minting costs a
132
- * round trip; treating the expiry as a failure costs the whole sync and leaves
133
- * the space looking unreadable when nothing about the membership changed.
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.rev === 'string' ? repo.rev : null });
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
@@ -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,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,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"}
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"}
@@ -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";
@@ -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
- rev: string;
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,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"}
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"}
@@ -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
- if (typeof space !== 'string' || typeof repo !== 'string' || typeof rev !== 'string')
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, rev };
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
+ }
@@ -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 two hours.
23
+ * by up to ten minutes.
24
24
  */
25
25
  import type { OAuthConfig } from '../config.ts';
26
26
  import { type SpaceCredential } from './credential.ts';
@@ -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 two hours.
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.
@@ -1 +1 @@
1
- {"version":3,"file":"xrpc.d.ts","sourceRoot":"","sources":["../src/xrpc.ts"],"names":[],"mappings":"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"}
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hatk/hatk",
3
- "version": "0.0.1-alpha.100",
3
+ "version": "0.0.1-alpha.102",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "hatk": "dist/cli.js"