@hatk/hatk 0.0.1-alpha.111 → 0.0.1-alpha.112

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.
@@ -196,21 +196,39 @@ export interface ObtainedSession {
196
196
  export declare function obtainSession(config: OAuthConfig, url: string, body: Record<string, unknown>, opts?: {
197
197
  headers?: Record<string, string>;
198
198
  }): Promise<ObtainedSession>;
199
- export declare function refreshPdsSession(config: OAuthConfig, session: {
199
+ type RefreshableSession = {
200
200
  did: string;
201
201
  pds_endpoint: string;
202
202
  pds_auth_server?: string;
203
203
  pds_token_endpoint?: string;
204
+ /** The access token the caller was refused with, when it has one. */
205
+ access_token?: string;
204
206
  refresh_token: string;
205
207
  dpop_jkt: string;
206
208
  requested_scope?: string | null;
207
209
  granted_scope?: string | null;
208
- }): Promise<{
210
+ };
211
+ type RefreshedTokens = {
209
212
  accessToken: string;
210
213
  refreshToken?: string;
211
214
  expiresAt?: number;
212
- } | null>;
215
+ };
216
+ /**
217
+ * Renew a PDS session's access token, once per DID at a time.
218
+ *
219
+ * A refresh token is single-use, and the reference auth server treats a second
220
+ * use as theft: it answers `invalid_grant` and revokes the whole grant, the
221
+ * tokens just issued to the first use included. Two requests that find the
222
+ * access token expired together — an upload and the client's retry of it, say
223
+ * — would each spend the same refresh token, and the second would sign the
224
+ * user out. So a caller arriving while a refresh is in flight waits for that
225
+ * one, and a caller holding a session read before someone else's refresh
226
+ * finished is handed the tokens that refresh stored rather than replaying the
227
+ * spent one.
228
+ */
229
+ export declare function refreshPdsSession(config: OAuthConfig, session: RefreshableSession): Promise<RefreshedTokens | null>;
213
230
  export declare function authenticate(authHeader: string | null, dpopHeader: string | null, method: string, url: string): Promise<{
214
231
  did: string;
215
232
  } | null>;
233
+ export {};
216
234
  //# sourceMappingURL=server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/oauth/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/oauth/server.ts"],"names":[],"mappings":"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"}
@@ -5,7 +5,7 @@ import { initSession } from "./session.js";
5
5
  import { resolveClient, validateRedirectUri, isLoopbackClient } from "./client.js";
6
6
  import { negotiateScope } from "./service-describe.js";
7
7
  import { discoverAuthServer, resolveHandle, fetchProtectedResourceMetadata, fetchAuthServerMetadata, } from "./discovery.js";
8
- import { getServerKey, storeServerKey, storeOAuthRequest, getOAuthRequest, deleteOAuthRequest, storeAuthCode, consumeAuthCode, storeSession, deleteSession, checkAndStoreDpopJti, cleanupExpiredOAuth, storeRefreshToken, getRefreshToken, revokeRefreshToken, } from "./db.js";
8
+ import { getServerKey, storeServerKey, storeOAuthRequest, getOAuthRequest, deleteOAuthRequest, storeAuthCode, consumeAuthCode, storeSession, getSession, deleteSession, checkAndStoreDpopJti, cleanupExpiredOAuth, storeRefreshToken, getRefreshToken, revokeRefreshToken, } from "./db.js";
9
9
  import { emit } from "../logger.js";
10
10
  import { querySQL } from "../database/db.js";
11
11
  import { fireOnLoginHook } from "../hooks.js";
@@ -944,10 +944,48 @@ export async function obtainSession(config, url, body, opts = {}) {
944
944
  emit('oauth', 'session_obtained', { did, from: origin });
945
945
  return { did, response: out };
946
946
  }
947
- // --- PDS Session Refresh ---
947
+ /** The refresh in flight for each DID, which every caller arriving meanwhile shares. */
948
+ const refreshesInFlight = new Map();
949
+ /**
950
+ * Renew a PDS session's access token, once per DID at a time.
951
+ *
952
+ * A refresh token is single-use, and the reference auth server treats a second
953
+ * use as theft: it answers `invalid_grant` and revokes the whole grant, the
954
+ * tokens just issued to the first use included. Two requests that find the
955
+ * access token expired together — an upload and the client's retry of it, say
956
+ * — would each spend the same refresh token, and the second would sign the
957
+ * user out. So a caller arriving while a refresh is in flight waits for that
958
+ * one, and a caller holding a session read before someone else's refresh
959
+ * finished is handed the tokens that refresh stored rather than replaying the
960
+ * spent one.
961
+ */
948
962
  export async function refreshPdsSession(config, session) {
949
963
  if (!session.refresh_token)
950
964
  return null;
965
+ const inFlight = refreshesInFlight.get(session.did);
966
+ if (inFlight)
967
+ return inFlight;
968
+ const refresh = refreshUnlessRenewed(config, session).finally(() => refreshesInFlight.delete(session.did));
969
+ refreshesInFlight.set(session.did, refresh);
970
+ return refresh;
971
+ }
972
+ async function refreshUnlessRenewed(config, session) {
973
+ const stored = await getSession(session.did);
974
+ // A refresh that failed meanwhile ended the session; asking again would fail too.
975
+ if (!stored)
976
+ return null;
977
+ const renewed = stored.refresh_token !== session.refresh_token ||
978
+ (session.access_token !== undefined && stored.access_token !== session.access_token);
979
+ if (renewed) {
980
+ return {
981
+ accessToken: stored.access_token,
982
+ refreshToken: stored.refresh_token ?? undefined,
983
+ expiresAt: stored.token_expires_at == null ? undefined : Number(stored.token_expires_at),
984
+ };
985
+ }
986
+ return exchangeRefreshToken(config, session);
987
+ }
988
+ async function exchangeRefreshToken(config, session) {
951
989
  // Prefer the endpoint the auth server advertised when this session was
952
990
  // authorized. Sessions predating that column fall back to the auth server
953
991
  // (or, older still, the PDS) with the path this used to hardcode.
@@ -965,8 +1003,9 @@ export async function refreshPdsSession(config, session) {
965
1003
  body: body.toString(),
966
1004
  });
967
1005
  // Handle DPoP nonce retry
1006
+ let errBody = {};
968
1007
  if (!tokenRes.ok) {
969
- const errBody = await tokenRes.json().catch(() => ({}));
1008
+ errBody = await tokenRes.json().catch(() => ({}));
970
1009
  if (errBody.error === 'use_dpop_nonce') {
971
1010
  const nonce = tokenRes.headers.get('DPoP-Nonce');
972
1011
  if (nonce) {
@@ -976,14 +1015,20 @@ export async function refreshPdsSession(config, session) {
976
1015
  headers: { 'Content-Type': 'application/x-www-form-urlencoded', DPoP: retryProof },
977
1016
  body: body.toString(),
978
1017
  });
1018
+ if (!tokenRes.ok)
1019
+ errBody = await tokenRes.json().catch(() => ({}));
979
1020
  }
980
1021
  }
981
1022
  }
982
1023
  if (!tokenRes.ok) {
1024
+ // The description is what tells a replayed refresh token from a revoked
1025
+ // or expired grant; the status is 400 for all of them.
983
1026
  emit('oauth', 'pds_session_refresh_error', {
984
1027
  status: tokenRes.status,
985
1028
  did: session.did,
986
1029
  pds_endpoint: session.pds_endpoint,
1030
+ error: errBody.error,
1031
+ error_description: errBody.error_description,
987
1032
  });
988
1033
  await deleteSession(session.did);
989
1034
  return null;
@@ -1 +1 @@
1
- {"version":3,"file":"pds-proxy.d.ts","sourceRoot":"","sources":["../src/pds-proxy.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pds-proxy.d.ts","sourceRoot":"","sources":["../src/pds-proxy.ts"],"names":[],"mappings":"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"}
package/dist/pds-proxy.js CHANGED
@@ -57,14 +57,39 @@ function withheldByServer(session) {
57
57
  return count(session.granted_scope) < count(session.requested_scope);
58
58
  }
59
59
  /** End the session so the user authorizes again — unless that could not help. */
60
- async function refuseForScope(session) {
60
+ async function refuseForScope(session, pdsUrl, refusal) {
61
61
  if (withheldByServer(session))
62
62
  throw new ScopeWithheldProxyError();
63
+ emit('oauth', 'pds_session_ended', {
64
+ did: session.did,
65
+ nsid: nsidOf(pdsUrl),
66
+ status: refusal.status,
67
+ error: refusal.body.error,
68
+ message: refusal.body.message,
69
+ });
63
70
  await deleteSession(session.did);
64
71
  throw new ScopeMissingProxyError();
65
72
  }
73
+ function nsidOf(pdsUrl) {
74
+ return new URL(pdsUrl).pathname.replace(/^\/xrpc\//, '');
75
+ }
66
76
  /** Shared retry logic: DPoP nonce handling + token refresh. */
67
- async function withDpopRetry(oauthConfig, session, doFetch) {
77
+ async function withDpopRetry(oauthConfig, session, pdsUrl, doFetch) {
78
+ const result = await fetchWithDpopRetry(oauthConfig, session, pdsUrl, doFetch);
79
+ // Callers pass on only the error name; the message is what says which blob
80
+ // was missing or which record was refused.
81
+ if (!result.ok) {
82
+ emit('pds', 'refused', {
83
+ did: session.did,
84
+ nsid: nsidOf(pdsUrl),
85
+ status: result.status,
86
+ error: result.body.error,
87
+ message: result.body.message,
88
+ });
89
+ }
90
+ return result;
91
+ }
92
+ async function fetchWithDpopRetry(oauthConfig, session, pdsUrl, doFetch) {
68
93
  let accessToken = session.access_token;
69
94
  let result = await doFetch(accessToken);
70
95
  if (result.ok)
@@ -81,7 +106,7 @@ async function withDpopRetry(oauthConfig, session, doFetch) {
81
106
  }
82
107
  // Step 2: handle insufficient scope — see refuseForScope
83
108
  if (result.body.error === 'ScopeMissingError')
84
- await refuseForScope(session);
109
+ await refuseForScope(session, pdsUrl, result);
85
110
  // Step 3: handle expired PDS token — refresh and retry
86
111
  if (isTokenError(result.body.error)) {
87
112
  const refreshed = await refreshPdsSession(oauthConfig, session);
@@ -106,7 +131,7 @@ async function withDpopRetry(oauthConfig, session, doFetch) {
106
131
  // space endpoints refuse, which is indistinguishable from expiry until
107
132
  // the refresh rules expiry out.
108
133
  if (!result.ok && isTokenError(result.body.error))
109
- await refuseForScope(session);
134
+ await refuseForScope(session, pdsUrl, result);
110
135
  }
111
136
  }
112
137
  return result;
@@ -147,7 +172,7 @@ async function proxyToPds(oauthConfig, session, method, pdsUrl, body) {
147
172
  const serverKey = await getServerKey('appview-oauth-key');
148
173
  const privateJwk = JSON.parse(serverKey.privateKey);
149
174
  const publicJwk = JSON.parse(serverKey.publicKey);
150
- return withDpopRetry(oauthConfig, session, async (token, nonce) => {
175
+ return withDpopRetry(oauthConfig, session, pdsUrl, async (token, nonce) => {
151
176
  const proof = await createDpopProof(privateJwk, publicJwk, method, pdsUrl, token, nonce);
152
177
  // A GET carries no body, and sending a JSON content-type without one makes
153
178
  // some servers try to parse it. Queries reach here through pdsXrpc.
@@ -170,7 +195,7 @@ async function proxyToPdsRaw(oauthConfig, session, pdsUrl, body, contentType) {
170
195
  const serverKey = await getServerKey('appview-oauth-key');
171
196
  const privateJwk = JSON.parse(serverKey.privateKey);
172
197
  const publicJwk = JSON.parse(serverKey.publicKey);
173
- return withDpopRetry(oauthConfig, session, async (token, nonce) => {
198
+ return withDpopRetry(oauthConfig, session, pdsUrl, async (token, nonce) => {
174
199
  const proof = await createDpopProof(privateJwk, publicJwk, 'POST', pdsUrl, token, nonce);
175
200
  const res = await fetch(pdsUrl, {
176
201
  method: 'POST',
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAmFA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAuE9C;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CA2M3F;AAgDD,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,mFAAmF;IACnF,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AASD,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,KAAK,EAAE,WAAW,GAAG,IAAI,CAAA;IACzB,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACxF,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAC5D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,mFAAmF;IACnF,MAAM,CAAC,EAAE,kBAAkB,CAAA;IAC3B,+FAA+F;IAC/F,UAAU,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAA;CACrC;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA4wC5F;AAGD,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EAAE,EACrB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,KAAK,EAAE,WAAW,GAAG,IAAI,EACzB,MAAM,GAAE,MAAM,EAAO,EACrB,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,EAC5D,QAAQ,CAAC,EAAE,MAAM,IAAI,GACpB,OAAO,WAAW,EAAE,MAAM,CAG5B"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAmFA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAuE9C;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CA2M3F;AAgDD,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,mFAAmF;IACnF,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AASD,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,KAAK,EAAE,WAAW,GAAG,IAAI,CAAA;IACzB,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACxF,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAC5D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,mFAAmF;IACnF,MAAM,CAAC,EAAE,kBAAkB,CAAA;IAC3B,+FAA+F;IAC/F,UAAU,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAA;CACrC;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAsxC5F;AAGD,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EAAE,EACrB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,KAAK,EAAE,WAAW,GAAG,IAAI,EACzB,MAAM,GAAE,MAAM,EAAO,EACrB,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,EAC5D,QAAQ,CAAC,EAAE,MAAM,IAAI,GACpB,OAAO,WAAW,EAAE,MAAM,CAG5B"}
package/dist/server.js CHANGED
@@ -375,7 +375,7 @@ export function createHandler(config) {
375
375
  function invalidateAdminStats() {
376
376
  adminStats = null;
377
377
  }
378
- return async (request) => {
378
+ const route = async (request, logRequest) => {
379
379
  const url = new URL(request.url);
380
380
  const acceptEncoding = request.headers.get('accept-encoding');
381
381
  // CORS preflight
@@ -1536,9 +1536,10 @@ export function createHandler(config) {
1536
1536
  }
1537
1537
  finally {
1538
1538
  if ((isXrpc || isAdmin) && elapsed) {
1539
- emit('server', 'request', {
1539
+ logRequest({
1540
1540
  method: request.method,
1541
1541
  path: url.pathname,
1542
+ viewer: viewer?.did,
1542
1543
  duration_ms: elapsed(),
1543
1544
  collection: url.searchParams.get('collection') || undefined,
1544
1545
  query: url.searchParams.get('q') || undefined,
@@ -1547,6 +1548,15 @@ export function createHandler(config) {
1547
1548
  }
1548
1549
  }
1549
1550
  };
1551
+ // The status is only known once a route has answered, and routes answer from
1552
+ // dozens of places, so the line is written here rather than inside.
1553
+ return async (request) => {
1554
+ let fields;
1555
+ const response = await route(request, (logged) => (fields = logged));
1556
+ if (fields)
1557
+ emit('server', 'request', { ...fields, status: response.status });
1558
+ return response;
1559
+ };
1550
1560
  }
1551
1561
  // Backward-compatible wrapper
1552
1562
  export function startServer(port, collections, publicDir, oauth, admins = [], resolveViewer, onResync) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hatk/hatk",
3
- "version": "0.0.1-alpha.111",
3
+ "version": "0.0.1-alpha.112",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "hatk": "dist/cli.js"