@alteran/astro 0.1.14 → 0.3.1
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/README.md +35 -10
- package/index.js +2 -4
- package/migrations/0006_adorable_spectrum.sql +11 -0
- package/migrations/meta/0006_snapshot.json +429 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +7 -3
- package/src/db/account.ts +145 -0
- package/src/db/dal.ts +27 -9
- package/src/db/repo.ts +9 -8
- package/src/db/schema.ts +29 -11
- package/src/lib/actor.ts +133 -0
- package/src/lib/appview.ts +508 -0
- package/src/lib/auth.ts +22 -2
- package/src/lib/blob-refs.ts +9 -13
- package/src/lib/chat.ts +238 -0
- package/src/lib/config.ts +15 -7
- package/src/lib/feed.ts +165 -0
- package/src/lib/jwt.ts +231 -79
- package/src/lib/labeler.ts +91 -0
- package/src/lib/mst/blockstore.ts +98 -14
- package/src/lib/password.ts +40 -0
- package/src/lib/preferences.ts +73 -0
- package/src/lib/relay.ts +101 -0
- package/src/lib/secrets.ts +29 -21
- package/src/lib/session-tokens.ts +202 -0
- package/src/lib/token-cleanup.ts +3 -12
- package/src/lib/util.ts +17 -2
- package/src/middleware.ts +20 -21
- package/src/pages/.well-known/did.json.ts +45 -32
- package/src/pages/xrpc/app.bsky.actor.getPreferences.ts +23 -0
- package/src/pages/xrpc/app.bsky.actor.getProfile.ts +34 -0
- package/src/pages/xrpc/app.bsky.actor.getProfiles.ts +42 -0
- package/src/pages/xrpc/app.bsky.actor.putPreferences.ts +36 -0
- package/src/pages/xrpc/app.bsky.feed.getAuthorFeed.ts +42 -0
- package/src/pages/xrpc/app.bsky.feed.getPostThread.ts +37 -0
- package/src/pages/xrpc/app.bsky.feed.getPosts.ts +26 -0
- package/src/pages/xrpc/app.bsky.feed.getTimeline.ts +35 -0
- package/src/pages/xrpc/app.bsky.graph.getFollowers.ts +29 -0
- package/src/pages/xrpc/app.bsky.graph.getFollows.ts +29 -0
- package/src/pages/xrpc/app.bsky.labeler.getServices.ts +29 -0
- package/src/pages/xrpc/app.bsky.notification.getUnreadCount.ts +20 -0
- package/src/pages/xrpc/app.bsky.notification.listNotifications.ts +27 -0
- package/src/pages/xrpc/app.bsky.unspecced.getAgeAssuranceState.ts +19 -0
- package/src/pages/xrpc/app.bsky.unspecced.getConfig.ts +15 -0
- package/src/pages/xrpc/chat.bsky.convo.getLog.ts +26 -0
- package/src/pages/xrpc/chat.bsky.convo.listConvos.ts +37 -0
- package/src/pages/xrpc/com.atproto.identity.getRecommendedDidCredentials.ts +64 -66
- package/src/pages/xrpc/com.atproto.identity.requestPlcOperationSignature.ts +24 -0
- package/src/pages/xrpc/com.atproto.identity.signPlcOperation.ts +127 -0
- package/src/pages/xrpc/com.atproto.identity.submitPlcOperation.ts +91 -0
- package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +6 -2
- package/src/pages/xrpc/com.atproto.server.createSession.ts +36 -8
- package/src/pages/xrpc/com.atproto.server.describeServer.ts +37 -4
- package/src/pages/xrpc/com.atproto.server.getServiceAuth.ts +64 -0
- package/src/pages/xrpc/com.atproto.server.refreshSession.ts +55 -32
- package/src/services/repo-manager.ts +15 -6
- package/src/worker/runtime.ts +21 -0
- package/types/env.d.ts +11 -2
- package/src/pages/xrpc/com.atproto.repo.importRepo.ts +0 -142
- package/src/pages/xrpc/com.atproto.server.activateAccount.ts +0 -53
- package/src/pages/xrpc/com.atproto.server.createAccount.ts +0 -99
- package/src/pages/xrpc/com.atproto.server.deactivateAccount.ts +0 -53
package/src/lib/jwt.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import type { Env } from
|
|
2
|
-
import { getRuntimeString } from
|
|
1
|
+
import type { Env } from "../env";
|
|
2
|
+
import { getRuntimeString } from "./secrets";
|
|
3
|
+
import { base58btc } from "multiformats/bases/base58";
|
|
4
|
+
import {
|
|
5
|
+
issueSessionTokens,
|
|
6
|
+
verifyAccessToken,
|
|
7
|
+
verifyRefreshToken,
|
|
8
|
+
} from "./session-tokens";
|
|
3
9
|
|
|
4
10
|
export interface JwtClaims {
|
|
5
11
|
sub: string; // DID
|
|
@@ -7,163 +13,309 @@ export interface JwtClaims {
|
|
|
7
13
|
scope?: string;
|
|
8
14
|
aud?: string;
|
|
9
15
|
jti?: string;
|
|
10
|
-
t:
|
|
16
|
+
t: "access" | "refresh";
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
// JWT
|
|
14
|
-
export async function signJwt(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const payload: Record<string, unknown> = {
|
|
22
|
-
iss: env.PDS_HOSTNAME || 'alteran',
|
|
23
|
-
sub: claims.sub,
|
|
24
|
-
aud: claims.aud || env.PDS_HOSTNAME || 'alteran',
|
|
25
|
-
iat,
|
|
26
|
-
exp,
|
|
27
|
-
t: kind,
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
// Add optional claims
|
|
31
|
-
if (claims.handle) payload.handle = claims.handle;
|
|
32
|
-
if (claims.scope) payload.scope = claims.scope;
|
|
33
|
-
if (claims.jti) payload.jti = claims.jti;
|
|
34
|
-
|
|
35
|
-
const secret = await getRuntimeString(
|
|
36
|
-
env,
|
|
37
|
-
kind === 'access' ? 'ACCESS_TOKEN_SECRET' : 'REFRESH_TOKEN_SECRET',
|
|
38
|
-
kind === 'access' ? 'dev-access' : 'dev-refresh'
|
|
39
|
-
);
|
|
40
|
-
if (!secret) {
|
|
41
|
-
throw new Error(`Missing ${kind === 'access' ? 'ACCESS_TOKEN_SECRET' : 'REFRESH_TOKEN_SECRET'}`);
|
|
42
|
-
}
|
|
43
|
-
const algorithm = (env.JWT_ALGORITHM as string | undefined) ?? 'HS256';
|
|
44
|
-
|
|
45
|
-
if (algorithm === 'EdDSA') {
|
|
46
|
-
return await eddsaJwtSign(payload, env);
|
|
20
|
+
export async function signJwt(
|
|
21
|
+
env: Env,
|
|
22
|
+
claims: JwtClaims,
|
|
23
|
+
kind: "access" | "refresh",
|
|
24
|
+
): Promise<string> {
|
|
25
|
+
if (!claims.sub) {
|
|
26
|
+
throw new Error("Cannot sign JWT without subject");
|
|
47
27
|
}
|
|
48
|
-
|
|
49
|
-
|
|
28
|
+
const { accessJwt, refreshJwt } = await issueSessionTokens(env, claims.sub, {
|
|
29
|
+
jti: claims.jti,
|
|
30
|
+
});
|
|
31
|
+
return kind === "access" ? accessJwt : refreshJwt;
|
|
50
32
|
}
|
|
51
33
|
|
|
52
|
-
export async function verifyJwt(
|
|
53
|
-
|
|
34
|
+
export async function verifyJwt(
|
|
35
|
+
env: Env,
|
|
36
|
+
token: string,
|
|
37
|
+
): Promise<{ valid: boolean; payload: JwtClaims } | null> {
|
|
38
|
+
const parts = token.split(".");
|
|
54
39
|
if (parts.length !== 3) return null;
|
|
55
|
-
const header = JSON.parse(
|
|
40
|
+
const header = JSON.parse(
|
|
41
|
+
atob(parts[0].replace(/-/g, "+").replace(/_/g, "/")),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (header.typ === "at+jwt") {
|
|
45
|
+
const payload = await verifyAccessToken(env, token).catch(() => null);
|
|
46
|
+
if (!payload) return null;
|
|
47
|
+
if (!payload.sub) return null;
|
|
48
|
+
const claims: JwtClaims = {
|
|
49
|
+
sub: String(payload.sub),
|
|
50
|
+
aud: payload.aud as string | undefined,
|
|
51
|
+
scope: payload.scope as string | undefined,
|
|
52
|
+
jti: payload.jti as string | undefined,
|
|
53
|
+
t: "access",
|
|
54
|
+
};
|
|
55
|
+
if (payload.handle) {
|
|
56
|
+
claims.handle = String(payload.handle);
|
|
57
|
+
}
|
|
58
|
+
return { valid: true, payload: claims };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (header.typ === "refresh+jwt") {
|
|
62
|
+
const verified = await verifyRefreshToken(env, token).catch(() => null);
|
|
63
|
+
if (!verified) return null;
|
|
64
|
+
if (!verified.payload.sub) return null;
|
|
65
|
+
const payload: JwtClaims = {
|
|
66
|
+
sub: String(verified.payload.sub),
|
|
67
|
+
aud: verified.payload.aud as string | undefined,
|
|
68
|
+
scope: verified.payload.scope as string | undefined,
|
|
69
|
+
jti: verified.payload.jti as string | undefined,
|
|
70
|
+
t: "refresh",
|
|
71
|
+
};
|
|
72
|
+
return { valid: true, payload };
|
|
73
|
+
}
|
|
56
74
|
|
|
57
|
-
const payload = JSON.parse(
|
|
75
|
+
const payload = JSON.parse(
|
|
76
|
+
atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")),
|
|
77
|
+
);
|
|
58
78
|
|
|
59
79
|
let ok = false;
|
|
60
|
-
if (header.alg ===
|
|
80
|
+
if (header.alg === "HS256" && header.typ === "JWT") {
|
|
61
81
|
const secret = await getRuntimeString(
|
|
62
82
|
env,
|
|
63
|
-
payload.t ===
|
|
64
|
-
payload.t ===
|
|
83
|
+
payload.t === "refresh" ? "REFRESH_TOKEN_SECRET" : "REFRESH_TOKEN",
|
|
84
|
+
payload.t === "refresh" ? "dev-refresh" : "dev-access",
|
|
65
85
|
);
|
|
66
86
|
if (!secret) return null;
|
|
67
|
-
ok = await hmacJwtVerify(parts[0] +
|
|
68
|
-
} else if (header.alg ===
|
|
69
|
-
ok = await eddsaJwtVerify(parts[0] +
|
|
87
|
+
ok = await hmacJwtVerify(parts[0] + "." + parts[1], parts[2], secret);
|
|
88
|
+
} else if (header.alg === "EdDSA" && header.typ === "JWT") {
|
|
89
|
+
ok = await eddsaJwtVerify(parts[0] + "." + parts[1], parts[2], env);
|
|
70
90
|
} else {
|
|
71
91
|
return null;
|
|
72
92
|
}
|
|
73
93
|
|
|
74
94
|
const now = Math.floor(Date.now() / 1000);
|
|
75
95
|
if (!ok || (payload.exp && now > payload.exp)) return null;
|
|
76
|
-
return { valid: true, payload };
|
|
96
|
+
return { valid: true, payload: payload as JwtClaims };
|
|
77
97
|
}
|
|
78
98
|
|
|
79
99
|
async function hmacJwtSign(payload: any, secret: string): Promise<string> {
|
|
80
100
|
const enc = new TextEncoder();
|
|
81
|
-
const header = { alg:
|
|
101
|
+
const header = { alg: "HS256", typ: "JWT" };
|
|
82
102
|
const h = b64url(enc.encode(JSON.stringify(header)));
|
|
83
103
|
const p = b64url(enc.encode(JSON.stringify(payload)));
|
|
84
104
|
const data = `${h}.${p}`;
|
|
85
|
-
const key = await crypto.subtle.importKey(
|
|
86
|
-
|
|
105
|
+
const key = await crypto.subtle.importKey(
|
|
106
|
+
"raw",
|
|
107
|
+
enc.encode(secret),
|
|
108
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
109
|
+
false,
|
|
110
|
+
["sign"],
|
|
111
|
+
);
|
|
112
|
+
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(data));
|
|
87
113
|
const s = b64url(new Uint8Array(sig));
|
|
88
114
|
return `${h}.${p}.${s}`;
|
|
89
115
|
}
|
|
90
116
|
|
|
91
|
-
async function hmacJwtVerify(
|
|
117
|
+
async function hmacJwtVerify(
|
|
118
|
+
data: string,
|
|
119
|
+
sigB64: string,
|
|
120
|
+
secret: string,
|
|
121
|
+
): Promise<boolean> {
|
|
92
122
|
const enc = new TextEncoder();
|
|
93
|
-
const key = await crypto.subtle.importKey(
|
|
94
|
-
|
|
123
|
+
const key = await crypto.subtle.importKey(
|
|
124
|
+
"raw",
|
|
125
|
+
enc.encode(secret),
|
|
126
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
127
|
+
false,
|
|
128
|
+
["verify"],
|
|
129
|
+
);
|
|
130
|
+
const ok = await crypto.subtle.verify(
|
|
131
|
+
"HMAC",
|
|
132
|
+
key,
|
|
133
|
+
b64urlDecode(sigB64),
|
|
134
|
+
enc.encode(data),
|
|
135
|
+
);
|
|
95
136
|
return !!ok;
|
|
96
137
|
}
|
|
97
138
|
|
|
98
139
|
async function eddsaJwtSign(payload: any, env: Env): Promise<string> {
|
|
99
140
|
const enc = new TextEncoder();
|
|
100
|
-
const header = { alg:
|
|
141
|
+
const header = { alg: "EdDSA", typ: "JWT" };
|
|
101
142
|
const h = b64url(enc.encode(JSON.stringify(header)));
|
|
102
143
|
const p = b64url(enc.encode(JSON.stringify(payload)));
|
|
103
144
|
const data = `${h}.${p}`;
|
|
104
145
|
|
|
105
146
|
// Import Ed25519 private key from env
|
|
106
|
-
const keyData = await getRuntimeString(env,
|
|
147
|
+
const keyData = await getRuntimeString(env, "REPO_SIGNING_KEY");
|
|
107
148
|
if (!keyData) {
|
|
108
|
-
throw new Error(
|
|
149
|
+
throw new Error("REPO_SIGNING_KEY not configured for EdDSA JWTs");
|
|
109
150
|
}
|
|
110
151
|
|
|
111
152
|
// Decode base64 private key
|
|
112
153
|
const keyBytes = b64urlDecode(keyData);
|
|
113
154
|
const key = await crypto.subtle.importKey(
|
|
114
|
-
|
|
155
|
+
"pkcs8",
|
|
115
156
|
keyBytes,
|
|
116
|
-
{ name:
|
|
157
|
+
{ name: "Ed25519" } as any,
|
|
117
158
|
false,
|
|
118
|
-
[
|
|
159
|
+
["sign"],
|
|
119
160
|
);
|
|
120
161
|
|
|
121
|
-
const sig = await crypto.subtle.sign(
|
|
162
|
+
const sig = await crypto.subtle.sign("Ed25519", key, enc.encode(data));
|
|
122
163
|
const s = b64url(new Uint8Array(sig));
|
|
123
164
|
return `${h}.${p}.${s}`;
|
|
124
165
|
}
|
|
125
166
|
|
|
126
|
-
async function eddsaJwtVerify(
|
|
167
|
+
async function eddsaJwtVerify(
|
|
168
|
+
data: string,
|
|
169
|
+
sigB64: string,
|
|
170
|
+
env: Env,
|
|
171
|
+
): Promise<boolean> {
|
|
127
172
|
const enc = new TextEncoder();
|
|
128
173
|
|
|
129
174
|
// Import Ed25519 public key from env
|
|
130
|
-
const keyData = await getRuntimeString(env,
|
|
175
|
+
const keyData = await getRuntimeString(env, "REPO_SIGNING_KEY_PUBLIC");
|
|
131
176
|
if (!keyData) {
|
|
132
|
-
console.error(
|
|
177
|
+
console.error(
|
|
178
|
+
"EdDSA JWT verification failed: REPO_SIGNING_KEY_PUBLIC not configured",
|
|
179
|
+
);
|
|
133
180
|
return false;
|
|
134
181
|
}
|
|
135
182
|
|
|
136
183
|
try {
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
false
|
|
143
|
-
|
|
144
|
-
);
|
|
184
|
+
const key = await importEd25519PublicKey(keyData);
|
|
185
|
+
if (!key) {
|
|
186
|
+
console.error(
|
|
187
|
+
"EdDSA JWT verification failed: unsupported public key format for Ed25519",
|
|
188
|
+
);
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
145
191
|
|
|
146
|
-
const ok = await crypto.subtle.verify(
|
|
192
|
+
const ok = await crypto.subtle.verify(
|
|
193
|
+
"Ed25519",
|
|
194
|
+
key,
|
|
195
|
+
b64urlDecode(sigB64),
|
|
196
|
+
enc.encode(data),
|
|
197
|
+
);
|
|
147
198
|
return !!ok;
|
|
148
199
|
} catch (error) {
|
|
149
|
-
console.error(
|
|
200
|
+
console.error("EdDSA JWT verification error:", error);
|
|
150
201
|
return false;
|
|
151
202
|
}
|
|
152
203
|
}
|
|
153
204
|
|
|
154
205
|
function b64url(bytes: ArrayBuffer | Uint8Array): string {
|
|
155
206
|
const b = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
156
|
-
let s =
|
|
207
|
+
let s = "";
|
|
157
208
|
for (let i = 0; i < b.length; i++) {
|
|
158
209
|
s += String.fromCharCode(b[i]);
|
|
159
210
|
}
|
|
160
|
-
return btoa(s).replace(/\+/g,
|
|
211
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
161
212
|
}
|
|
162
213
|
|
|
163
214
|
function b64urlDecode(s: string): Uint8Array {
|
|
164
|
-
const pad = s.length % 4 === 2 ?
|
|
165
|
-
const bin = atob(s.replace(/-/g,
|
|
215
|
+
const pad = s.length % 4 === 2 ? "==" : s.length % 4 === 3 ? "=" : "";
|
|
216
|
+
const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/") + pad);
|
|
166
217
|
const out = new Uint8Array(bin.length);
|
|
167
218
|
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
168
219
|
return out;
|
|
169
220
|
}
|
|
221
|
+
|
|
222
|
+
async function importEd25519PublicKey(
|
|
223
|
+
value: string,
|
|
224
|
+
): Promise<CryptoKey | null> {
|
|
225
|
+
const attempts = buildPublicKeyCandidates(value);
|
|
226
|
+
for (const attempt of attempts) {
|
|
227
|
+
try {
|
|
228
|
+
return await crypto.subtle.importKey(
|
|
229
|
+
attempt.format,
|
|
230
|
+
attempt.data,
|
|
231
|
+
{ name: "Ed25519", namedCurve: "Ed25519" } as any,
|
|
232
|
+
false,
|
|
233
|
+
["verify"],
|
|
234
|
+
);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.warn(
|
|
237
|
+
"EdDSA JWT verification warning: failed to import key candidate",
|
|
238
|
+
error,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
type KeyImportAttempt = { format: KeyFormat; data: Uint8Array };
|
|
246
|
+
type KeyFormat = "raw" | "spki";
|
|
247
|
+
|
|
248
|
+
function buildPublicKeyCandidates(value: string): KeyImportAttempt[] {
|
|
249
|
+
const trimmed = value.trim();
|
|
250
|
+
const attempts: KeyImportAttempt[] = [];
|
|
251
|
+
|
|
252
|
+
const didKeyCandidate = decodeDidKey(trimmed);
|
|
253
|
+
if (didKeyCandidate) {
|
|
254
|
+
attempts.push({ format: "raw", data: didKeyCandidate });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const pemMatch = trimmed.match(
|
|
258
|
+
/-----BEGIN PUBLIC KEY-----([\s\S]+?)-----END PUBLIC KEY-----/,
|
|
259
|
+
);
|
|
260
|
+
if (pemMatch) {
|
|
261
|
+
const derBytes = decodeBase64(pemMatch[1].replace(/\s+/g, ""));
|
|
262
|
+
if (derBytes) {
|
|
263
|
+
attempts.push({ format: "spki", data: derBytes });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const compact = trimmed.replace(/\s+/g, "");
|
|
268
|
+
const decoded = decodeBase64(compact);
|
|
269
|
+
if (decoded) {
|
|
270
|
+
if (decoded.length === 32) {
|
|
271
|
+
attempts.push({ format: "raw", data: decoded });
|
|
272
|
+
} else {
|
|
273
|
+
attempts.push({ format: "spki", data: decoded });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return attempts;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function decodeBase64(value: string): Uint8Array | null {
|
|
281
|
+
const cleaned = value.replace(/\s+/g, "");
|
|
282
|
+
if (!cleaned) return null;
|
|
283
|
+
try {
|
|
284
|
+
return b64urlDecode(cleaned);
|
|
285
|
+
} catch {
|
|
286
|
+
const normalized = cleaned.replace(/-/g, "+").replace(/_/g, "/");
|
|
287
|
+
const padLength = normalized.length % 4;
|
|
288
|
+
const padded =
|
|
289
|
+
padLength === 0
|
|
290
|
+
? normalized
|
|
291
|
+
: padLength === 2
|
|
292
|
+
? normalized + "=="
|
|
293
|
+
: padLength === 3
|
|
294
|
+
? normalized + "="
|
|
295
|
+
: normalized + "===";
|
|
296
|
+
const bin = atob(padded);
|
|
297
|
+
const out = new Uint8Array(bin.length);
|
|
298
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function decodeDidKey(didKey: string): Uint8Array | null {
|
|
304
|
+
if (!didKey.startsWith("did:key:")) return null;
|
|
305
|
+
try {
|
|
306
|
+
const multibase = didKey.slice("did:key:".length);
|
|
307
|
+
const bytes = base58btc.decode(multibase);
|
|
308
|
+
if (bytes.length === 34 && bytes[0] === 0xed && bytes[1] === 0x01) {
|
|
309
|
+
return bytes.slice(2);
|
|
310
|
+
}
|
|
311
|
+
console.warn(
|
|
312
|
+
"EdDSA JWT verification warning: unsupported did:key multicodec prefix",
|
|
313
|
+
);
|
|
314
|
+
} catch (error) {
|
|
315
|
+
console.warn(
|
|
316
|
+
"EdDSA JWT verification warning: failed to parse did:key",
|
|
317
|
+
error,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Env } from '../env';
|
|
2
|
+
import { getRecord } from '../db/dal';
|
|
3
|
+
import { buildProfileView, getPrimaryActor } from './actor';
|
|
4
|
+
|
|
5
|
+
export interface LabelerViewOptions {
|
|
6
|
+
detailed?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const LABELER_COLLECTION = 'app.bsky.labeler.service';
|
|
10
|
+
const LABELER_RKEY = 'self';
|
|
11
|
+
|
|
12
|
+
export async function getLabelerServiceViews(
|
|
13
|
+
env: Env,
|
|
14
|
+
dids: string[],
|
|
15
|
+
options: LabelerViewOptions = {}
|
|
16
|
+
) {
|
|
17
|
+
const detailed = options.detailed ?? false;
|
|
18
|
+
const primaryActor = await getPrimaryActor(env);
|
|
19
|
+
|
|
20
|
+
const unique = Array.from(new Set(dids.map((did) => did.trim()).filter(Boolean)));
|
|
21
|
+
const views: any[] = [];
|
|
22
|
+
|
|
23
|
+
for (const did of unique) {
|
|
24
|
+
if (did !== primaryActor.did) continue; // Single-user PDS only has local labeler data
|
|
25
|
+
|
|
26
|
+
const uri = `at://${did}/${LABELER_COLLECTION}/${LABELER_RKEY}`;
|
|
27
|
+
const row = await getRecord(env, uri);
|
|
28
|
+
if (!row || !row.json) continue;
|
|
29
|
+
|
|
30
|
+
let record: any;
|
|
31
|
+
try {
|
|
32
|
+
record = JSON.parse(row.json);
|
|
33
|
+
} catch {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (typeof record !== 'object' || record === null) continue;
|
|
38
|
+
|
|
39
|
+
const indexedAt = typeof record.createdAt === 'string' ? record.createdAt : new Date().toISOString();
|
|
40
|
+
const baseView: any = {
|
|
41
|
+
uri,
|
|
42
|
+
cid: row.cid,
|
|
43
|
+
creator: buildProfileView(primaryActor),
|
|
44
|
+
indexedAt,
|
|
45
|
+
likeCount: 0,
|
|
46
|
+
viewer: {},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (detailed) {
|
|
50
|
+
const policies = normalizePolicies(record.policies);
|
|
51
|
+
views.push({
|
|
52
|
+
...baseView,
|
|
53
|
+
policies,
|
|
54
|
+
reasonTypes: Array.isArray(record.reasonTypes) ? record.reasonTypes : undefined,
|
|
55
|
+
subjectTypes: Array.isArray(record.subjectTypes) ? record.subjectTypes : undefined,
|
|
56
|
+
subjectCollections: Array.isArray(record.subjectCollections) ? record.subjectCollections : undefined,
|
|
57
|
+
labels: extractLabels(record.labels),
|
|
58
|
+
});
|
|
59
|
+
} else {
|
|
60
|
+
const labels = extractLabels(record.labels);
|
|
61
|
+
if (labels) baseView.labels = labels;
|
|
62
|
+
views.push(baseView);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return views;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizePolicies(input: any) {
|
|
70
|
+
if (input && typeof input === 'object') {
|
|
71
|
+
const labelValues = Array.isArray(input.labelValues) ? input.labelValues : [];
|
|
72
|
+
const labelValueDefinitions = Array.isArray(input.labelValueDefinitions)
|
|
73
|
+
? input.labelValueDefinitions
|
|
74
|
+
: undefined;
|
|
75
|
+
return {
|
|
76
|
+
labelValues,
|
|
77
|
+
labelValueDefinitions,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
labelValues: [],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function extractLabels(input: any) {
|
|
87
|
+
if (!input) return undefined;
|
|
88
|
+
if (Array.isArray(input)) return input.length ? input : undefined;
|
|
89
|
+
if (typeof input === 'object') return input;
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
@@ -72,23 +72,107 @@ export class D1Blockstore implements WritableBlockstore {
|
|
|
72
72
|
|
|
73
73
|
async put(cid: CID, bytes: Uint8Array): Promise<void> {
|
|
74
74
|
const db = drizzle(this.env.DB);
|
|
75
|
+
const cidStr = cid.toString();
|
|
75
76
|
|
|
76
|
-
//
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
.
|
|
81
|
-
.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
77
|
+
// Check if block already exists - D1 has issues with ON CONFLICT DO NOTHING
|
|
78
|
+
const existing = await db
|
|
79
|
+
.select({ cid: blockstore.cid })
|
|
80
|
+
.from(blockstore)
|
|
81
|
+
.where(eq(blockstore.cid, cidStr))
|
|
82
|
+
.get();
|
|
83
|
+
|
|
84
|
+
if (existing) {
|
|
85
|
+
// Block already exists, skip insert
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Encode Uint8Array to base64 string for storage. Chunk to avoid call-stack limits.
|
|
90
|
+
let binary = '';
|
|
91
|
+
const CHUNK_SIZE = 0x8000;
|
|
92
|
+
for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
|
|
93
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK_SIZE));
|
|
94
|
+
}
|
|
95
|
+
const base64 = btoa(binary);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await db
|
|
99
|
+
.insert(blockstore)
|
|
100
|
+
.values({
|
|
101
|
+
cid: cidStr,
|
|
102
|
+
bytes: base64,
|
|
103
|
+
})
|
|
104
|
+
.run();
|
|
105
|
+
} catch (error: any) {
|
|
106
|
+
// If we get a unique constraint error, another request inserted it - that's ok
|
|
107
|
+
if (error?.message?.includes('UNIQUE constraint failed') ||
|
|
108
|
+
error?.message?.includes('constraint failed')) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
console.error(JSON.stringify({
|
|
112
|
+
level: 'error',
|
|
113
|
+
type: 'blockstore_put',
|
|
114
|
+
cid: cidStr,
|
|
115
|
+
size: bytes.byteLength,
|
|
116
|
+
message: error?.message,
|
|
117
|
+
}));
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
87
120
|
}
|
|
88
121
|
|
|
89
122
|
async putMany(blocks: Map<CID, Uint8Array>): Promise<void> {
|
|
90
|
-
|
|
91
|
-
|
|
123
|
+
const db = drizzle(this.env.DB);
|
|
124
|
+
const BATCH_SIZE = 100; // Insert 100 blocks at a time
|
|
125
|
+
const entries = Array.from(blocks.entries());
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < entries.length; i += BATCH_SIZE) {
|
|
128
|
+
const batch = entries.slice(i, i + BATCH_SIZE);
|
|
129
|
+
const values = [];
|
|
130
|
+
|
|
131
|
+
for (const [cid, bytes] of batch) {
|
|
132
|
+
const cidStr = cid.toString();
|
|
133
|
+
|
|
134
|
+
// Check if block already exists
|
|
135
|
+
const existing = await db
|
|
136
|
+
.select({ cid: blockstore.cid })
|
|
137
|
+
.from(blockstore)
|
|
138
|
+
.where(eq(blockstore.cid, cidStr))
|
|
139
|
+
.get();
|
|
140
|
+
|
|
141
|
+
if (existing) continue;
|
|
142
|
+
|
|
143
|
+
// Encode to base64
|
|
144
|
+
let binary = '';
|
|
145
|
+
const CHUNK_SIZE = 0x8000;
|
|
146
|
+
for (let j = 0; j < bytes.length; j += CHUNK_SIZE) {
|
|
147
|
+
binary += String.fromCharCode(...bytes.subarray(j, j + CHUNK_SIZE));
|
|
148
|
+
}
|
|
149
|
+
const base64 = btoa(binary);
|
|
150
|
+
|
|
151
|
+
values.push({ cid: cidStr, bytes: base64 });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (values.length > 0) {
|
|
155
|
+
try {
|
|
156
|
+
await db.insert(blockstore).values(values).run();
|
|
157
|
+
} catch (error: any) {
|
|
158
|
+
// If batch insert fails, fall back to individual inserts
|
|
159
|
+
for (const value of values) {
|
|
160
|
+
try {
|
|
161
|
+
await db.insert(blockstore).values(value).run();
|
|
162
|
+
} catch (e: any) {
|
|
163
|
+
if (!e?.message?.includes('UNIQUE constraint failed') &&
|
|
164
|
+
!e?.message?.includes('constraint failed')) {
|
|
165
|
+
console.error(JSON.stringify({
|
|
166
|
+
level: 'error',
|
|
167
|
+
type: 'blockstore_put_many',
|
|
168
|
+
cid: value.cid,
|
|
169
|
+
message: e?.message,
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
92
176
|
}
|
|
93
177
|
}
|
|
94
178
|
|
|
@@ -102,4 +186,4 @@ export class D1Blockstore implements WritableBlockstore {
|
|
|
102
186
|
}
|
|
103
187
|
return dagCbor.decode(bytes) as T;
|
|
104
188
|
}
|
|
105
|
-
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { randomBytes } from '@noble/hashes/utils.js';
|
|
2
|
+
import { scryptAsync } from '@noble/hashes/scrypt.js';
|
|
3
|
+
import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js';
|
|
4
|
+
|
|
5
|
+
const SALT_BYTES = 16;
|
|
6
|
+
const KEY_LEN = 64;
|
|
7
|
+
const SCRYPT_OPTS = {
|
|
8
|
+
N: 1 << 15, // Close to Node scrypt defaults while remaining worker-friendly
|
|
9
|
+
r: 8,
|
|
10
|
+
p: 1,
|
|
11
|
+
dkLen: KEY_LEN,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
async function derive(password: string, saltHex: string): Promise<string> {
|
|
15
|
+
const salt = hexToBytes(saltHex);
|
|
16
|
+
const key = await scryptAsync(password, salt, SCRYPT_OPTS);
|
|
17
|
+
return bytesToHex(key);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function hashPassword(password: string): Promise<string> {
|
|
21
|
+
const saltHex = bytesToHex(randomBytes(SALT_BYTES));
|
|
22
|
+
const hashHex = await derive(password, saltHex);
|
|
23
|
+
return `${saltHex}:${hashHex}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function verifyPassword(password: string, stored: string | null): Promise<boolean> {
|
|
27
|
+
if (!stored) return false;
|
|
28
|
+
const [saltHex, hashHex] = stored.split(':');
|
|
29
|
+
if (!saltHex || !hashHex) return false;
|
|
30
|
+
const candidate = await derive(password, saltHex);
|
|
31
|
+
return candidate === hashHex;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function rehashIfNeeded(password: string, stored: string | null): Promise<string | null> {
|
|
35
|
+
if (!stored) return null;
|
|
36
|
+
const [saltHex] = stored.split(':');
|
|
37
|
+
if (!saltHex) return null;
|
|
38
|
+
// Currently no adaptive parameters; placeholder for future upgrades.
|
|
39
|
+
return null;
|
|
40
|
+
}
|