@alteran/astro 0.3.8 → 0.5.2

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 (136) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +19 -30
  3. package/index.js +34 -28
  4. package/migrations/0007_bored_spitfire.sql +26 -0
  5. package/migrations/0008_furry_ozymandias.sql +2 -0
  6. package/migrations/meta/0007_snapshot.json +534 -0
  7. package/migrations/meta/0008_snapshot.json +548 -0
  8. package/migrations/meta/_journal.json +14 -0
  9. package/package.json +10 -9
  10. package/src/app.ts +8 -4
  11. package/src/db/account.ts +25 -6
  12. package/src/db/dal.ts +34 -23
  13. package/src/db/repo.ts +35 -35
  14. package/src/db/schema.ts +5 -1
  15. package/src/db/seed.ts +5 -13
  16. package/src/entrypoints/server.ts +2 -22
  17. package/src/handlers/root.ts +4 -4
  18. package/src/lib/account-state.ts +156 -0
  19. package/src/lib/actor.ts +28 -12
  20. package/src/lib/appview/auth-policy.ts +66 -0
  21. package/src/lib/appview/did-resolver.ts +233 -0
  22. package/src/lib/appview/proxy.ts +221 -0
  23. package/src/lib/appview/service-config.ts +61 -0
  24. package/src/lib/appview/service-jwt.ts +93 -0
  25. package/src/lib/appview/types.ts +25 -0
  26. package/src/lib/appview.ts +5 -532
  27. package/src/lib/auth-errors.ts +24 -0
  28. package/src/lib/auth.ts +63 -15
  29. package/src/lib/blockstore-gc.ts +2 -1
  30. package/src/lib/cache.ts +30 -4
  31. package/src/lib/chat.ts +14 -8
  32. package/src/lib/commit.ts +26 -36
  33. package/src/lib/config.ts +26 -15
  34. package/src/lib/did-document.ts +32 -0
  35. package/src/lib/errors.ts +54 -0
  36. package/src/lib/feed.ts +18 -19
  37. package/src/lib/firehose/frames.ts +87 -47
  38. package/src/lib/firehose/validation.ts +3 -3
  39. package/src/lib/jwt.ts +85 -177
  40. package/src/lib/labeler.ts +43 -30
  41. package/src/lib/logger.ts +4 -0
  42. package/src/lib/mst/block-map.ts +172 -0
  43. package/src/lib/mst/blockstore.ts +56 -93
  44. package/src/lib/mst/index.ts +1 -0
  45. package/src/lib/mst/leaf.ts +25 -0
  46. package/src/lib/mst/mst.ts +81 -237
  47. package/src/lib/mst/serialize.ts +97 -0
  48. package/src/lib/mst/types.ts +21 -0
  49. package/src/lib/oauth/clients.ts +67 -0
  50. package/src/lib/oauth/dpop-errors.ts +15 -0
  51. package/src/lib/oauth/dpop.ts +150 -0
  52. package/src/lib/oauth/resource.ts +199 -0
  53. package/src/lib/oauth/store.ts +77 -0
  54. package/src/lib/preferences.ts +9 -34
  55. package/src/lib/refresh-session.ts +161 -0
  56. package/src/lib/relay.ts +10 -8
  57. package/src/lib/secrets.ts +6 -7
  58. package/src/lib/sequencer.ts +12 -3
  59. package/src/lib/service-auth.ts +184 -0
  60. package/src/lib/session-tokens.ts +28 -76
  61. package/src/lib/streaming-car.ts +3 -0
  62. package/src/lib/tracing.ts +4 -3
  63. package/src/lib/util.ts +65 -15
  64. package/src/middleware.ts +1 -1
  65. package/src/pages/.well-known/did.json.ts +27 -30
  66. package/src/pages/.well-known/oauth-authorization-server.ts +31 -0
  67. package/src/pages/.well-known/oauth-protected-resource.ts +22 -0
  68. package/src/pages/debug/record.ts +1 -1
  69. package/src/pages/debug/sequencer.ts +28 -0
  70. package/src/pages/oauth/authorize.ts +78 -0
  71. package/src/pages/oauth/consent.ts +80 -0
  72. package/src/pages/oauth/par.ts +121 -0
  73. package/src/pages/oauth/token.ts +158 -0
  74. package/src/pages/xrpc/[...nsid].ts +61 -0
  75. package/src/pages/xrpc/app.bsky.actor.getPreferences.ts +12 -13
  76. package/src/pages/xrpc/app.bsky.actor.putPreferences.ts +23 -23
  77. package/src/pages/xrpc/app.bsky.unspecced.getAgeAssuranceState.ts +9 -2
  78. package/src/pages/xrpc/chat.bsky.convo.getLog.ts +9 -2
  79. package/src/pages/xrpc/chat.bsky.convo.listConvos.ts +9 -2
  80. package/src/pages/xrpc/com.atproto.identity.getRecommendedDidCredentials.ts +43 -41
  81. package/src/pages/xrpc/com.atproto.identity.requestPlcOperationSignature.ts +10 -3
  82. package/src/pages/xrpc/com.atproto.identity.resolveHandle.ts +40 -9
  83. package/src/pages/xrpc/com.atproto.identity.signPlcOperation.ts +41 -29
  84. package/src/pages/xrpc/com.atproto.identity.submitPlcOperation.ts +20 -6
  85. package/src/pages/xrpc/com.atproto.identity.updateHandle.ts +1 -1
  86. package/src/pages/xrpc/com.atproto.repo.applyWrites.ts +101 -11
  87. package/src/pages/xrpc/com.atproto.repo.createRecord.ts +44 -14
  88. package/src/pages/xrpc/com.atproto.repo.deleteRecord.ts +41 -13
  89. package/src/pages/xrpc/com.atproto.repo.describeRepo.ts +2 -2
  90. package/src/pages/xrpc/com.atproto.repo.getRecord.ts +14 -1
  91. package/src/pages/xrpc/com.atproto.repo.listMissingBlobs.ts +14 -6
  92. package/src/pages/xrpc/com.atproto.repo.listRecords.ts +1 -1
  93. package/src/pages/xrpc/com.atproto.repo.putRecord.ts +42 -14
  94. package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +76 -15
  95. package/src/pages/xrpc/com.atproto.server.checkAccountStatus.ts +20 -8
  96. package/src/pages/xrpc/com.atproto.server.createSession.ts +31 -11
  97. package/src/pages/xrpc/com.atproto.server.describeServer.ts +1 -1
  98. package/src/pages/xrpc/com.atproto.server.getServiceAuth.ts +12 -5
  99. package/src/pages/xrpc/com.atproto.server.getSession.ts +22 -8
  100. package/src/pages/xrpc/com.atproto.server.refreshSession.ts +30 -72
  101. package/src/pages/xrpc/com.atproto.sync.getBlob.ts +71 -22
  102. package/src/pages/xrpc/com.atproto.sync.getCheckout.json.ts +1 -1
  103. package/src/pages/xrpc/com.atproto.sync.getCheckout.ts +1 -1
  104. package/src/pages/xrpc/com.atproto.sync.getHead.ts +7 -2
  105. package/src/pages/xrpc/com.atproto.sync.getLatestCommit.ts +1 -1
  106. package/src/pages/xrpc/com.atproto.sync.getRecord.ts +5 -27
  107. package/src/pages/xrpc/com.atproto.sync.getRepo.json.ts +1 -1
  108. package/src/pages/xrpc/com.atproto.sync.getRepo.ts +50 -5
  109. package/src/pages/xrpc/com.atproto.sync.getRepoStatus.ts +58 -0
  110. package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +1 -1
  111. package/src/pages/xrpc/com.atproto.sync.listRepos.ts +5 -3
  112. package/src/services/car.ts +207 -55
  113. package/src/services/r2-blob-store.ts +1 -1
  114. package/src/services/repo/blockstore-ops.ts +29 -0
  115. package/src/services/repo/operations.ts +133 -0
  116. package/src/services/repo-manager.ts +202 -253
  117. package/src/worker/runtime.ts +53 -8
  118. package/src/worker/sequencer/broadcast.ts +91 -0
  119. package/src/worker/sequencer/cid-helpers.ts +39 -0
  120. package/src/worker/sequencer/payload.ts +84 -0
  121. package/src/worker/sequencer/types.ts +36 -0
  122. package/src/worker/sequencer/upgrade.ts +141 -0
  123. package/src/worker/sequencer.ts +288 -412
  124. package/types/env.d.ts +15 -3
  125. package/src/pages/xrpc/app.bsky.actor.getProfile.ts +0 -49
  126. package/src/pages/xrpc/app.bsky.actor.getProfiles.ts +0 -51
  127. package/src/pages/xrpc/app.bsky.feed.getActorFeeds.ts +0 -25
  128. package/src/pages/xrpc/app.bsky.feed.getAuthorFeed.ts +0 -42
  129. package/src/pages/xrpc/app.bsky.feed.getFeedGenerators.ts +0 -25
  130. package/src/pages/xrpc/app.bsky.feed.getPostThread.ts +0 -37
  131. package/src/pages/xrpc/app.bsky.feed.getPosts.ts +0 -26
  132. package/src/pages/xrpc/app.bsky.feed.getTimeline.ts +0 -47
  133. package/src/pages/xrpc/app.bsky.graph.getFollowers.ts +0 -29
  134. package/src/pages/xrpc/app.bsky.graph.getFollows.ts +0 -29
  135. package/src/pages/xrpc/app.bsky.notification.getUnreadCount.ts +0 -20
  136. package/src/pages/xrpc/app.bsky.notification.listNotifications.ts +0 -27
@@ -5,25 +5,26 @@ import { CID } from 'multiformats/cid';
5
5
  /**
6
6
  * Frame types for AT Protocol firehose
7
7
  */
8
- export enum FrameType {
9
- Message = 1,
10
- Error = -1,
11
- }
8
+ export const FrameType = {
9
+ Message: 1,
10
+ Error: -1,
11
+ } as const;
12
+ export type FrameType = (typeof FrameType)[keyof typeof FrameType];
12
13
 
13
14
  /**
14
15
  * Frame header structure
15
16
  */
16
17
  export interface FrameHeader {
17
- op: FrameType;
18
- t?: string; // Message type discriminator
18
+ readonly op: FrameType;
19
+ readonly t?: string; // Message type discriminator
19
20
  }
20
21
 
21
22
  /**
22
23
  * Error frame body
23
24
  */
24
25
  export interface ErrorFrameBody {
25
- error: string;
26
- message?: string;
26
+ readonly error: string;
27
+ readonly message?: string;
27
28
  }
28
29
 
29
30
  /**
@@ -39,6 +40,7 @@ export abstract class Frame {
39
40
 
40
41
  /**
41
42
  * Encode frame to bytes (header + body as CBOR)
43
+ * Deprecated for WS firehose: upstream expects a single CBOR object per message.
42
44
  */
43
45
  toBytes(): Uint8Array {
44
46
  const headerBytes = dagCbor.encode(this.header);
@@ -48,6 +50,7 @@ export abstract class Frame {
48
50
 
49
51
  /**
50
52
  * Encode with 4-byte big-endian length prefix (payload = header||body encoded as dag-cbor)
53
+ * Deprecated for WS firehose: kept for tests/back-compat only.
51
54
  */
52
55
  toFramedBytes(): Uint8Array {
53
56
  const payload = this.toBytes();
@@ -114,58 +117,59 @@ export class ErrorFrame extends Frame {
114
117
  */
115
118
 
116
119
  export interface InfoMessage {
117
- name: string;
118
- message?: string;
120
+ readonly name: string;
121
+ readonly message?: string;
119
122
  }
120
123
 
121
124
  export interface RepoOp {
122
- action: 'create' | 'update' | 'delete';
123
- path: string;
124
- cid: CID | null;
125
- prev?: CID;
125
+ readonly action: 'create' | 'update' | 'delete';
126
+ readonly path: string;
127
+ readonly cid: CID | null;
128
+ readonly prev?: CID;
126
129
  }
127
130
 
128
131
  export interface CommitMessage {
129
- seq: number;
130
- rebase: boolean;
131
- tooBig: boolean;
132
- repo: string; // DID
133
- commit: CID;
134
- prev: CID | null;
135
- rev: string; // TID
136
- since: string | null; // Previous TID
137
- blocks: Uint8Array; // CAR bytes
138
- ops: RepoOp[];
139
- blobs: CID[];
140
- time: string; // ISO 8601
141
- prevData?: CID; // Previous MST root
132
+ readonly seq: number;
133
+ readonly rebase: boolean;
134
+ readonly tooBig: boolean;
135
+ readonly repo: string; // DID
136
+ readonly commit: CID;
137
+ readonly prev: CID | null;
138
+ readonly rev: string; // TID
139
+ readonly since: string | null; // Previous TID
140
+ readonly blocks: Uint8Array; // CAR bytes
141
+ readonly ops: readonly RepoOp[];
142
+ readonly blobs: readonly CID[];
143
+ readonly time: string; // ISO 8601
144
+ readonly prevData?: CID; // Previous MST root
142
145
  }
143
146
 
144
147
  export interface IdentityMessage {
145
- seq: number;
146
- did: string;
147
- time: string;
148
- handle?: string;
148
+ readonly seq: number;
149
+ readonly did: string;
150
+ readonly time: string;
151
+ readonly handle?: string;
149
152
  }
150
153
 
151
154
  export interface AccountMessage {
152
- seq: number;
153
- did: string;
154
- time: string;
155
- active: boolean;
156
- status?: string;
155
+ readonly seq: number;
156
+ readonly did: string;
157
+ readonly time: string;
158
+ readonly active: boolean;
159
+ readonly status?: string;
157
160
  }
158
161
 
159
162
  export interface SyncMessage {
160
- seq: number;
161
- did: string;
162
- time: string;
163
- active: boolean;
164
- status?: string;
163
+ readonly seq: number;
164
+ readonly did: string;
165
+ readonly time: string;
166
+ readonly active: boolean;
167
+ readonly status?: string;
165
168
  }
166
169
 
167
170
  /**
168
- * Create an #info frame
171
+ * Legacy helpers (frames) — kept for tests/back-compat only.
172
+ * Upstream subscribeRepos expects a single CBOR object with $type.
169
173
  */
170
174
  export function createInfoFrame(name: string, message?: string): MessageFrame<InfoMessage> {
171
175
  return new MessageFrame({ name, message }, '#info');
@@ -208,22 +212,58 @@ export function createErrorFrame(error: string, message?: string): ErrorFrame {
208
212
 
209
213
  // Binary encoders (with 4-byte length prefix)
210
214
  export function encodeInfoFrame(name: string, message?: string): Uint8Array {
211
- return createInfoFrame(name, message).toFramedBytes();
215
+ // Send as a single WebSocket message containing CBOR(header)||CBOR(body)
216
+ return createInfoFrame(name, message).toBytes();
212
217
  }
213
218
 
214
219
  export function encodeCommitFrame(data: CommitMessage): Uint8Array {
215
- return createCommitFrame(data).toFramedBytes();
220
+ return createCommitFrame(data).toBytes();
216
221
  }
217
222
 
218
223
  export function encodeIdentityFrame(data: IdentityMessage): Uint8Array {
219
- return createIdentityFrame(data).toFramedBytes();
224
+ return createIdentityFrame(data).toBytes();
220
225
  }
221
226
 
222
227
  export function encodeAccountFrame(data: AccountMessage): Uint8Array {
223
- return createAccountFrame(data).toFramedBytes();
228
+ return createAccountFrame(data).toBytes();
224
229
  }
225
230
 
226
231
  // Alias for TODO nomenclature (#sync)
227
232
  export function encodeSyncFrame(data: SyncMessage): Uint8Array {
228
- return createSyncFrame(data).toFramedBytes();
233
+ return createSyncFrame(data).toBytes();
234
+ }
235
+
236
+ // ----------------------------------------------------------------------------
237
+ // Spec-compliant builders for subscribeRepos WS messages
238
+ // Each message is a single CBOR object with a $type field
239
+ // ----------------------------------------------------------------------------
240
+
241
+ export type CommitEventObj = CommitMessage & { $type: '#commit' };
242
+ export type InfoEventObj = InfoMessage & { $type: '#info' };
243
+ export type IdentityEventObj = IdentityMessage & { $type: '#identity' };
244
+ export type AccountEventObj = AccountMessage & { $type: '#account' };
245
+ export type SyncEventObj = SyncMessage & { $type: '#sync' };
246
+
247
+ export function createCommitEvent(data: CommitMessage): CommitEventObj {
248
+ return { $type: '#commit', ...data };
249
+ }
250
+
251
+ export function createInfoEvent(name: string, message?: string): InfoEventObj {
252
+ return { $type: '#info', name, ...(message ? { message } : {}) };
253
+ }
254
+
255
+ export function createIdentityEvent(data: IdentityMessage): IdentityEventObj {
256
+ return { $type: '#identity', ...data };
257
+ }
258
+
259
+ export function createAccountEvent(data: AccountMessage): AccountEventObj {
260
+ return { $type: '#account', ...data };
261
+ }
262
+
263
+ export function createSyncEvent(data: SyncMessage): SyncEventObj {
264
+ return { $type: '#sync', ...data };
265
+ }
266
+
267
+ export function encodeEvent(obj: object): Uint8Array {
268
+ return dagCbor.encode(obj);
229
269
  }
@@ -1,9 +1,9 @@
1
- import { createErrorFrame } from './frames';
1
+ import { createInfoEvent, encodeEvent } from './frames';
2
2
 
3
3
  export function checkCursor(cursor: number, currentSeq: number): Uint8Array | null {
4
4
  if (Number.isFinite(cursor) && Number.isFinite(currentSeq) && cursor > currentSeq) {
5
- return createErrorFrame('FutureCursor', 'Cursor is ahead of current sequence').toFramedBytes();
5
+ const info = createInfoEvent('OutdatedCursor', 'Cursor is ahead of current sequence');
6
+ return encodeEvent(info);
6
7
  }
7
8
  return null;
8
9
  }
9
-
package/src/lib/jwt.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Env } from "../env";
2
+ import { AuthTokenExpiredError } from "./auth-errors";
3
+ import { errorCode } from "./errors";
2
4
  import { getRuntimeString } from "./secrets";
3
- import { base58btc } from "multiformats/bases/base58";
4
5
  import {
5
6
  issueSessionTokens,
6
7
  verifyAccessToken,
@@ -13,6 +14,9 @@ export interface JwtClaims {
13
14
  scope?: string;
14
15
  aud?: string;
15
16
  jti?: string;
17
+ iss?: string;
18
+ iat?: number;
19
+ exp?: number;
16
20
  t: "access" | "refresh";
17
21
  }
18
22
 
@@ -35,31 +39,66 @@ export async function verifyJwt(
35
39
  env: Env,
36
40
  token: string,
37
41
  ): Promise<{ valid: boolean; payload: JwtClaims } | null> {
42
+ console.error('[verifyJwt] Starting verification');
43
+
38
44
  const parts = token.split(".");
39
- if (parts.length !== 3) return null;
45
+ if (parts.length !== 3) {
46
+ console.error('[verifyJwt] Invalid token parts:', parts.length);
47
+ return null;
48
+ }
49
+
40
50
  const header = JSON.parse(
41
51
  atob(parts[0].replace(/-/g, "+").replace(/_/g, "/")),
42
52
  );
53
+ console.error('[verifyJwt] Header:', JSON.stringify(header));
43
54
 
44
55
  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;
56
+ console.error('[verifyJwt] Detected at+jwt type');
57
+ let payload;
58
+ try {
59
+ payload = await verifyAccessToken(env, token);
60
+ } catch (error) {
61
+ if (isJwtExpiredError(error)) {
62
+ console.error('[verifyJwt] Access token expired');
63
+ throw new AuthTokenExpiredError();
64
+ }
65
+ console.error('[verifyJwt] verifyAccessToken failed:', error);
66
+ return null;
67
+ }
68
+ if (!payload) {
69
+ console.error('[verifyJwt] No payload from verifyAccessToken');
70
+ return null;
71
+ }
72
+ if (!payload.sub) {
73
+ console.error('[verifyJwt] No sub in payload');
74
+ return null;
75
+ }
48
76
  const claims: JwtClaims = {
49
77
  sub: String(payload.sub),
50
78
  aud: payload.aud as string | undefined,
51
79
  scope: payload.scope as string | undefined,
52
80
  jti: payload.jti as string | undefined,
81
+ iss: (payload as any).iss as string | undefined,
82
+ iat: (payload as any).iat as number | undefined,
83
+ exp: (payload as any).exp as number | undefined,
53
84
  t: "access",
54
85
  };
55
86
  if (payload.handle) {
56
87
  claims.handle = String(payload.handle);
57
88
  }
89
+ console.error('[verifyJwt] at+jwt verified successfully');
58
90
  return { valid: true, payload: claims };
59
91
  }
60
92
 
61
93
  if (header.typ === "refresh+jwt") {
62
- const verified = await verifyRefreshToken(env, token).catch(() => null);
94
+ console.error('[verifyJwt] Detected refresh+jwt type');
95
+ const verified = await verifyRefreshToken(env, token).catch((error) => {
96
+ if (isJwtExpiredError(error)) {
97
+ console.error('[verifyJwt] Refresh token expired');
98
+ throw new AuthTokenExpiredError();
99
+ }
100
+ return null;
101
+ });
63
102
  if (!verified) return null;
64
103
  if (!verified.payload.sub) return null;
65
104
  const payload: JwtClaims = {
@@ -67,32 +106,52 @@ export async function verifyJwt(
67
106
  aud: verified.payload.aud as string | undefined,
68
107
  scope: verified.payload.scope as string | undefined,
69
108
  jti: verified.payload.jti as string | undefined,
109
+ iss: (verified.payload as any).iss as string | undefined,
110
+ iat: (verified.payload as any).iat as number | undefined,
111
+ exp: (verified.payload as any).exp as number | undefined,
70
112
  t: "refresh",
71
113
  };
114
+ console.error('[verifyJwt] refresh+jwt verified successfully');
72
115
  return { valid: true, payload };
73
116
  }
74
117
 
118
+ console.error('[verifyJwt] Fallback to legacy JWT verification');
75
119
  const payload = JSON.parse(
76
120
  atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")),
77
121
  );
122
+ console.error('[verifyJwt] Payload type:', payload.t);
78
123
 
79
124
  let ok = false;
80
125
  if (header.alg === "HS256" && header.typ === "JWT") {
126
+ console.error('[verifyJwt] Using HS256 verification');
81
127
  const secret = await getRuntimeString(
82
128
  env,
83
129
  payload.t === "refresh" ? "REFRESH_TOKEN_SECRET" : "REFRESH_TOKEN",
84
130
  payload.t === "refresh" ? "dev-refresh" : "dev-access",
85
131
  );
86
- if (!secret) return null;
132
+ if (!secret) {
133
+ console.error('[verifyJwt] No secret found');
134
+ return null;
135
+ }
136
+ console.error('[verifyJwt] Secret found, verifying signature');
87
137
  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);
138
+ console.error('[verifyJwt] Signature verification:', ok);
90
139
  } else {
140
+ console.error('[verifyJwt] Unsupported alg/typ:', header.alg, header.typ);
91
141
  return null;
92
142
  }
93
143
 
94
144
  const now = Math.floor(Date.now() / 1000);
95
- if (!ok || (payload.exp && now > payload.exp)) return null;
145
+ if (!ok) {
146
+ console.error('[verifyJwt] Signature verification failed');
147
+ return null;
148
+ }
149
+ if (payload.exp && now > payload.exp) {
150
+ console.error('[verifyJwt] Token expired. Now:', now, 'Exp:', payload.exp);
151
+ throw new AuthTokenExpiredError();
152
+ }
153
+
154
+ console.error('[verifyJwt] Legacy JWT verified successfully');
96
155
  return { valid: true, payload: payload as JwtClaims };
97
156
  }
98
157
 
@@ -102,9 +161,11 @@ async function hmacJwtSign(payload: any, secret: string): Promise<string> {
102
161
  const h = b64url(enc.encode(JSON.stringify(header)));
103
162
  const p = b64url(enc.encode(JSON.stringify(payload)));
104
163
  const data = `${h}.${p}`;
164
+ const keyBytes = enc.encode(secret);
165
+ const keyBuf = (() => { const b = new ArrayBuffer(keyBytes.byteLength); new Uint8Array(b).set(keyBytes); return b; })();
105
166
  const key = await crypto.subtle.importKey(
106
167
  "raw",
107
- enc.encode(secret),
168
+ keyBuf,
108
169
  { name: "HMAC", hash: "SHA-256" },
109
170
  false,
110
171
  ["sign"],
@@ -120,87 +181,28 @@ async function hmacJwtVerify(
120
181
  secret: string,
121
182
  ): Promise<boolean> {
122
183
  const enc = new TextEncoder();
184
+ const keyBytes = enc.encode(secret);
185
+ const keyBuf = (() => { const b = new ArrayBuffer(keyBytes.byteLength); new Uint8Array(b).set(keyBytes); return b; })();
123
186
  const key = await crypto.subtle.importKey(
124
187
  "raw",
125
- enc.encode(secret),
188
+ keyBuf,
126
189
  { name: "HMAC", hash: "SHA-256" },
127
190
  false,
128
191
  ["verify"],
129
192
  );
193
+ const sigBytes = b64urlDecode(sigB64);
194
+ const sigBuf = (() => { const b = new ArrayBuffer(sigBytes.byteLength); new Uint8Array(b).set(sigBytes); return b; })();
195
+ const dataBytes = enc.encode(data);
196
+ const dataBuf = (() => { const b = new ArrayBuffer(dataBytes.byteLength); new Uint8Array(b).set(dataBytes); return b; })();
130
197
  const ok = await crypto.subtle.verify(
131
198
  "HMAC",
132
199
  key,
133
- b64urlDecode(sigB64),
134
- enc.encode(data),
200
+ sigBuf,
201
+ dataBuf,
135
202
  );
136
203
  return !!ok;
137
204
  }
138
205
 
139
- async function eddsaJwtSign(payload: any, env: Env): Promise<string> {
140
- const enc = new TextEncoder();
141
- const header = { alg: "EdDSA", typ: "JWT" };
142
- const h = b64url(enc.encode(JSON.stringify(header)));
143
- const p = b64url(enc.encode(JSON.stringify(payload)));
144
- const data = `${h}.${p}`;
145
-
146
- // Import Ed25519 private key from env
147
- const keyData = await getRuntimeString(env, "REPO_SIGNING_KEY");
148
- if (!keyData) {
149
- throw new Error("REPO_SIGNING_KEY not configured for EdDSA JWTs");
150
- }
151
-
152
- // Decode base64 private key
153
- const keyBytes = b64urlDecode(keyData);
154
- const key = await crypto.subtle.importKey(
155
- "pkcs8",
156
- keyBytes,
157
- { name: "Ed25519" } as any,
158
- false,
159
- ["sign"],
160
- );
161
-
162
- const sig = await crypto.subtle.sign("Ed25519", key, enc.encode(data));
163
- const s = b64url(new Uint8Array(sig));
164
- return `${h}.${p}.${s}`;
165
- }
166
-
167
- async function eddsaJwtVerify(
168
- data: string,
169
- sigB64: string,
170
- env: Env,
171
- ): Promise<boolean> {
172
- const enc = new TextEncoder();
173
-
174
- // Import Ed25519 public key from env
175
- const keyData = await getRuntimeString(env, "REPO_SIGNING_KEY_PUBLIC");
176
- if (!keyData) {
177
- console.error(
178
- "EdDSA JWT verification failed: REPO_SIGNING_KEY_PUBLIC not configured",
179
- );
180
- return false;
181
- }
182
-
183
- try {
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
- }
191
-
192
- const ok = await crypto.subtle.verify(
193
- "Ed25519",
194
- key,
195
- b64urlDecode(sigB64),
196
- enc.encode(data),
197
- );
198
- return !!ok;
199
- } catch (error) {
200
- console.error("EdDSA JWT verification error:", error);
201
- return false;
202
- }
203
- }
204
206
 
205
207
  function b64url(bytes: ArrayBuffer | Uint8Array): string {
206
208
  const b = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
@@ -219,103 +221,9 @@ function b64urlDecode(s: string): Uint8Array {
219
221
  return out;
220
222
  }
221
223
 
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
- }
224
+ // EdDSA (Ed25519) path removed; only HS256 session tokens are supported
302
225
 
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;
226
+ function isJwtExpiredError(error: unknown): boolean {
227
+ return error instanceof AuthTokenExpiredError ||
228
+ errorCode(error) === "ERR_JWT_EXPIRED";
321
229
  }