@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.
- package/LICENSE +21 -0
- package/README.md +19 -30
- package/index.js +34 -28
- package/migrations/0007_bored_spitfire.sql +26 -0
- package/migrations/0008_furry_ozymandias.sql +2 -0
- package/migrations/meta/0007_snapshot.json +534 -0
- package/migrations/meta/0008_snapshot.json +548 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +10 -9
- package/src/app.ts +8 -4
- package/src/db/account.ts +25 -6
- package/src/db/dal.ts +34 -23
- package/src/db/repo.ts +35 -35
- package/src/db/schema.ts +5 -1
- package/src/db/seed.ts +5 -13
- package/src/entrypoints/server.ts +2 -22
- package/src/handlers/root.ts +4 -4
- package/src/lib/account-state.ts +156 -0
- package/src/lib/actor.ts +28 -12
- package/src/lib/appview/auth-policy.ts +66 -0
- package/src/lib/appview/did-resolver.ts +233 -0
- package/src/lib/appview/proxy.ts +221 -0
- package/src/lib/appview/service-config.ts +61 -0
- package/src/lib/appview/service-jwt.ts +93 -0
- package/src/lib/appview/types.ts +25 -0
- package/src/lib/appview.ts +5 -532
- package/src/lib/auth-errors.ts +24 -0
- package/src/lib/auth.ts +63 -15
- package/src/lib/blockstore-gc.ts +2 -1
- package/src/lib/cache.ts +30 -4
- package/src/lib/chat.ts +14 -8
- package/src/lib/commit.ts +26 -36
- package/src/lib/config.ts +26 -15
- package/src/lib/did-document.ts +32 -0
- package/src/lib/errors.ts +54 -0
- package/src/lib/feed.ts +18 -19
- package/src/lib/firehose/frames.ts +87 -47
- package/src/lib/firehose/validation.ts +3 -3
- package/src/lib/jwt.ts +85 -177
- package/src/lib/labeler.ts +43 -30
- package/src/lib/logger.ts +4 -0
- package/src/lib/mst/block-map.ts +172 -0
- package/src/lib/mst/blockstore.ts +56 -93
- package/src/lib/mst/index.ts +1 -0
- package/src/lib/mst/leaf.ts +25 -0
- package/src/lib/mst/mst.ts +81 -237
- package/src/lib/mst/serialize.ts +97 -0
- package/src/lib/mst/types.ts +21 -0
- package/src/lib/oauth/clients.ts +67 -0
- package/src/lib/oauth/dpop-errors.ts +15 -0
- package/src/lib/oauth/dpop.ts +150 -0
- package/src/lib/oauth/resource.ts +199 -0
- package/src/lib/oauth/store.ts +77 -0
- package/src/lib/preferences.ts +9 -34
- package/src/lib/refresh-session.ts +161 -0
- package/src/lib/relay.ts +10 -8
- package/src/lib/secrets.ts +6 -7
- package/src/lib/sequencer.ts +12 -3
- package/src/lib/service-auth.ts +184 -0
- package/src/lib/session-tokens.ts +28 -76
- package/src/lib/streaming-car.ts +3 -0
- package/src/lib/tracing.ts +4 -3
- package/src/lib/util.ts +65 -15
- package/src/middleware.ts +1 -1
- package/src/pages/.well-known/did.json.ts +27 -30
- package/src/pages/.well-known/oauth-authorization-server.ts +31 -0
- package/src/pages/.well-known/oauth-protected-resource.ts +22 -0
- package/src/pages/debug/record.ts +1 -1
- package/src/pages/debug/sequencer.ts +28 -0
- package/src/pages/oauth/authorize.ts +78 -0
- package/src/pages/oauth/consent.ts +80 -0
- package/src/pages/oauth/par.ts +121 -0
- package/src/pages/oauth/token.ts +158 -0
- package/src/pages/xrpc/[...nsid].ts +61 -0
- package/src/pages/xrpc/app.bsky.actor.getPreferences.ts +12 -13
- package/src/pages/xrpc/app.bsky.actor.putPreferences.ts +23 -23
- package/src/pages/xrpc/app.bsky.unspecced.getAgeAssuranceState.ts +9 -2
- package/src/pages/xrpc/chat.bsky.convo.getLog.ts +9 -2
- package/src/pages/xrpc/chat.bsky.convo.listConvos.ts +9 -2
- package/src/pages/xrpc/com.atproto.identity.getRecommendedDidCredentials.ts +43 -41
- package/src/pages/xrpc/com.atproto.identity.requestPlcOperationSignature.ts +10 -3
- package/src/pages/xrpc/com.atproto.identity.resolveHandle.ts +40 -9
- package/src/pages/xrpc/com.atproto.identity.signPlcOperation.ts +41 -29
- package/src/pages/xrpc/com.atproto.identity.submitPlcOperation.ts +20 -6
- package/src/pages/xrpc/com.atproto.identity.updateHandle.ts +1 -1
- package/src/pages/xrpc/com.atproto.repo.applyWrites.ts +101 -11
- package/src/pages/xrpc/com.atproto.repo.createRecord.ts +44 -14
- package/src/pages/xrpc/com.atproto.repo.deleteRecord.ts +41 -13
- package/src/pages/xrpc/com.atproto.repo.describeRepo.ts +2 -2
- package/src/pages/xrpc/com.atproto.repo.getRecord.ts +14 -1
- package/src/pages/xrpc/com.atproto.repo.listMissingBlobs.ts +14 -6
- package/src/pages/xrpc/com.atproto.repo.listRecords.ts +1 -1
- package/src/pages/xrpc/com.atproto.repo.putRecord.ts +42 -14
- package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +76 -15
- package/src/pages/xrpc/com.atproto.server.checkAccountStatus.ts +20 -8
- package/src/pages/xrpc/com.atproto.server.createSession.ts +31 -11
- package/src/pages/xrpc/com.atproto.server.describeServer.ts +1 -1
- package/src/pages/xrpc/com.atproto.server.getServiceAuth.ts +12 -5
- package/src/pages/xrpc/com.atproto.server.getSession.ts +22 -8
- package/src/pages/xrpc/com.atproto.server.refreshSession.ts +30 -72
- package/src/pages/xrpc/com.atproto.sync.getBlob.ts +71 -22
- package/src/pages/xrpc/com.atproto.sync.getCheckout.json.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.getCheckout.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.getHead.ts +7 -2
- package/src/pages/xrpc/com.atproto.sync.getLatestCommit.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.getRecord.ts +5 -27
- package/src/pages/xrpc/com.atproto.sync.getRepo.json.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.getRepo.ts +50 -5
- package/src/pages/xrpc/com.atproto.sync.getRepoStatus.ts +58 -0
- package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.listRepos.ts +5 -3
- package/src/services/car.ts +207 -55
- package/src/services/r2-blob-store.ts +1 -1
- package/src/services/repo/blockstore-ops.ts +29 -0
- package/src/services/repo/operations.ts +133 -0
- package/src/services/repo-manager.ts +202 -253
- package/src/worker/runtime.ts +53 -8
- package/src/worker/sequencer/broadcast.ts +91 -0
- package/src/worker/sequencer/cid-helpers.ts +39 -0
- package/src/worker/sequencer/payload.ts +84 -0
- package/src/worker/sequencer/types.ts +36 -0
- package/src/worker/sequencer/upgrade.ts +141 -0
- package/src/worker/sequencer.ts +288 -412
- package/types/env.d.ts +15 -3
- package/src/pages/xrpc/app.bsky.actor.getProfile.ts +0 -49
- package/src/pages/xrpc/app.bsky.actor.getProfiles.ts +0 -51
- package/src/pages/xrpc/app.bsky.feed.getActorFeeds.ts +0 -25
- package/src/pages/xrpc/app.bsky.feed.getAuthorFeed.ts +0 -42
- package/src/pages/xrpc/app.bsky.feed.getFeedGenerators.ts +0 -25
- package/src/pages/xrpc/app.bsky.feed.getPostThread.ts +0 -37
- package/src/pages/xrpc/app.bsky.feed.getPosts.ts +0 -26
- package/src/pages/xrpc/app.bsky.feed.getTimeline.ts +0 -47
- package/src/pages/xrpc/app.bsky.graph.getFollowers.ts +0 -29
- package/src/pages/xrpc/app.bsky.graph.getFollows.ts +0 -29
- package/src/pages/xrpc/app.bsky.notification.getUnreadCount.ts +0 -20
- 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
|
|
9
|
-
Message
|
|
10
|
-
Error
|
|
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
|
-
*
|
|
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
|
-
|
|
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).
|
|
220
|
+
return createCommitFrame(data).toBytes();
|
|
216
221
|
}
|
|
217
222
|
|
|
218
223
|
export function encodeIdentityFrame(data: IdentityMessage): Uint8Array {
|
|
219
|
-
return createIdentityFrame(data).
|
|
224
|
+
return createIdentityFrame(data).toBytes();
|
|
220
225
|
}
|
|
221
226
|
|
|
222
227
|
export function encodeAccountFrame(data: AccountMessage): Uint8Array {
|
|
223
|
-
return createAccountFrame(data).
|
|
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).
|
|
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 {
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
|
304
|
-
|
|
305
|
-
|
|
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
|
}
|