@hatk/hatk 0.0.1-alpha.45 → 0.0.1-alpha.47

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.
@@ -61,7 +61,7 @@ export class SQLiteSearchPort {
61
61
  await this.port.execute(`INSERT INTO ${shadowTable}_fts(${shadowTable}_fts, rowid, uri, ${colList}) VALUES('delete', $1, $2, ${placeholders.join(', ')})`, [old.rowid, uri, ...searchColumns.map((c) => old[c] ?? '')]);
62
62
  }
63
63
  async search(shadowTable, query, _searchColumns, limit, offset) {
64
- const escaped = query.replace(/['"*(){}[\]^~\\:]/g, ' ').trim();
64
+ const escaped = query.replace(/['"*(){}[\]^~\\:.]/g, ' ').trim();
65
65
  if (!escaped)
66
66
  return [];
67
67
  const sql = `SELECT uri, -bm25(${shadowTable}_fts) AS score
@@ -59,6 +59,17 @@ export declare function getClientMetadata(issuer: string, config: OAuthConfig):
59
59
  dpop_bound_access_tokens: boolean;
60
60
  scope: string;
61
61
  };
62
+ /**
63
+ * Handle a Pushed Authorization Request (PAR).
64
+ *
65
+ * Supports account creation via `prompt=create`. When set, `login_hint`
66
+ * is treated as a PDS hostname (e.g. "selfhosted.social" or "localhost:2583")
67
+ * rather than a handle or DID. The auth server is discovered from the PDS's
68
+ * protected resource metadata, and `prompt=create` is forwarded to the PDS
69
+ * PAR so it shows the signup page.
70
+ *
71
+ * For normal login, `login_hint` is a handle or DID as usual.
72
+ */
62
73
  export declare function handlePar(config: OAuthConfig, body: Record<string, string>, dpopHeader: string, requestUrl: string): Promise<{
63
74
  request_uri: string;
64
75
  expires_in: number;
@@ -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;AA4E/C,wBAAsB,SAAS,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBrG;AAID,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW;;;;;;;;;;;;;;;;;;;EAqBxE;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW;;;;;EAO/E;AAED,wBAAgB,OAAO;;;;;;;;;;;;;;;;;;;;;;EAWtB;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW;;;;;;;;;EAcpE;AAID,wBAAsB,SAAS,CAC7B,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CA+ItD;AAID,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,GAAG,MAAM,CAShF;AAID,wBAAsB,WAAW,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAsGtF;AAID,wBAAsB,cAAc,CAClC,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,GAAG,EAAE,MAAM,GAAG,IAAI,GACjB,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CA0HrG;AAID,wBAAsB,WAAW,CAC/B,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,GAAG,CAAC,CAUd;AA0JD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACtF,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAoEpF;AAID,wBAAsB,YAAY,CAChC,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CA0BjC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/oauth/server.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AA4E/C,wBAAsB,SAAS,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBrG;AAID,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW;;;;;;;;;;;;;;;;;;;EAqBxE;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW;;;;;EAO/E;AAED,wBAAgB,OAAO;;;;;;;;;;;;;;;;;;;;;;EAWtB;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW;;;;;;;;;EAcpE;AAID;;;;;;;;;;GAUG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CAwKtD;AAID,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,GAAG,MAAM,CAShF;AAID,wBAAsB,WAAW,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAsGtF;AAID,wBAAsB,cAAc,CAClC,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,GAAG,EAAE,MAAM,GAAG,IAAI,GACjB,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CA0HrG;AAID,wBAAsB,WAAW,CAC/B,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,GAAG,CAAC,CAUd;AA0JD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACtF,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAoEpF;AAID,wBAAsB,YAAY,CAChC,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CA0BjC"}
@@ -3,7 +3,7 @@ import { generateKeyPair, importPrivateKey, computeJwkThumbprint, signJwt, parse
3
3
  import { parseDpopProof, createDpopProof } from "./dpop.js";
4
4
  import { initSession } from "./session.js";
5
5
  import { resolveClient, validateRedirectUri, isLoopbackClient } from "./client.js";
6
- import { discoverAuthServer, resolveHandle } from "./discovery.js";
6
+ import { discoverAuthServer, resolveHandle, fetchProtectedResourceMetadata, fetchAuthServerMetadata } from "./discovery.js";
7
7
  import { getServerKey, storeServerKey, storeOAuthRequest, getOAuthRequest, deleteOAuthRequest, storeAuthCode, consumeAuthCode, storeSession, deleteSession, checkAndStoreDpopJti, cleanupExpiredOAuth, storeRefreshToken, getRefreshToken, revokeRefreshToken, } from "./db.js";
8
8
  import { emit } from "../logger.js";
9
9
  import { querySQL } from "../database/db.js";
@@ -122,6 +122,17 @@ export function getClientMetadata(issuer, config) {
122
122
  };
123
123
  }
124
124
  // --- PAR Endpoint ---
125
+ /**
126
+ * Handle a Pushed Authorization Request (PAR).
127
+ *
128
+ * Supports account creation via `prompt=create`. When set, `login_hint`
129
+ * is treated as a PDS hostname (e.g. "selfhosted.social" or "localhost:2583")
130
+ * rather than a handle or DID. The auth server is discovered from the PDS's
131
+ * protected resource metadata, and `prompt=create` is forwarded to the PDS
132
+ * PAR so it shows the signup page.
133
+ *
134
+ * For normal login, `login_hint` is a handle or DID as usual.
135
+ */
125
136
  export async function handlePar(config, body, dpopHeader, requestUrl) {
126
137
  // Validate client DPoP proof
127
138
  const dpop = await parseDpopProof(dpopHeader, 'POST', requestUrl);
@@ -146,9 +157,34 @@ export async function handlePar(config, body, dpopHeader, requestUrl) {
146
157
  throw new Error('code_challenge is required');
147
158
  if (body.code_challenge_method && body.code_challenge_method !== 'S256')
148
159
  throw new Error('Only S256 supported');
149
- // Resolve DID from login_hint
160
+ // Resolve DID and PDS from login_hint
161
+ const prompt = body.prompt;
150
162
  let did = body.login_hint;
151
- if (did && !did.startsWith('did:')) {
163
+ let pdsRequestUri;
164
+ let pdsAuthServer;
165
+ let pdsCodeVerifier;
166
+ let pdsState;
167
+ let pdsEndpoint;
168
+ if (prompt === 'create' && body.login_hint) {
169
+ // Account creation: login_hint is a PDS URL, discover auth server from it directly
170
+ let pdsUrl;
171
+ if (body.login_hint.startsWith('http')) {
172
+ pdsUrl = body.login_hint;
173
+ }
174
+ else if (body.login_hint.match(/^localhost[:/]/)) {
175
+ pdsUrl = `http://${body.login_hint}`;
176
+ }
177
+ else {
178
+ pdsUrl = `https://${body.login_hint}`;
179
+ }
180
+ pdsEndpoint = pdsUrl;
181
+ const protectedResource = await fetchProtectedResourceMetadata(pdsUrl);
182
+ pdsAuthServer = protectedResource.authorization_servers[0];
183
+ if (!pdsAuthServer)
184
+ throw new Error(`No auth server for PDS ${pdsUrl}`);
185
+ did = undefined; // no DID yet for account creation
186
+ }
187
+ else if (did && !did.startsWith('did:')) {
152
188
  try {
153
189
  did = await resolveHandle(did, _relayUrl);
154
190
  }
@@ -156,33 +192,37 @@ export async function handlePar(config, body, dpopHeader, requestUrl) {
156
192
  throw new Error('Handle not found');
157
193
  }
158
194
  }
159
- // Discover user's PDS auth server
160
- let pdsRequestUri;
161
- let pdsAuthServer;
162
- let pdsCodeVerifier;
163
- let pdsState;
164
- let pdsEndpoint;
165
- if (did) {
195
+ // Discover user's PDS auth server (for login flow with a resolved DID)
196
+ if (did && !pdsAuthServer) {
166
197
  const discovery = await discoverAuthServer(did, _plcUrl);
167
198
  pdsAuthServer = discovery.authServerEndpoint;
168
199
  pdsEndpoint = discovery.pdsEndpoint;
200
+ }
201
+ if (pdsAuthServer) {
202
+ const authServerMetadata = await fetchAuthServerMetadata(pdsAuthServer);
169
203
  // Create PKCE for our PAR to the PDS
170
204
  pdsCodeVerifier = randomToken();
171
205
  const pdsCodeChallenge = base64UrlEncode(await sha256(pdsCodeVerifier));
172
206
  pdsState = randomToken(); // unique state to correlate callback
173
207
  // PAR to the PDS
174
- const parEndpoint = discovery.authServerMetadata.pushed_authorization_request_endpoint || `${pdsAuthServer}/oauth/par`;
208
+ const parEndpoint = authServerMetadata.pushed_authorization_request_endpoint || `${pdsAuthServer}/oauth/par`;
175
209
  const serverDpopProof = await createDpopProof(serverPrivateJwk, serverPublicJwk, 'POST', parEndpoint);
176
- const pdsParBody = new URLSearchParams({
210
+ const pdsParParams = {
177
211
  client_id: pdsClientId(config.issuer, config),
178
212
  redirect_uri: pdsRedirectUri(config.issuer),
179
213
  response_type: 'code',
180
214
  code_challenge: pdsCodeChallenge,
181
215
  code_challenge_method: 'S256',
182
216
  scope: body.scope || 'atproto transition:generic',
183
- login_hint: body.login_hint || did,
184
217
  state: pdsState,
185
- });
218
+ };
219
+ if (prompt === 'create') {
220
+ pdsParParams.prompt = 'create';
221
+ }
222
+ if (did) {
223
+ pdsParParams.login_hint = body.login_hint || did;
224
+ }
225
+ const pdsParBody = new URLSearchParams(pdsParParams);
186
226
  const pdsParRes = await fetch(parEndpoint, {
187
227
  method: 'POST',
188
228
  headers: { 'Content-Type': 'application/x-www-form-urlencoded', DPoP: serverDpopProof },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hatk/hatk",
3
- "version": "0.0.1-alpha.45",
3
+ "version": "0.0.1-alpha.47",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "hatk": "dist/cli.js"