@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.
- package/dist/oauth/server.d.ts +21 -3
- package/dist/oauth/server.d.ts.map +1 -1
- package/dist/oauth/server.js +48 -3
- package/dist/pds-proxy.d.ts.map +1 -1
- package/dist/pds-proxy.js +31 -6
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +12 -2
- package/package.json +1 -1
package/dist/oauth/server.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
}
|
|
210
|
+
};
|
|
211
|
+
type RefreshedTokens = {
|
|
209
212
|
accessToken: string;
|
|
210
213
|
refreshToken?: string;
|
|
211
214
|
expiresAt?: number;
|
|
212
|
-
}
|
|
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":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/oauth/server.ts"],"names":[],"mappings":"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"}
|
package/dist/oauth/server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/pds-proxy.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pds-proxy.d.ts","sourceRoot":"","sources":["../src/pds-proxy.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAU9C,qBAAa,UAAW,SAAQ,KAAK;IAE1B,MAAM,EAAE,MAAM;gBAAd,MAAM,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM;CAIlB;AAED,qBAAa,sBAAuB,SAAQ,UAAU;;CAIrD;AAED;;;;;;GAMG;AACH,qBAAa,uBAAwB,SAAQ,UAAU;;CAItD;
|
|
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',
|
package/dist/server.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|