@alteran/astro 0.7.7 → 0.8.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/README.md +25 -25
- package/migrations/0010_eminent_klaw.sql +37 -0
- package/migrations/0011_chief_darwin.sql +31 -0
- package/migrations/0012_backfill_blob_usage.sql +39 -0
- package/migrations/meta/0010_snapshot.json +790 -0
- package/migrations/meta/0011_snapshot.json +813 -0
- package/migrations/meta/_journal.json +22 -1
- package/package.json +24 -41
- package/src/db/blob.ts +323 -0
- package/src/db/dal.ts +224 -78
- package/src/db/repo.ts +205 -25
- package/src/db/schema.ts +14 -5
- package/src/handlers/debug.ts +4 -3
- package/src/lib/appview/auth-policy.ts +7 -24
- package/src/lib/appview/proxy.ts +56 -23
- package/src/lib/appview/types.ts +1 -6
- package/src/lib/auth-scope.ts +399 -0
- package/src/lib/auth.ts +40 -39
- package/src/lib/commit.ts +37 -15
- package/src/lib/did-document.ts +4 -5
- package/src/lib/jwt.ts +3 -1
- package/src/lib/mime.ts +9 -0
- package/src/lib/oauth/resource.ts +49 -0
- package/src/lib/preference-policy.ts +45 -0
- package/src/lib/preferences.ts +0 -4
- package/src/lib/public-host.ts +127 -0
- package/src/lib/ratelimit.ts +37 -12
- package/src/lib/relay.ts +7 -27
- package/src/lib/repo-write-blob-constraints.ts +141 -0
- package/src/lib/repo-write-data.ts +195 -0
- package/src/lib/repo-write-error.ts +46 -0
- package/src/lib/repo-write-validation.ts +463 -0
- package/src/lib/session-tokens.ts +22 -5
- package/src/lib/unsupported-routes.ts +32 -0
- package/src/lib/util.ts +57 -2
- package/src/pages/.well-known/atproto-did.ts +15 -3
- package/src/pages/.well-known/did.json.ts +13 -7
- package/src/pages/debug/db/bootstrap.ts +4 -3
- package/src/pages/debug/gc/blobs.ts +11 -8
- package/src/pages/debug/record.ts +11 -0
- package/src/pages/xrpc/[...nsid].ts +17 -9
- package/src/pages/xrpc/app.bsky.actor.getPreferences.ts +9 -3
- package/src/pages/xrpc/app.bsky.actor.putPreferences.ts +17 -4
- package/src/pages/xrpc/app.bsky.unspecced.getAgeAssuranceState.ts +4 -2
- package/src/pages/xrpc/chat.bsky.convo.getLog.ts +4 -2
- package/src/pages/xrpc/chat.bsky.convo.listConvos.ts +4 -2
- package/src/pages/xrpc/com.atproto.identity.getRecommendedDidCredentials.ts +10 -6
- package/src/pages/xrpc/com.atproto.identity.requestPlcOperationSignature.ts +4 -3
- package/src/pages/xrpc/com.atproto.identity.resolveHandle.ts +13 -5
- package/src/pages/xrpc/com.atproto.identity.signPlcOperation.ts +4 -2
- package/src/pages/xrpc/com.atproto.identity.submitPlcOperation.ts +4 -2
- package/src/pages/xrpc/com.atproto.identity.updateHandle.ts +12 -36
- package/src/pages/xrpc/com.atproto.repo.applyWrites.ts +90 -139
- package/src/pages/xrpc/com.atproto.repo.createRecord.ts +74 -47
- package/src/pages/xrpc/com.atproto.repo.deleteRecord.ts +119 -46
- package/src/pages/xrpc/com.atproto.repo.describeRepo.ts +21 -20
- package/src/pages/xrpc/com.atproto.repo.getRecord.ts +6 -1
- package/src/pages/xrpc/com.atproto.repo.listMissingBlobs.ts +4 -2
- package/src/pages/xrpc/com.atproto.repo.putRecord.ts +84 -47
- package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +199 -78
- package/src/pages/xrpc/com.atproto.server.checkAccountStatus.ts +4 -2
- package/src/pages/xrpc/com.atproto.server.getServiceAuth.ts +88 -21
- package/src/pages/xrpc/com.atproto.server.getSession.ts +3 -13
- package/src/pages/xrpc/com.atproto.sync.getBlob.ts +92 -74
- package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +45 -23
- package/src/services/car.ts +13 -0
- package/src/services/repo/apply-prepared-writes.ts +185 -0
- package/src/services/repo/blob-refs.ts +48 -0
- package/src/services/repo/blockstore-ops.ts +59 -17
- package/src/services/repo/list-blobs.ts +43 -0
- package/src/services/repo-manager.ts +221 -78
- package/src/worker/runtime.ts +1 -1
- package/src/worker/sequencer/upgrade.ts +4 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { lexicons } from '@atproto/api';
|
|
2
|
+
import { ensureValidDatetime } from '@atproto/syntax';
|
|
3
|
+
import { mimeMatches } from './mime';
|
|
4
|
+
import { RepoWriteError } from './repo-write-error';
|
|
5
|
+
|
|
6
|
+
type LexNode = Record<string, unknown>;
|
|
7
|
+
|
|
8
|
+
const MAX_SCHEMA_DEPTH = 100;
|
|
9
|
+
|
|
10
|
+
export function enforceRepoWriteLexiconConstraints(
|
|
11
|
+
recordDef: Record<string, unknown>,
|
|
12
|
+
record: Record<string, unknown>,
|
|
13
|
+
): void {
|
|
14
|
+
const schema = asNode(recordDef.record);
|
|
15
|
+
if (!schema) return;
|
|
16
|
+
enforceNode(schema, record, 'record', 0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function enforceNode(
|
|
20
|
+
node: LexNode,
|
|
21
|
+
value: unknown,
|
|
22
|
+
path: string,
|
|
23
|
+
depth: number,
|
|
24
|
+
): void {
|
|
25
|
+
if (depth > MAX_SCHEMA_DEPTH) {
|
|
26
|
+
throw new RepoWriteError('InvalidRequest', 'record schema is too deeply nested');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const type = node.type;
|
|
30
|
+
if (type === 'ref') {
|
|
31
|
+
const ref = typeof node.ref === 'string' ? node.ref : null;
|
|
32
|
+
const target = ref ? asNode(lexicons.getDef(ref)) : null;
|
|
33
|
+
if (target) enforceNode(target, value, path, depth + 1);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (type === 'union') {
|
|
38
|
+
enforceUnionNode(node, value, path, depth);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (type === 'object') {
|
|
43
|
+
enforceObjectNode(node, value, path, depth);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (type === 'array') {
|
|
48
|
+
const items = asNode(node.items);
|
|
49
|
+
if (items && Array.isArray(value)) {
|
|
50
|
+
for (let index = 0; index < value.length; index++) {
|
|
51
|
+
enforceNode(items, value[index], `${path}/${index}`, depth + 1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (type === 'blob') {
|
|
58
|
+
enforceBlobNode(node, value, path);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (type === 'string' && node.format === 'datetime' && typeof value === 'string') {
|
|
63
|
+
try {
|
|
64
|
+
ensureValidDatetime(value);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
const message = error instanceof Error ? error.message : 'invalid datetime';
|
|
67
|
+
throw new RepoWriteError('InvalidRequest', `${path} ${message}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function enforceObjectNode(
|
|
73
|
+
node: LexNode,
|
|
74
|
+
value: unknown,
|
|
75
|
+
path: string,
|
|
76
|
+
depth: number,
|
|
77
|
+
): void {
|
|
78
|
+
if (!isRecord(value)) return;
|
|
79
|
+
const properties = asNode(node.properties);
|
|
80
|
+
if (!properties) return;
|
|
81
|
+
|
|
82
|
+
for (const [key, child] of Object.entries(properties)) {
|
|
83
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) continue;
|
|
84
|
+
const childNode = asNode(child);
|
|
85
|
+
if (childNode) enforceNode(childNode, value[key], `${path}/${key}`, depth + 1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function enforceUnionNode(
|
|
90
|
+
node: LexNode,
|
|
91
|
+
value: unknown,
|
|
92
|
+
path: string,
|
|
93
|
+
depth: number,
|
|
94
|
+
): void {
|
|
95
|
+
if (!isRecord(value) || typeof value.$type !== 'string') return;
|
|
96
|
+
const refs = Array.isArray(node.refs) ? node.refs : [];
|
|
97
|
+
for (const refValue of refs) {
|
|
98
|
+
if (typeof refValue !== 'string' || !refMatchesType(refValue, value.$type)) continue;
|
|
99
|
+
const target = asNode(lexicons.getDef(refValue));
|
|
100
|
+
if (target) enforceNode(target, value, path, depth + 1);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function enforceBlobNode(node: LexNode, value: unknown, path: string): void {
|
|
106
|
+
if (!isRecord(value) || value.$type !== 'blob') return;
|
|
107
|
+
const mimeType = typeof value.mimeType === 'string' ? value.mimeType : '';
|
|
108
|
+
const size = typeof value.size === 'number' ? value.size : Number.NaN;
|
|
109
|
+
const accept = strings(node.accept);
|
|
110
|
+
if (accept.length > 0 && !accept.some((candidate) => mimeMatches(candidate, mimeType))) {
|
|
111
|
+
throw new RepoWriteError('InvalidMimeType', `${path} blob mime type is not accepted`);
|
|
112
|
+
}
|
|
113
|
+
if (typeof node.maxSize === 'number' && Number.isFinite(size) && size > node.maxSize) {
|
|
114
|
+
throw new RepoWriteError('BlobTooLarge', `${path} blob exceeds maxSize`, 413);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function refMatchesType(ref: string, type: string): boolean {
|
|
119
|
+
const refUri = toLexUri(ref);
|
|
120
|
+
const typeUri = toLexUri(type);
|
|
121
|
+
if (refUri === typeUri) return true;
|
|
122
|
+
if (typeUri.endsWith('#main')) return refUri === typeUri.slice(0, -5);
|
|
123
|
+
if (!typeUri.includes('#')) return refUri === `${typeUri}#main`;
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function toLexUri(value: string): string {
|
|
128
|
+
return value.startsWith('lex:') ? value : `lex:${value}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function strings(value: unknown): string[] {
|
|
132
|
+
return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function asNode(value: unknown): LexNode | null {
|
|
136
|
+
return isRecord(value) ? value : null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
140
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
141
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { CID } from 'multiformats/cid';
|
|
2
|
+
import { RepoWriteError } from './repo-write-error';
|
|
3
|
+
|
|
4
|
+
export type BlobReference = {
|
|
5
|
+
cid: string;
|
|
6
|
+
mimeType: string;
|
|
7
|
+
size: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function validateRawRecord(collection: string, value: unknown): Record<string, unknown> {
|
|
11
|
+
if (!isPlainObject(value)) {
|
|
12
|
+
throw new RepoWriteError('InvalidRequest', 'record must be an object');
|
|
13
|
+
}
|
|
14
|
+
const record: Record<string, unknown> = Object.create(null);
|
|
15
|
+
for (const [key, child] of Object.entries(value)) {
|
|
16
|
+
record[key] = child;
|
|
17
|
+
}
|
|
18
|
+
if (record.$type === undefined) {
|
|
19
|
+
record.$type = collection;
|
|
20
|
+
} else if (record.$type !== collection) {
|
|
21
|
+
throw new RepoWriteError('InvalidRequest', 'record $type must match collection');
|
|
22
|
+
}
|
|
23
|
+
validateRawValue(record, 'record');
|
|
24
|
+
return record;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function collectBlobRefs(value: unknown, refs: BlobReference[]): void {
|
|
28
|
+
if (!value || typeof value !== 'object') return;
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
for (const item of value) collectBlobRefs(item, refs);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const obj = value as Record<string, unknown>;
|
|
34
|
+
if (obj.$type === 'blob') {
|
|
35
|
+
const blob = validateBlobObject(obj, 'blob');
|
|
36
|
+
refs.push({ cid: blob.cid.toString(), mimeType: blob.mimeType, size: blob.size });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
for (const child of Object.values(obj)) collectBlobRefs(child, refs);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function validateRawValue(value: unknown, path: string): void {
|
|
43
|
+
if (value === null) return;
|
|
44
|
+
if (typeof value === 'string' || typeof value === 'boolean') return;
|
|
45
|
+
if (typeof value === 'number') {
|
|
46
|
+
if (!Number.isInteger(value)) {
|
|
47
|
+
throw new RepoWriteError('InvalidRequest', `${path} must contain integer numbers`);
|
|
48
|
+
}
|
|
49
|
+
if (!Number.isSafeInteger(value)) {
|
|
50
|
+
throw new RepoWriteError('InvalidRequest', `${path} contains an unsafe integer`);
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(value)) {
|
|
55
|
+
for (let index = 0; index < value.length; index++) {
|
|
56
|
+
validateRawValue(value[index], `${path}/${index}`);
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (!isPlainObject(value)) {
|
|
61
|
+
throw new RepoWriteError('InvalidRequest', `${path} contains an unsupported value`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if ('$link' in value) {
|
|
65
|
+
validateCidLinkObject(value, path);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if ('$bytes' in value) {
|
|
69
|
+
validateBytesObject(value, path);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (isLegacyBlobObject(value)) {
|
|
73
|
+
throw new RepoWriteError('InvalidRequest', `${path} contains a legacy blob object`);
|
|
74
|
+
}
|
|
75
|
+
if (value.$type === 'blob') {
|
|
76
|
+
validateBlobObject(value, path);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const [key, child] of Object.entries(value)) {
|
|
81
|
+
if (key.length === 0) {
|
|
82
|
+
throw new RepoWriteError('InvalidRequest', `${path} contains an empty object key`);
|
|
83
|
+
}
|
|
84
|
+
if (key === '__proto__') {
|
|
85
|
+
throw new RepoWriteError('InvalidRequest', `${path} contains a forbidden object key`);
|
|
86
|
+
}
|
|
87
|
+
if (key === '$type' && typeof child !== 'string') {
|
|
88
|
+
throw new RepoWriteError('InvalidRequest', `${path} $type must be a string`);
|
|
89
|
+
}
|
|
90
|
+
validateRawValue(child, `${path}/${key}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function validateCidLinkObject(obj: Record<string, unknown>, path: string): CID {
|
|
95
|
+
const keys = Object.keys(obj);
|
|
96
|
+
if (keys.length !== 1 || typeof obj.$link !== 'string') {
|
|
97
|
+
throw new RepoWriteError('InvalidRequest', `${path} must be a CID link object`);
|
|
98
|
+
}
|
|
99
|
+
if (!obj.$link.startsWith('b') || obj.$link !== obj.$link.toLowerCase()) {
|
|
100
|
+
throw new RepoWriteError('InvalidRequest', `${path} must contain a base32 CID string`);
|
|
101
|
+
}
|
|
102
|
+
let cid: CID;
|
|
103
|
+
try {
|
|
104
|
+
cid = CID.parse(obj.$link);
|
|
105
|
+
} catch {
|
|
106
|
+
throw new RepoWriteError('InvalidRequest', `${path} must contain a valid CID`);
|
|
107
|
+
}
|
|
108
|
+
if (cid.toString() !== obj.$link) {
|
|
109
|
+
throw new RepoWriteError('InvalidRequest', `${path} must contain a base32 CID string`);
|
|
110
|
+
}
|
|
111
|
+
validateDataCid(cid, path);
|
|
112
|
+
return cid;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function validateBytesObject(obj: Record<string, unknown>, path: string): void {
|
|
116
|
+
const keys = Object.keys(obj);
|
|
117
|
+
if (keys.length !== 1 || typeof obj.$bytes !== 'string') {
|
|
118
|
+
throw new RepoWriteError('InvalidRequest', `${path} must be a bytes object`);
|
|
119
|
+
}
|
|
120
|
+
if (!isSimpleBase64(obj.$bytes)) {
|
|
121
|
+
throw new RepoWriteError('InvalidRequest', `${path} must contain RFC-4648 base64 bytes`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function validateBlobObject(obj: Record<string, unknown>, path: string): {
|
|
126
|
+
cid: CID;
|
|
127
|
+
mimeType: string;
|
|
128
|
+
size: number;
|
|
129
|
+
} {
|
|
130
|
+
const keys = Object.keys(obj).sort();
|
|
131
|
+
if (keys.join('\0') !== ['$type', 'mimeType', 'ref', 'size'].sort().join('\0')) {
|
|
132
|
+
throw new RepoWriteError('InvalidRequest', `${path} must be a regular blob object`);
|
|
133
|
+
}
|
|
134
|
+
if (obj.$type !== 'blob') {
|
|
135
|
+
throw new RepoWriteError('InvalidRequest', `${path} must be a typed blob`);
|
|
136
|
+
}
|
|
137
|
+
if (!isPlainObject(obj.ref)) {
|
|
138
|
+
throw new RepoWriteError('InvalidRequest', `${path}.ref must be a CID link`);
|
|
139
|
+
}
|
|
140
|
+
const cid = validateCidLinkObject(obj.ref, `${path}/ref`);
|
|
141
|
+
validateRawBlobCid(cid, path);
|
|
142
|
+
if (typeof obj.mimeType !== 'string' || obj.mimeType.length === 0) {
|
|
143
|
+
throw new RepoWriteError('InvalidRequest', `${path}.mimeType must be non-empty`);
|
|
144
|
+
}
|
|
145
|
+
if (typeof obj.size !== 'number' || !Number.isInteger(obj.size) || obj.size < 0) {
|
|
146
|
+
throw new RepoWriteError('InvalidRequest', `${path}.size must be a non-negative integer`);
|
|
147
|
+
}
|
|
148
|
+
return { cid, mimeType: obj.mimeType, size: obj.size };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function validateRawBlobCid(cid: CID, path: string): void {
|
|
152
|
+
if (cid.version !== 1 || cid.code !== 0x55) {
|
|
153
|
+
throw new RepoWriteError('InvalidRequest', `${path}.ref must be a raw CIDv1 blob CID`);
|
|
154
|
+
}
|
|
155
|
+
if (cid.multihash.code !== 0x12 || cid.multihash.digest.byteLength !== 32) {
|
|
156
|
+
throw new RepoWriteError('InvalidRequest', `${path}.ref must use a SHA-256 multihash`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isLegacyBlobObject(obj: Record<string, unknown>): boolean {
|
|
161
|
+
const keys = Object.keys(obj).sort();
|
|
162
|
+
return keys.join('\0') === ['cid', 'mimeType'].join('\0') &&
|
|
163
|
+
typeof obj.cid === 'string' &&
|
|
164
|
+
typeof obj.mimeType === 'string';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function validateDataCid(cid: CID, path: string): void {
|
|
168
|
+
if (cid.version !== 1) {
|
|
169
|
+
throw new RepoWriteError('InvalidRequest', `${path} must contain a CIDv1 link`);
|
|
170
|
+
}
|
|
171
|
+
if (cid.code !== 0x71 && cid.code !== 0x55) {
|
|
172
|
+
throw new RepoWriteError('InvalidRequest', `${path} must use dag-cbor or raw multicodec`);
|
|
173
|
+
}
|
|
174
|
+
if (cid.multihash.code !== 0x12 || cid.multihash.digest.byteLength !== 32) {
|
|
175
|
+
throw new RepoWriteError('InvalidRequest', `${path} must use a SHA-256 multihash`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function isSimpleBase64(value: string): boolean {
|
|
180
|
+
if (value === '') return true;
|
|
181
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value)) return false;
|
|
182
|
+
const padding = value.endsWith('==') ? 2 : value.endsWith('=') ? 1 : 0;
|
|
183
|
+
const bodyLength = value.length - padding;
|
|
184
|
+
if (bodyLength === 0) return false;
|
|
185
|
+
if (padding > 0 && value.length % 4 !== 0) return false;
|
|
186
|
+
if (padding === 1) return bodyLength % 4 === 3;
|
|
187
|
+
if (padding === 2) return bodyLength % 4 === 2;
|
|
188
|
+
return bodyLength % 4 !== 1;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
192
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
193
|
+
const proto = Object.getPrototypeOf(value);
|
|
194
|
+
return proto === Object.prototype || proto === null;
|
|
195
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export class RepoWriteError extends Error {
|
|
2
|
+
constructor(
|
|
3
|
+
public readonly error: string,
|
|
4
|
+
message: string,
|
|
5
|
+
public readonly status = 400,
|
|
6
|
+
) {
|
|
7
|
+
super(message);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
toResponse(): Response {
|
|
11
|
+
return jsonError(this.error, this.message, this.status);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function jsonError(error: string, message?: string, status = 400): Response {
|
|
16
|
+
return new Response(JSON.stringify({
|
|
17
|
+
error,
|
|
18
|
+
...(message ? { message } : {}),
|
|
19
|
+
}), {
|
|
20
|
+
status,
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function invalidSwap(message = 'Invalid swap'): Response {
|
|
26
|
+
return jsonError('InvalidSwap', message, 400);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function handleRepoWriteError(error: unknown): Response {
|
|
30
|
+
if (error instanceof RepoWriteError) return error.toResponse();
|
|
31
|
+
if (isRepoBlobNotFound(error)) {
|
|
32
|
+
return jsonError('BlobNotFound', 'blob not found', 400);
|
|
33
|
+
}
|
|
34
|
+
if (isRepoCommitConflict(error)) {
|
|
35
|
+
return jsonError('InvalidSwap', 'repo head changed', 400);
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isRepoCommitConflict(error: unknown): boolean {
|
|
41
|
+
return error instanceof Error && error.name === 'RepoCommitConflictError';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isRepoBlobNotFound(error: unknown): boolean {
|
|
45
|
+
return error instanceof Error && error.name === 'RepoBlobNotFoundError';
|
|
46
|
+
}
|