@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.
Files changed (62) hide show
  1. package/README.md +35 -10
  2. package/index.js +2 -4
  3. package/migrations/0006_adorable_spectrum.sql +11 -0
  4. package/migrations/meta/0006_snapshot.json +429 -0
  5. package/migrations/meta/_journal.json +7 -0
  6. package/package.json +7 -3
  7. package/src/db/account.ts +145 -0
  8. package/src/db/dal.ts +27 -9
  9. package/src/db/repo.ts +9 -8
  10. package/src/db/schema.ts +29 -11
  11. package/src/lib/actor.ts +133 -0
  12. package/src/lib/appview.ts +508 -0
  13. package/src/lib/auth.ts +22 -2
  14. package/src/lib/blob-refs.ts +9 -13
  15. package/src/lib/chat.ts +238 -0
  16. package/src/lib/config.ts +15 -7
  17. package/src/lib/feed.ts +165 -0
  18. package/src/lib/jwt.ts +231 -79
  19. package/src/lib/labeler.ts +91 -0
  20. package/src/lib/mst/blockstore.ts +98 -14
  21. package/src/lib/password.ts +40 -0
  22. package/src/lib/preferences.ts +73 -0
  23. package/src/lib/relay.ts +101 -0
  24. package/src/lib/secrets.ts +29 -21
  25. package/src/lib/session-tokens.ts +202 -0
  26. package/src/lib/token-cleanup.ts +3 -12
  27. package/src/lib/util.ts +17 -2
  28. package/src/middleware.ts +20 -21
  29. package/src/pages/.well-known/did.json.ts +45 -32
  30. package/src/pages/xrpc/app.bsky.actor.getPreferences.ts +23 -0
  31. package/src/pages/xrpc/app.bsky.actor.getProfile.ts +34 -0
  32. package/src/pages/xrpc/app.bsky.actor.getProfiles.ts +42 -0
  33. package/src/pages/xrpc/app.bsky.actor.putPreferences.ts +36 -0
  34. package/src/pages/xrpc/app.bsky.feed.getAuthorFeed.ts +42 -0
  35. package/src/pages/xrpc/app.bsky.feed.getPostThread.ts +37 -0
  36. package/src/pages/xrpc/app.bsky.feed.getPosts.ts +26 -0
  37. package/src/pages/xrpc/app.bsky.feed.getTimeline.ts +35 -0
  38. package/src/pages/xrpc/app.bsky.graph.getFollowers.ts +29 -0
  39. package/src/pages/xrpc/app.bsky.graph.getFollows.ts +29 -0
  40. package/src/pages/xrpc/app.bsky.labeler.getServices.ts +29 -0
  41. package/src/pages/xrpc/app.bsky.notification.getUnreadCount.ts +20 -0
  42. package/src/pages/xrpc/app.bsky.notification.listNotifications.ts +27 -0
  43. package/src/pages/xrpc/app.bsky.unspecced.getAgeAssuranceState.ts +19 -0
  44. package/src/pages/xrpc/app.bsky.unspecced.getConfig.ts +15 -0
  45. package/src/pages/xrpc/chat.bsky.convo.getLog.ts +26 -0
  46. package/src/pages/xrpc/chat.bsky.convo.listConvos.ts +37 -0
  47. package/src/pages/xrpc/com.atproto.identity.getRecommendedDidCredentials.ts +64 -66
  48. package/src/pages/xrpc/com.atproto.identity.requestPlcOperationSignature.ts +24 -0
  49. package/src/pages/xrpc/com.atproto.identity.signPlcOperation.ts +127 -0
  50. package/src/pages/xrpc/com.atproto.identity.submitPlcOperation.ts +91 -0
  51. package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +6 -2
  52. package/src/pages/xrpc/com.atproto.server.createSession.ts +36 -8
  53. package/src/pages/xrpc/com.atproto.server.describeServer.ts +37 -4
  54. package/src/pages/xrpc/com.atproto.server.getServiceAuth.ts +64 -0
  55. package/src/pages/xrpc/com.atproto.server.refreshSession.ts +55 -32
  56. package/src/services/repo-manager.ts +15 -6
  57. package/src/worker/runtime.ts +21 -0
  58. package/types/env.d.ts +11 -2
  59. package/src/pages/xrpc/com.atproto.repo.importRepo.ts +0 -142
  60. package/src/pages/xrpc/com.atproto.server.activateAccount.ts +0 -53
  61. package/src/pages/xrpc/com.atproto.server.createAccount.ts +0 -99
  62. 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 '../env';
2
- import { getRuntimeString } from './secrets';
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: 'access' | 'refresh';
16
+ t: "access" | "refresh";
11
17
  }
12
18
 
13
19
  // JWT
14
- export async function signJwt(env: Env, claims: JwtClaims, kind: 'access' | 'refresh'): Promise<string> {
15
- const iat = Math.floor(Date.now() / 1000);
16
- const ttlAccess = Number((env.PDS_ACCESS_TTL_SEC as string | undefined) ?? 3600);
17
- const ttlRefresh = Number((env.PDS_REFRESH_TTL_SEC as string | undefined) ?? 30 * 24 * 3600);
18
- const exp = iat + (kind === 'access' ? ttlAccess : ttlRefresh);
19
-
20
- // Build proper JWT claims
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
- return await hmacJwtSign(payload, secret);
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(env: Env, token: string): Promise<{ valid: boolean; payload: any } | null> {
53
- const parts = token.split('.');
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(atob(parts[0].replace(/-/g, '+').replace(/_/g, '/')));
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(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
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 === 'HS256' && header.typ === 'JWT') {
80
+ if (header.alg === "HS256" && header.typ === "JWT") {
61
81
  const secret = await getRuntimeString(
62
82
  env,
63
- payload.t === 'refresh' ? 'REFRESH_TOKEN_SECRET' : 'ACCESS_TOKEN_SECRET',
64
- payload.t === 'refresh' ? 'dev-refresh' : 'dev-access'
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] + '.' + parts[1], parts[2], secret);
68
- } else if (header.alg === 'EdDSA' && header.typ === 'JWT') {
69
- ok = await eddsaJwtVerify(parts[0] + '.' + parts[1], parts[2], env);
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: 'HS256', typ: 'JWT' };
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('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
86
- const sig = await crypto.subtle.sign('HMAC', key, enc.encode(data));
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(data: string, sigB64: string, secret: string): Promise<boolean> {
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('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
94
- const ok = await crypto.subtle.verify('HMAC', key, b64urlDecode(sigB64), enc.encode(data));
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: 'EdDSA', typ: 'JWT' };
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, 'REPO_SIGNING_KEY');
147
+ const keyData = await getRuntimeString(env, "REPO_SIGNING_KEY");
107
148
  if (!keyData) {
108
- throw new Error('REPO_SIGNING_KEY not configured for EdDSA JWTs');
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
- 'pkcs8',
155
+ "pkcs8",
115
156
  keyBytes,
116
- { name: 'Ed25519' } as any,
157
+ { name: "Ed25519" } as any,
117
158
  false,
118
- ['sign']
159
+ ["sign"],
119
160
  );
120
161
 
121
- const sig = await crypto.subtle.sign('Ed25519', key, enc.encode(data));
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(data: string, sigB64: string, env: Env): Promise<boolean> {
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, 'REPO_SIGNING_KEY_PUBLIC');
175
+ const keyData = await getRuntimeString(env, "REPO_SIGNING_KEY_PUBLIC");
131
176
  if (!keyData) {
132
- console.error('EdDSA JWT verification failed: REPO_SIGNING_KEY_PUBLIC not configured');
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 keyBytes = b64urlDecode(keyData);
138
- const key = await crypto.subtle.importKey(
139
- 'raw',
140
- keyBytes,
141
- { name: 'Ed25519', namedCurve: 'Ed25519' } as any,
142
- false,
143
- ['verify']
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('Ed25519', key, b64urlDecode(sigB64), enc.encode(data));
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('EdDSA JWT verification error:', 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, '-').replace(/\//g, '_').replace(/=+$/, '');
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 ? '==' : s.length % 4 === 3 ? '=' : '';
165
- const bin = atob(s.replace(/-/g, '+').replace(/_/g, '/') + pad);
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
- // Encode Uint8Array to base64 string for storage
77
- const base64 = btoa(String.fromCharCode(...Array.from(bytes)));
78
-
79
- await db
80
- .insert(blockstore)
81
- .values({
82
- cid: cid.toString(),
83
- bytes: base64,
84
- })
85
- .onConflictDoNothing()
86
- .run();
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
- for (const [cid, bytes] of Array.from(blocks)) {
91
- await this.put(cid, bytes);
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
+ }