@alteran/astro 0.3.9 → 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 +263 -405
- 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.getSuggestedFeeds.ts +0 -23
- 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
- package/src/pages/xrpc/app.bsky.unspecced.getSuggestedFeeds.ts +0 -23
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
-
import {
|
|
2
|
+
import { errorMessage } from '../../lib/errors';
|
|
3
|
+
import { AuthTokenExpiredError, expiredToken, isAuthorized, unauthorized } from '../../lib/auth';
|
|
3
4
|
import { getAccountState } from '../../db/dal';
|
|
5
|
+
import { toWireStatus } from '../../lib/account-state';
|
|
4
6
|
import { getDb } from '../../db/client';
|
|
5
7
|
import { repo_root, record, blob_ref, commit_log } from '../../db/schema';
|
|
6
8
|
import { eq, count } from 'drizzle-orm';
|
|
@@ -20,15 +22,24 @@ export const prerender = false;
|
|
|
20
22
|
export async function GET({ locals, request }: APIContext) {
|
|
21
23
|
const { env } = locals.runtime;
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
try {
|
|
26
|
+
if (!(await isAuthorized(request, env))) return unauthorized();
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error instanceof AuthTokenExpiredError) {
|
|
29
|
+
return expiredToken();
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
24
33
|
|
|
25
34
|
try {
|
|
26
|
-
const did = env.PDS_DID ?? 'did:example:single-user';
|
|
35
|
+
const did = String(env.PDS_DID ?? 'did:example:single-user');
|
|
27
36
|
const db = getDb(env);
|
|
28
37
|
|
|
29
|
-
// Get account state
|
|
38
|
+
// Get account state. No row means an unmigrated account, treated as active
|
|
39
|
+
// for backward compatibility.
|
|
30
40
|
const accountState = await getAccountState(env, did);
|
|
31
|
-
const
|
|
41
|
+
const wire = accountState ? toWireStatus(accountState) : { active: true };
|
|
42
|
+
const { active, status } = wire;
|
|
32
43
|
|
|
33
44
|
// Get repo head
|
|
34
45
|
const repoRoot = await db
|
|
@@ -65,6 +76,7 @@ export async function GET({ locals, request }: APIContext) {
|
|
|
65
76
|
JSON.stringify({
|
|
66
77
|
did,
|
|
67
78
|
active,
|
|
79
|
+
...(status ? { status } : {}),
|
|
68
80
|
head: repoRoot?.commitCid ?? null,
|
|
69
81
|
rev: repoRoot?.rev ?? 0,
|
|
70
82
|
recordCount,
|
|
@@ -80,13 +92,13 @@ export async function GET({ locals, request }: APIContext) {
|
|
|
80
92
|
}),
|
|
81
93
|
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
82
94
|
);
|
|
83
|
-
} catch (error
|
|
95
|
+
} catch (error) {
|
|
84
96
|
return new Response(
|
|
85
97
|
JSON.stringify({
|
|
86
98
|
error: 'InternalServerError',
|
|
87
|
-
message: error
|
|
99
|
+
message: errorMessage(error) || 'Failed to check account status'
|
|
88
100
|
}),
|
|
89
101
|
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
90
102
|
);
|
|
91
103
|
}
|
|
92
|
-
}
|
|
104
|
+
}
|
|
@@ -7,6 +7,7 @@ import { createAccount, getAccountByIdentifier, storeRefreshToken } from '../../
|
|
|
7
7
|
import { hashPassword, verifyPassword } from '../../lib/password';
|
|
8
8
|
import { issueSessionTokens } from '../../lib/session-tokens';
|
|
9
9
|
import { getRuntimeString } from '../../lib/secrets';
|
|
10
|
+
import { buildDidDocument } from '../../lib/did-document';
|
|
10
11
|
|
|
11
12
|
export const prerender = false;
|
|
12
13
|
|
|
@@ -33,23 +34,28 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
33
34
|
);
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
const
|
|
37
|
-
const
|
|
37
|
+
const rawBody = await readJson(request).catch(() => ({}));
|
|
38
|
+
const body = (rawBody ?? {}) as { identifier?: unknown; password?: unknown };
|
|
39
|
+
const identifier: string =
|
|
40
|
+
typeof body.identifier === 'string' && body.identifier
|
|
41
|
+
? body.identifier
|
|
42
|
+
: (await getRuntimeString(env, 'PDS_HANDLE', 'user.example')) ?? 'user.example';
|
|
38
43
|
const password = typeof body.password === 'string' ? body.password : '';
|
|
39
44
|
|
|
40
|
-
let account = await getAccountByIdentifier(env, identifier
|
|
45
|
+
let account = await getAccountByIdentifier(env, identifier);
|
|
41
46
|
if (!account) {
|
|
42
47
|
const fallbackPassword = await getRuntimeString(env, 'USER_PASSWORD', '');
|
|
43
48
|
if (fallbackPassword) {
|
|
44
|
-
const fallbackDid =
|
|
45
|
-
|
|
49
|
+
const fallbackDid =
|
|
50
|
+
(await getRuntimeString(env, 'PDS_DID', 'did:example:single-user')) ?? 'did:example:single-user';
|
|
51
|
+
const fallbackHandle = (await getRuntimeString(env, 'PDS_HANDLE', identifier)) ?? identifier;
|
|
46
52
|
const hashed = await hashPassword(fallbackPassword);
|
|
47
53
|
await createAccount(env, {
|
|
48
54
|
did: fallbackDid,
|
|
49
55
|
handle: fallbackHandle,
|
|
50
56
|
passwordScrypt: hashed,
|
|
51
57
|
});
|
|
52
|
-
account = await getAccountByIdentifier(env, identifier
|
|
58
|
+
account = await getAccountByIdentifier(env, identifier);
|
|
53
59
|
}
|
|
54
60
|
}
|
|
55
61
|
const passwordHash = account?.passwordScrypt ?? null;
|
|
@@ -102,8 +108,8 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
102
108
|
await db.delete(login_attempts).where(eq(login_attempts.ip, clientIp)).run();
|
|
103
109
|
}
|
|
104
110
|
|
|
105
|
-
const did = account?.did ?? (await getRuntimeString(env, 'PDS_DID', 'did:example:single-user'));
|
|
106
|
-
const handle = account?.handle ?? (await getRuntimeString(env, 'PDS_HANDLE', identifier ?? 'user.example'));
|
|
111
|
+
const did = (account?.did ?? (await getRuntimeString(env, 'PDS_DID', 'did:example:single-user')) ?? 'did:example:single-user');
|
|
112
|
+
const handle = (account?.handle ?? (await getRuntimeString(env, 'PDS_HANDLE', identifier ?? 'user.example')) ?? (identifier ?? 'user.example'));
|
|
107
113
|
|
|
108
114
|
const { accessJwt, refreshJwt, refreshPayload, refreshExpiry } = await issueSessionTokens(env, did);
|
|
109
115
|
|
|
@@ -114,7 +120,21 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
114
120
|
appPasswordName: null,
|
|
115
121
|
});
|
|
116
122
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
// Build didDoc for the response (required by official API contract)
|
|
124
|
+
const didDoc = await buildDidDocument(env, did, handle);
|
|
125
|
+
|
|
126
|
+
const email = account?.email ?? (env.PDS_EMAIL as string | undefined);
|
|
127
|
+
|
|
128
|
+
return new Response(
|
|
129
|
+
JSON.stringify({
|
|
130
|
+
did,
|
|
131
|
+
didDoc,
|
|
132
|
+
handle,
|
|
133
|
+
accessJwt,
|
|
134
|
+
refreshJwt,
|
|
135
|
+
active: true,
|
|
136
|
+
...(email ? { email, emailConfirmed: true, emailAuthFactor: false } : {}),
|
|
137
|
+
}),
|
|
138
|
+
{ headers: { 'Content-Type': 'application/json' } },
|
|
139
|
+
);
|
|
120
140
|
}
|
|
@@ -5,7 +5,7 @@ export const prerender = false;
|
|
|
5
5
|
|
|
6
6
|
export function GET({ locals }: APIContext) {
|
|
7
7
|
const { env } = locals.runtime;
|
|
8
|
-
const did =
|
|
8
|
+
const did = env.PDS_DID as string;
|
|
9
9
|
const availableUserDomains: string[] = [];
|
|
10
10
|
|
|
11
11
|
const links = typeof env.PDS_LINK_PRIVACY === 'string' || typeof env.PDS_LINK_TOS === 'string'
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
-
import {
|
|
2
|
+
import { verifyResourceRequestHybrid, dpopResourceUnauthorized, handleResourceAuthError } from '../../lib/oauth/resource';
|
|
3
3
|
import { createServiceAuthToken } from '../../lib/appview';
|
|
4
4
|
|
|
5
5
|
export const prerender = false;
|
|
6
6
|
|
|
7
7
|
export async function GET({ locals, request }: APIContext) {
|
|
8
8
|
const { env } = locals.runtime;
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
let auth: { did: string; token: string } | null = null;
|
|
10
|
+
try {
|
|
11
|
+
auth = await verifyResourceRequestHybrid(env, request);
|
|
12
|
+
if (!auth) return dpopResourceUnauthorized(env);
|
|
13
|
+
} catch (error) {
|
|
14
|
+
const handled = await handleResourceAuthError(env, error);
|
|
15
|
+
if (handled) return handled;
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
11
18
|
|
|
12
19
|
const url = new URL(request.url);
|
|
13
20
|
const audienceParam = url.searchParams.get('aud');
|
|
@@ -50,11 +57,11 @@ export async function GET({ locals, request }: APIContext) {
|
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
try {
|
|
53
|
-
const token = await createServiceAuthToken(env, auth.
|
|
60
|
+
const token = await createServiceAuthToken(env, auth.did, audience, lexiconMethod, expiresIn);
|
|
54
61
|
return new Response(JSON.stringify({ token }), {
|
|
55
62
|
headers: { 'Content-Type': 'application/json' },
|
|
56
63
|
});
|
|
57
|
-
} catch (error
|
|
64
|
+
} catch (error) {
|
|
58
65
|
console.error('service auth error:', error);
|
|
59
66
|
return new Response(JSON.stringify({ error: 'InternalServerError' }), {
|
|
60
67
|
status: 500,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
+
import { AuthTokenExpiredError, authenticateRequest, expiredToken, unauthorized } from '../../lib/auth';
|
|
3
|
+
import { getAccountByIdentifier } from '../../db/account';
|
|
2
4
|
|
|
3
5
|
export const prerender = false;
|
|
4
6
|
|
|
@@ -6,20 +8,32 @@ export const prerender = false;
|
|
|
6
8
|
* com.atproto.server.getSession
|
|
7
9
|
* Get information about the current session
|
|
8
10
|
*/
|
|
9
|
-
export async function GET({ locals }: APIContext) {
|
|
11
|
+
export async function GET({ locals, request }: APIContext) {
|
|
10
12
|
const { env } = locals.runtime;
|
|
11
13
|
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
+
// Validate the access token
|
|
15
|
+
let authContext;
|
|
16
|
+
try {
|
|
17
|
+
authContext = await authenticateRequest(request, env);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (error instanceof AuthTokenExpiredError) {
|
|
20
|
+
return expiredToken();
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
if (!authContext) {
|
|
25
|
+
return unauthorized();
|
|
26
|
+
}
|
|
14
27
|
|
|
15
|
-
const did =
|
|
16
|
-
const
|
|
28
|
+
const did = authContext.claims.sub;
|
|
29
|
+
const account = await getAccountByIdentifier(env, did);
|
|
30
|
+
const handle = account?.handle ?? (env.PDS_HANDLE as string) ?? 'user.example.com';
|
|
17
31
|
|
|
18
32
|
return new Response(
|
|
19
33
|
JSON.stringify({
|
|
20
34
|
did,
|
|
21
35
|
handle,
|
|
22
|
-
email: 'user@example.com',
|
|
36
|
+
email: account?.email ?? (env.PDS_EMAIL as string | undefined) ?? 'user@example.com',
|
|
23
37
|
emailConfirmed: true,
|
|
24
38
|
emailAuthFactor: false,
|
|
25
39
|
didDoc: {
|
|
@@ -31,7 +45,7 @@ export async function GET({ locals }: APIContext) {
|
|
|
31
45
|
{
|
|
32
46
|
id: '#atproto_pds',
|
|
33
47
|
type: 'AtprotoPersonalDataServer',
|
|
34
|
-
serviceEndpoint: `https://${handle}`,
|
|
48
|
+
serviceEndpoint: `https://${env.PDS_HOSTNAME ?? handle}`,
|
|
35
49
|
},
|
|
36
50
|
],
|
|
37
51
|
},
|
|
@@ -43,4 +57,4 @@ export async function GET({ locals }: APIContext) {
|
|
|
43
57
|
},
|
|
44
58
|
}
|
|
45
59
|
);
|
|
46
|
-
};
|
|
60
|
+
};
|
|
@@ -1,90 +1,48 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
2
|
import { bearerToken } from '../../lib/util';
|
|
3
3
|
import { lazyCleanupExpiredTokens } from '../../lib/token-cleanup';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { attemptRefresh, type RefreshOutcome } from '../../lib/refresh-session';
|
|
5
|
+
import { buildDidDocument } from '../../lib/did-document';
|
|
6
|
+
import { getAccountState } from '../../db/dal';
|
|
7
|
+
import { getAccountByIdentifier } from '../../db/account';
|
|
8
|
+
import { toWireStatus } from '../../lib/account-state';
|
|
7
9
|
|
|
8
10
|
export const prerender = false;
|
|
9
11
|
|
|
10
12
|
export async function POST({ locals, request }: APIContext) {
|
|
11
13
|
const { env } = locals.runtime;
|
|
12
14
|
const token = bearerToken(request);
|
|
13
|
-
if (!token) {
|
|
14
|
-
return new Response(
|
|
15
|
-
JSON.stringify({ error: 'AuthRequired', message: 'No authorization token provided' }),
|
|
16
|
-
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const verification = await verifyRefreshToken(env, token).catch(() => null);
|
|
21
|
-
if (!verification) {
|
|
22
|
-
return new Response(
|
|
23
|
-
JSON.stringify({ error: 'InvalidToken', message: 'Invalid or expired refresh token' }),
|
|
24
|
-
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
15
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
29
|
-
const { decoded } = verification;
|
|
30
16
|
|
|
31
|
-
|
|
32
|
-
return new Response(
|
|
33
|
-
JSON.stringify({ error: 'InvalidToken', message: 'Malformed refresh token' }),
|
|
34
|
-
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (typeof decoded.exp !== 'number' || decoded.exp <= nowSec) {
|
|
39
|
-
return new Response(
|
|
40
|
-
JSON.stringify({ error: 'ExpiredToken', message: 'Refresh token expired' }),
|
|
41
|
-
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
|
42
|
-
);
|
|
43
|
-
}
|
|
17
|
+
const outcome: RefreshOutcome = await attemptRefresh({ env, token, nowSec });
|
|
44
18
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return new Response(
|
|
48
|
-
JSON.stringify({ error: 'InvalidToken', message: 'Refresh token has been revoked' }),
|
|
49
|
-
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (stored.expiresAt <= nowSec) {
|
|
54
|
-
return new Response(
|
|
55
|
-
JSON.stringify({ error: 'ExpiredToken', message: 'Refresh token expired' }),
|
|
56
|
-
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
|
57
|
-
);
|
|
58
|
-
}
|
|
19
|
+
// Cleanup is best-effort and runs ~1% of the time regardless of outcome.
|
|
20
|
+
lazyCleanupExpiredTokens(env).catch(console.error);
|
|
59
21
|
|
|
60
|
-
if (
|
|
22
|
+
if (outcome.tag === 'failure') {
|
|
61
23
|
return new Response(
|
|
62
|
-
JSON.stringify({ error:
|
|
63
|
-
{ status:
|
|
24
|
+
JSON.stringify({ error: outcome.code, message: outcome.message }),
|
|
25
|
+
{ status: outcome.status, headers: { 'Content-Type': 'application/json' } },
|
|
64
26
|
);
|
|
65
27
|
}
|
|
66
28
|
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return new Response(JSON.stringify({ did, handle, accessJwt, refreshJwt }), {
|
|
88
|
-
headers: { 'Content-Type': 'application/json' },
|
|
89
|
-
});
|
|
29
|
+
const didDoc = await buildDidDocument(env, outcome.did, outcome.handle);
|
|
30
|
+
const accountState = await getAccountState(env, outcome.did);
|
|
31
|
+
const wire = accountState ? toWireStatus(accountState) : { active: true };
|
|
32
|
+
const account = await getAccountByIdentifier(env, outcome.did);
|
|
33
|
+
const email = account?.email ?? (env.PDS_EMAIL as string | undefined);
|
|
34
|
+
|
|
35
|
+
return new Response(
|
|
36
|
+
JSON.stringify({
|
|
37
|
+
did: outcome.did,
|
|
38
|
+
didDoc,
|
|
39
|
+
handle: outcome.handle,
|
|
40
|
+
accessJwt: outcome.accessJwt,
|
|
41
|
+
refreshJwt: outcome.refreshJwt,
|
|
42
|
+
active: wire.active,
|
|
43
|
+
...(wire.status ? { status: wire.status } : {}),
|
|
44
|
+
...(email ? { email, emailConfirmed: true, emailAuthFactor: false } : {}),
|
|
45
|
+
}),
|
|
46
|
+
{ headers: { 'Content-Type': 'application/json' } },
|
|
47
|
+
);
|
|
90
48
|
}
|
|
@@ -2,6 +2,10 @@ import type { APIContext } from 'astro';
|
|
|
2
2
|
import { getDb } from '../../db/client';
|
|
3
3
|
import { blob_ref } from '../../db/schema';
|
|
4
4
|
import { eq } from 'drizzle-orm';
|
|
5
|
+
import { CID } from 'multiformats/cid';
|
|
6
|
+
import { sha256 } from 'multiformats/hashes/sha2';
|
|
7
|
+
import { putBlobRef } from '../../db/dal';
|
|
8
|
+
import { isAccountActive } from '../../db/dal';
|
|
5
9
|
|
|
6
10
|
export const prerender = false;
|
|
7
11
|
|
|
@@ -19,66 +23,111 @@ export async function GET({ locals, url }: APIContext) {
|
|
|
19
23
|
const { env } = locals.runtime;
|
|
20
24
|
|
|
21
25
|
try {
|
|
26
|
+
const configuredDid = typeof env.PDS_DID === 'string' ? env.PDS_DID : undefined;
|
|
27
|
+
const did = url.searchParams.get('did') ?? configuredDid;
|
|
22
28
|
const cid = url.searchParams.get('cid');
|
|
23
|
-
if (!cid) {
|
|
29
|
+
if (!did || !cid) {
|
|
24
30
|
return new Response(
|
|
25
31
|
JSON.stringify({
|
|
26
32
|
error: 'InvalidRequest',
|
|
27
|
-
message: 'cid
|
|
33
|
+
message: 'did and cid parameters are required'
|
|
28
34
|
}),
|
|
29
35
|
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
30
36
|
);
|
|
31
37
|
}
|
|
32
38
|
|
|
39
|
+
const active = await isAccountActive(env, did);
|
|
40
|
+
if (!active) {
|
|
41
|
+
return new Response(
|
|
42
|
+
JSON.stringify({ error: 'AccountInactive', message: 'Account is not active' }),
|
|
43
|
+
{ status: 403, headers: { 'Content-Type': 'application/json' } }
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
const db = getDb(env);
|
|
34
48
|
|
|
35
49
|
// Look up blob metadata by CID
|
|
36
|
-
|
|
50
|
+
let blobMeta = await db
|
|
37
51
|
.select()
|
|
38
52
|
.from(blob_ref)
|
|
39
53
|
.where(eq(blob_ref.cid, cid))
|
|
40
54
|
.get();
|
|
41
55
|
|
|
42
|
-
|
|
56
|
+
let key: string | null = blobMeta?.key ?? null;
|
|
57
|
+
let mime: string = blobMeta?.mime ?? 'application/octet-stream';
|
|
58
|
+
let size: number | null = blobMeta?.size ?? null;
|
|
59
|
+
|
|
60
|
+
// Fallback for older uploads: derive R2 key from CID (raw/sha256) if DB row missing
|
|
61
|
+
if (!key) {
|
|
62
|
+
try {
|
|
63
|
+
const link = CID.parse(cid);
|
|
64
|
+
// blob CIDs are raw (0x55) with sha256 multihash
|
|
65
|
+
if (link.multihash.code !== sha256.code) {
|
|
66
|
+
return new Response(
|
|
67
|
+
JSON.stringify({ error: 'InvalidRequest', message: 'Unsupported multihash' }),
|
|
68
|
+
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
// Recreate legacy R2 key scheme used by store.put()
|
|
72
|
+
const digest = link.multihash.digest; // Uint8Array
|
|
73
|
+
// base64url encode
|
|
74
|
+
let s = '';
|
|
75
|
+
for (const b of digest) s += String.fromCharCode(b);
|
|
76
|
+
const b64url = btoa(s).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
|
|
77
|
+
key = `blobs/by-cid/${b64url}`;
|
|
78
|
+
} catch {
|
|
79
|
+
key = null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!key) {
|
|
43
84
|
return new Response(
|
|
44
|
-
JSON.stringify({
|
|
45
|
-
|
|
46
|
-
message: 'Blob not found'
|
|
47
|
-
}),
|
|
48
|
-
{ status: 404, headers: { 'Content-Type': 'application/json' } }
|
|
85
|
+
JSON.stringify({ error: 'InvalidRequest', message: 'Blob not found' }),
|
|
86
|
+
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
49
87
|
);
|
|
50
88
|
}
|
|
51
89
|
|
|
52
90
|
// Fetch blob from R2
|
|
53
91
|
const r2 = env.BLOBS;
|
|
54
|
-
const object = await r2.get(
|
|
92
|
+
const object = await r2.get(key);
|
|
55
93
|
|
|
56
94
|
if (!object) {
|
|
57
95
|
return new Response(
|
|
58
|
-
JSON.stringify({
|
|
59
|
-
|
|
60
|
-
message: 'Blob not found in storage'
|
|
61
|
-
}),
|
|
62
|
-
{ status: 404, headers: { 'Content-Type': 'application/json' } }
|
|
96
|
+
JSON.stringify({ error: 'InvalidRequest', message: 'Blob not found' }),
|
|
97
|
+
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
63
98
|
);
|
|
64
99
|
}
|
|
65
100
|
|
|
66
|
-
|
|
67
|
-
|
|
101
|
+
if (!blobMeta) {
|
|
102
|
+
try {
|
|
103
|
+
size = object.size ?? size ?? 0;
|
|
104
|
+
await putBlobRef(env, did, cid, key, mime, Number(size ?? 0));
|
|
105
|
+
} catch (backfillError) {
|
|
106
|
+
// Backfill is opportunistic; serving the blob is the priority.
|
|
107
|
+
console.warn('getBlob backfill failed:', backfillError);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// workers-types' ReadableStream lacks the DOM-types readMany member; the
|
|
112
|
+
// shapes are wire-compatible at runtime, so widen through unknown.
|
|
113
|
+
return new Response(object.body as unknown as ReadableStream<Uint8Array>, {
|
|
68
114
|
status: 200,
|
|
69
115
|
headers: {
|
|
70
|
-
'Content-Type':
|
|
71
|
-
'Content-Length':
|
|
116
|
+
'Content-Type': mime,
|
|
117
|
+
...(size != null ? { 'Content-Length': String(size) } : {}),
|
|
72
118
|
'Cache-Control': 'public, max-age=31536000, immutable',
|
|
119
|
+
'x-content-type-options': 'nosniff',
|
|
120
|
+
'content-security-policy': "default-src 'none'; sandbox",
|
|
73
121
|
},
|
|
74
122
|
});
|
|
75
|
-
} catch (error
|
|
123
|
+
} catch (error) {
|
|
124
|
+
const message = error instanceof Error ? error.message : 'Failed to retrieve blob';
|
|
76
125
|
return new Response(
|
|
77
126
|
JSON.stringify({
|
|
78
127
|
error: 'InternalServerError',
|
|
79
|
-
message
|
|
128
|
+
message,
|
|
80
129
|
}),
|
|
81
130
|
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
82
131
|
);
|
|
83
132
|
}
|
|
84
|
-
}
|
|
133
|
+
}
|
|
@@ -8,7 +8,7 @@ export const prerender = false;
|
|
|
8
8
|
export async function GET({ locals, request }: APIContext) {
|
|
9
9
|
const { env } = locals.runtime;
|
|
10
10
|
const url = new URL(request.url);
|
|
11
|
-
const did = url.searchParams.get('did') ?? (env.PDS_DID
|
|
11
|
+
const did = url.searchParams.get('did') ?? (env.PDS_DID as string);
|
|
12
12
|
const head = await getRepoRoot(env);
|
|
13
13
|
const rows = await dalListRecords(env);
|
|
14
14
|
const records = rows
|
|
@@ -6,7 +6,7 @@ export const prerender = false;
|
|
|
6
6
|
export async function GET({ locals, request }: APIContext) {
|
|
7
7
|
const { env } = locals.runtime;
|
|
8
8
|
const url = new URL(request.url);
|
|
9
|
-
const did = url.searchParams.get('did') ?? (env.PDS_DID
|
|
9
|
+
const did = url.searchParams.get('did') ?? (env.PDS_DID as string);
|
|
10
10
|
|
|
11
11
|
// Support commit range queries
|
|
12
12
|
const fromParam = url.searchParams.get('from');
|
|
@@ -6,6 +6,11 @@ export const prerender = false;
|
|
|
6
6
|
export async function GET({ locals }: APIContext) {
|
|
7
7
|
const { env } = locals.runtime;
|
|
8
8
|
const root = await getRepoRoot(env);
|
|
9
|
-
if (!root)
|
|
10
|
-
|
|
9
|
+
if (!root) {
|
|
10
|
+
return new Response(
|
|
11
|
+
JSON.stringify({ error: 'HeadNotFound', message: 'Head not found' }),
|
|
12
|
+
{ status: 404, headers: { 'Content-Type': 'application/json' } }
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
return new Response(JSON.stringify({ root: root.commitCid }), { headers: { 'Content-Type': 'application/json' } });
|
|
11
16
|
}
|
|
@@ -10,7 +10,7 @@ export const prerender = false;
|
|
|
10
10
|
export async function GET({ locals, url }: APIContext) {
|
|
11
11
|
const { env } = locals.runtime;
|
|
12
12
|
|
|
13
|
-
const did = url.searchParams.get('did') || env.PDS_DID
|
|
13
|
+
const did = url.searchParams.get('did') || (env.PDS_DID as string);
|
|
14
14
|
|
|
15
15
|
try {
|
|
16
16
|
const root = await getRoot(env);
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
2
|
import { RepoManager } from '../../services/repo-manager';
|
|
3
|
-
import {
|
|
4
|
-
import * as dagCbor from '@ipld/dag-cbor';
|
|
5
|
-
import { CID } from 'multiformats/cid';
|
|
6
|
-
import { sha256 } from 'multiformats/hashes/sha2';
|
|
3
|
+
import { buildRecordProofCar } from '../../services/car';
|
|
7
4
|
|
|
8
5
|
export const prerender = false;
|
|
9
6
|
|
|
@@ -14,7 +11,7 @@ export const prerender = false;
|
|
|
14
11
|
export async function GET({ locals, url }: APIContext) {
|
|
15
12
|
const { env } = locals.runtime;
|
|
16
13
|
|
|
17
|
-
const did = url.searchParams.get('did') || env.PDS_DID
|
|
14
|
+
const did = url.searchParams.get('did') || (env.PDS_DID as string);
|
|
18
15
|
const collection = url.searchParams.get('collection');
|
|
19
16
|
const rkey = url.searchParams.get('rkey');
|
|
20
17
|
|
|
@@ -26,31 +23,12 @@ export async function GET({ locals, url }: APIContext) {
|
|
|
26
23
|
}
|
|
27
24
|
|
|
28
25
|
try {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (!record) {
|
|
33
|
-
return new Response(
|
|
34
|
-
JSON.stringify({ error: 'RecordNotFound' }),
|
|
35
|
-
{ status: 404, headers: { 'Content-Type': 'application/json' } }
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Encode the record as a single-block CAR snapshot (root = record CID)
|
|
40
|
-
const { cid, bytes } = await encodeRecordBlock(record);
|
|
41
|
-
|
|
42
|
-
// Minimal CAR encoding (header + single block)
|
|
43
|
-
const header = dagCbor.encode({ version: 1, roots: [cid] });
|
|
44
|
-
const varint = (n: number) => { const a:number[]=[]; while(n>=0x80){a.push((n&0x7f)|0x80); n>>>=7;} a.push(n); return new Uint8Array(a); };
|
|
45
|
-
const concat = (parts: Uint8Array[]) => { const len = parts.reduce((n,p)=>n+p.byteLength,0); const out = new Uint8Array(len); let o=0; for(const p of parts){out.set(p,o); o+=p.byteLength;} return out; };
|
|
46
|
-
const block = concat([cid.bytes, bytes]);
|
|
47
|
-
const carBytes = concat([varint(header.byteLength), header, varint(block.byteLength), block]);
|
|
48
|
-
|
|
49
|
-
return new Response(carBytes as any, {
|
|
26
|
+
const { bytes } = await buildRecordProofCar(env as any, did, collection, rkey);
|
|
27
|
+
return new Response(bytes as any, {
|
|
50
28
|
status: 200,
|
|
51
29
|
headers: {
|
|
52
30
|
'Content-Type': 'application/vnd.ipld.car; version=1',
|
|
53
|
-
'Content-Disposition': 'inline; filename="record.car"',
|
|
31
|
+
'Content-Disposition': 'inline; filename="record-proof.car"',
|
|
54
32
|
},
|
|
55
33
|
});
|
|
56
34
|
} catch (error) {
|
|
@@ -8,7 +8,7 @@ export const prerender = false;
|
|
|
8
8
|
export async function GET({ locals, request }: APIContext) {
|
|
9
9
|
const { env } = locals.runtime;
|
|
10
10
|
const url = new URL(request.url);
|
|
11
|
-
const did = url.searchParams.get('did') ?? (env.PDS_DID
|
|
11
|
+
const did = url.searchParams.get('did') ?? (env.PDS_DID as string);
|
|
12
12
|
const head = await getRepoRoot(env);
|
|
13
13
|
const rows = await dalListRecords(env);
|
|
14
14
|
const records = rows
|