@pierre/storage 0.0.10 → 0.1.0
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 +209 -46
- package/dist/index.cjs +1079 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +578 -62
- package/dist/index.d.ts +578 -62
- package/dist/index.js +1078 -51
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/commit.ts +649 -0
- package/src/errors.ts +50 -0
- package/src/fetch.ts +75 -5
- package/src/index.ts +408 -44
- package/src/schemas.ts +138 -0
- package/src/types.ts +211 -62
- package/src/util.ts +0 -18
- package/src/webhook.ts +75 -3
package/src/fetch.ts
CHANGED
|
@@ -1,16 +1,40 @@
|
|
|
1
|
+
import { errorEnvelopeSchema } from './schemas';
|
|
1
2
|
import type { ValidAPIVersion, ValidMethod, ValidPath } from './types';
|
|
2
3
|
|
|
3
4
|
interface RequestOptions {
|
|
4
5
|
allowedStatus?: number[];
|
|
5
6
|
}
|
|
6
7
|
|
|
8
|
+
export class ApiError extends Error {
|
|
9
|
+
public readonly status: number;
|
|
10
|
+
public readonly statusText: string;
|
|
11
|
+
public readonly method: ValidMethod;
|
|
12
|
+
public readonly url: string;
|
|
13
|
+
public readonly body?: unknown;
|
|
14
|
+
|
|
15
|
+
constructor(params: {
|
|
16
|
+
message: string;
|
|
17
|
+
status: number;
|
|
18
|
+
statusText: string;
|
|
19
|
+
method: ValidMethod;
|
|
20
|
+
url: string;
|
|
21
|
+
body?: unknown;
|
|
22
|
+
}) {
|
|
23
|
+
super(params.message);
|
|
24
|
+
this.name = 'ApiError';
|
|
25
|
+
this.status = params.status;
|
|
26
|
+
this.statusText = params.statusText;
|
|
27
|
+
this.method = params.method;
|
|
28
|
+
this.url = params.url;
|
|
29
|
+
this.body = params.body;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
7
33
|
export class ApiFetcher {
|
|
8
34
|
constructor(
|
|
9
35
|
private readonly API_BASE_URL: string,
|
|
10
36
|
private readonly version: ValidAPIVersion,
|
|
11
|
-
) {
|
|
12
|
-
console.log('api fetcher created', API_BASE_URL, version);
|
|
13
|
-
}
|
|
37
|
+
) {}
|
|
14
38
|
|
|
15
39
|
private getBaseUrl() {
|
|
16
40
|
return `${this.API_BASE_URL}/api/v${this.version}`;
|
|
@@ -46,9 +70,55 @@ export class ApiFetcher {
|
|
|
46
70
|
|
|
47
71
|
if (!response.ok) {
|
|
48
72
|
const allowed = options?.allowedStatus ?? [];
|
|
49
|
-
if (
|
|
50
|
-
|
|
73
|
+
if (allowed.includes(response.status)) {
|
|
74
|
+
return response;
|
|
51
75
|
}
|
|
76
|
+
|
|
77
|
+
let errorBody: unknown;
|
|
78
|
+
let message: string | undefined;
|
|
79
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
if (contentType.includes('application/json')) {
|
|
83
|
+
errorBody = await response.json();
|
|
84
|
+
} else {
|
|
85
|
+
const text = await response.text();
|
|
86
|
+
errorBody = text;
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// Fallback to plain text if JSON parse failed after reading body
|
|
90
|
+
try {
|
|
91
|
+
errorBody = await response.text();
|
|
92
|
+
} catch {
|
|
93
|
+
errorBody = undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (typeof errorBody === 'string') {
|
|
98
|
+
const trimmed = errorBody.trim();
|
|
99
|
+
if (trimmed) {
|
|
100
|
+
message = trimmed;
|
|
101
|
+
}
|
|
102
|
+
} else if (errorBody && typeof errorBody === 'object') {
|
|
103
|
+
const parsedError = errorEnvelopeSchema.safeParse(errorBody);
|
|
104
|
+
if (parsedError.success) {
|
|
105
|
+
const trimmed = parsedError.data.error.trim();
|
|
106
|
+
if (trimmed) {
|
|
107
|
+
message = trimmed;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
throw new ApiError({
|
|
113
|
+
message:
|
|
114
|
+
message ??
|
|
115
|
+
`Request ${method} ${requestUrl} failed with status ${response.status} ${response.statusText}`,
|
|
116
|
+
status: response.status,
|
|
117
|
+
statusText: response.statusText,
|
|
118
|
+
method,
|
|
119
|
+
url: requestUrl,
|
|
120
|
+
body: errorBody,
|
|
121
|
+
});
|
|
52
122
|
}
|
|
53
123
|
return response;
|
|
54
124
|
}
|
package/src/index.ts
CHANGED
|
@@ -6,28 +6,56 @@
|
|
|
6
6
|
|
|
7
7
|
import { importPKCS8, SignJWT } from 'jose';
|
|
8
8
|
import snakecaseKeys from 'snakecase-keys';
|
|
9
|
+
import { createCommitBuilder, FetchCommitTransport, resolveCommitTtlSeconds } from './commit';
|
|
10
|
+
import { RefUpdateError } from './errors';
|
|
9
11
|
import { ApiFetcher } from './fetch';
|
|
12
|
+
import type { ResetCommitAckRaw } from './schemas';
|
|
13
|
+
import {
|
|
14
|
+
branchDiffResponseSchema,
|
|
15
|
+
commitDiffResponseSchema,
|
|
16
|
+
listBranchesResponseSchema,
|
|
17
|
+
listCommitsResponseSchema,
|
|
18
|
+
listFilesResponseSchema,
|
|
19
|
+
resetCommitAckSchema,
|
|
20
|
+
resetCommitResponseSchema,
|
|
21
|
+
} from './schemas';
|
|
10
22
|
import type {
|
|
23
|
+
BranchInfo,
|
|
24
|
+
CommitBuilder,
|
|
25
|
+
CommitInfo,
|
|
26
|
+
CreateCommitOptions,
|
|
11
27
|
CreateRepoOptions,
|
|
28
|
+
DiffFileState,
|
|
29
|
+
FileDiff,
|
|
30
|
+
FilteredFile,
|
|
12
31
|
FindOneOptions,
|
|
13
32
|
GetBranchDiffOptions,
|
|
14
33
|
GetBranchDiffResponse,
|
|
34
|
+
GetBranchDiffResult,
|
|
15
35
|
GetCommitDiffOptions,
|
|
16
36
|
GetCommitDiffResponse,
|
|
17
|
-
|
|
18
|
-
GetCommitResponse,
|
|
37
|
+
GetCommitDiffResult,
|
|
19
38
|
GetFileOptions,
|
|
20
39
|
GetRemoteURLOptions,
|
|
21
40
|
GitStorageOptions,
|
|
22
41
|
ListBranchesOptions,
|
|
23
42
|
ListBranchesResponse,
|
|
43
|
+
ListBranchesResult,
|
|
24
44
|
ListCommitsOptions,
|
|
25
45
|
ListCommitsResponse,
|
|
46
|
+
ListCommitsResult,
|
|
26
47
|
ListFilesOptions,
|
|
27
|
-
|
|
48
|
+
ListFilesResult,
|
|
28
49
|
OverrideableGitStorageOptions,
|
|
29
50
|
PullUpstreamOptions,
|
|
51
|
+
RawBranchInfo,
|
|
52
|
+
RawCommitInfo,
|
|
53
|
+
RawFileDiff,
|
|
54
|
+
RawFilteredFile,
|
|
55
|
+
RefUpdate,
|
|
30
56
|
Repo,
|
|
57
|
+
ResetCommitOptions,
|
|
58
|
+
ResetCommitResult,
|
|
31
59
|
ValidAPIVersion,
|
|
32
60
|
} from './types';
|
|
33
61
|
|
|
@@ -35,6 +63,8 @@ import type {
|
|
|
35
63
|
* Type definitions for Pierre Git Storage SDK
|
|
36
64
|
*/
|
|
37
65
|
|
|
66
|
+
export { RefUpdateError } from './errors';
|
|
67
|
+
export { ApiError } from './fetch';
|
|
38
68
|
// Import additional types from types.ts
|
|
39
69
|
export * from './types';
|
|
40
70
|
|
|
@@ -53,6 +83,122 @@ const STORAGE_BASE_URL = __STORAGE_BASE_URL__;
|
|
|
53
83
|
const API_VERSION: ValidAPIVersion = 1;
|
|
54
84
|
|
|
55
85
|
const apiInstanceMap = new Map<string, ApiFetcher>();
|
|
86
|
+
const DEFAULT_TOKEN_TTL_SECONDS = 60 * 60; // 1 hour
|
|
87
|
+
const RESET_COMMIT_ALLOWED_STATUS = [
|
|
88
|
+
400, // Bad Request - validation errors
|
|
89
|
+
401, // Unauthorized - missing/invalid auth header
|
|
90
|
+
403, // Forbidden - missing git:write scope
|
|
91
|
+
404, // Not Found - repo lookup failures
|
|
92
|
+
408, // Request Timeout - client cancelled
|
|
93
|
+
409, // Conflict - concurrent ref updates
|
|
94
|
+
412, // Precondition Failed - optimistic concurrency
|
|
95
|
+
422, // Unprocessable Entity - metadata issues
|
|
96
|
+
429, // Too Many Requests - upstream throttling
|
|
97
|
+
499, // Client Closed Request - storage cancellation
|
|
98
|
+
500, // Internal Server Error - generic failure
|
|
99
|
+
502, // Bad Gateway - storage/gateway bridge issues
|
|
100
|
+
503, // Service Unavailable - storage selection failures
|
|
101
|
+
504, // Gateway Timeout - long-running storage operations
|
|
102
|
+
] as const;
|
|
103
|
+
|
|
104
|
+
function resolveInvocationTtlSeconds(
|
|
105
|
+
options?: { ttl?: number },
|
|
106
|
+
defaultValue: number = DEFAULT_TOKEN_TTL_SECONDS,
|
|
107
|
+
): number {
|
|
108
|
+
if (typeof options?.ttl === 'number' && options.ttl > 0) {
|
|
109
|
+
return options.ttl;
|
|
110
|
+
}
|
|
111
|
+
return defaultValue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
type ResetCommitAck = ResetCommitAckRaw;
|
|
115
|
+
|
|
116
|
+
function toRefUpdate(result: ResetCommitAck['result']): RefUpdate {
|
|
117
|
+
return {
|
|
118
|
+
branch: result.branch,
|
|
119
|
+
oldSha: result.old_sha,
|
|
120
|
+
newSha: result.new_sha,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function buildResetCommitResult(ack: ResetCommitAck): ResetCommitResult {
|
|
125
|
+
const refUpdate = toRefUpdate(ack.result);
|
|
126
|
+
if (!ack.result.success) {
|
|
127
|
+
throw new RefUpdateError(
|
|
128
|
+
ack.result.message ?? `Reset commit failed with status ${ack.result.status}`,
|
|
129
|
+
{
|
|
130
|
+
status: ack.result.status,
|
|
131
|
+
message: ack.result.message,
|
|
132
|
+
refUpdate,
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
commitSha: ack.commit.commit_sha,
|
|
138
|
+
treeSha: ack.commit.tree_sha,
|
|
139
|
+
targetBranch: ack.commit.target_branch,
|
|
140
|
+
packBytes: ack.commit.pack_bytes,
|
|
141
|
+
refUpdate,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface ResetCommitFailureInfo {
|
|
146
|
+
status?: string;
|
|
147
|
+
message?: string;
|
|
148
|
+
refUpdate?: Partial<RefUpdate>;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function toPartialRefUpdate(
|
|
152
|
+
branch?: unknown,
|
|
153
|
+
oldSha?: unknown,
|
|
154
|
+
newSha?: unknown,
|
|
155
|
+
): Partial<RefUpdate> | undefined {
|
|
156
|
+
const refUpdate: Partial<RefUpdate> = {};
|
|
157
|
+
if (typeof branch === 'string' && branch.trim() !== '') {
|
|
158
|
+
refUpdate.branch = branch;
|
|
159
|
+
}
|
|
160
|
+
if (typeof oldSha === 'string' && oldSha.trim() !== '') {
|
|
161
|
+
refUpdate.oldSha = oldSha;
|
|
162
|
+
}
|
|
163
|
+
if (typeof newSha === 'string' && newSha.trim() !== '') {
|
|
164
|
+
refUpdate.newSha = newSha;
|
|
165
|
+
}
|
|
166
|
+
return Object.keys(refUpdate).length > 0 ? refUpdate : undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function parseResetCommitPayload(
|
|
170
|
+
payload: unknown,
|
|
171
|
+
): { ack: ResetCommitAck } | { failure: ResetCommitFailureInfo } | null {
|
|
172
|
+
const ack = resetCommitAckSchema.safeParse(payload);
|
|
173
|
+
if (ack.success) {
|
|
174
|
+
return { ack: ack.data };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const failure = resetCommitResponseSchema.safeParse(payload);
|
|
178
|
+
if (failure.success) {
|
|
179
|
+
const result = failure.data.result;
|
|
180
|
+
return {
|
|
181
|
+
failure: {
|
|
182
|
+
status: result.status,
|
|
183
|
+
message: result.message,
|
|
184
|
+
refUpdate: toPartialRefUpdate(result.branch, result.old_sha, result.new_sha),
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function httpStatusToResetStatus(status: number): string {
|
|
193
|
+
switch (status) {
|
|
194
|
+
case 409:
|
|
195
|
+
return 'conflict';
|
|
196
|
+
case 412:
|
|
197
|
+
return 'precondition_failed';
|
|
198
|
+
default:
|
|
199
|
+
return `${status}`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
56
202
|
|
|
57
203
|
function getApiInstance(baseUrl: string, version: ValidAPIVersion) {
|
|
58
204
|
if (!apiInstanceMap.has(`${baseUrl}--${version}`)) {
|
|
@@ -61,6 +207,114 @@ function getApiInstance(baseUrl: string, version: ValidAPIVersion) {
|
|
|
61
207
|
return apiInstanceMap.get(`${baseUrl}--${version}`)!;
|
|
62
208
|
}
|
|
63
209
|
|
|
210
|
+
function transformBranchInfo(raw: RawBranchInfo): BranchInfo {
|
|
211
|
+
return {
|
|
212
|
+
cursor: raw.cursor,
|
|
213
|
+
name: raw.name,
|
|
214
|
+
headSha: raw.head_sha,
|
|
215
|
+
createdAt: raw.created_at,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function transformListBranchesResult(raw: ListBranchesResponse): ListBranchesResult {
|
|
220
|
+
return {
|
|
221
|
+
branches: raw.branches.map(transformBranchInfo),
|
|
222
|
+
nextCursor: raw.next_cursor ?? undefined,
|
|
223
|
+
hasMore: raw.has_more,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function transformCommitInfo(raw: RawCommitInfo): CommitInfo {
|
|
228
|
+
const parsedDate = new Date(raw.date);
|
|
229
|
+
return {
|
|
230
|
+
sha: raw.sha,
|
|
231
|
+
message: raw.message,
|
|
232
|
+
authorName: raw.author_name,
|
|
233
|
+
authorEmail: raw.author_email,
|
|
234
|
+
committerName: raw.committer_name,
|
|
235
|
+
committerEmail: raw.committer_email,
|
|
236
|
+
date: parsedDate,
|
|
237
|
+
rawDate: raw.date,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function transformListCommitsResult(raw: ListCommitsResponse): ListCommitsResult {
|
|
242
|
+
return {
|
|
243
|
+
commits: raw.commits.map(transformCommitInfo),
|
|
244
|
+
nextCursor: raw.next_cursor ?? undefined,
|
|
245
|
+
hasMore: raw.has_more,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function normalizeDiffState(rawState: string): DiffFileState {
|
|
250
|
+
if (!rawState) {
|
|
251
|
+
return 'unknown';
|
|
252
|
+
}
|
|
253
|
+
const leading = rawState.trim()[0]?.toUpperCase();
|
|
254
|
+
switch (leading) {
|
|
255
|
+
case 'A':
|
|
256
|
+
return 'added';
|
|
257
|
+
case 'M':
|
|
258
|
+
return 'modified';
|
|
259
|
+
case 'D':
|
|
260
|
+
return 'deleted';
|
|
261
|
+
case 'R':
|
|
262
|
+
return 'renamed';
|
|
263
|
+
case 'C':
|
|
264
|
+
return 'copied';
|
|
265
|
+
case 'T':
|
|
266
|
+
return 'type_changed';
|
|
267
|
+
case 'U':
|
|
268
|
+
return 'unmerged';
|
|
269
|
+
default:
|
|
270
|
+
return 'unknown';
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function transformFileDiff(raw: RawFileDiff): FileDiff {
|
|
275
|
+
const normalizedState = normalizeDiffState(raw.state);
|
|
276
|
+
return {
|
|
277
|
+
path: raw.path,
|
|
278
|
+
state: normalizedState,
|
|
279
|
+
rawState: raw.state,
|
|
280
|
+
oldPath: raw.old_path ?? undefined,
|
|
281
|
+
raw: raw.raw,
|
|
282
|
+
bytes: raw.bytes,
|
|
283
|
+
isEof: raw.is_eof,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function transformFilteredFile(raw: RawFilteredFile): FilteredFile {
|
|
288
|
+
const normalizedState = normalizeDiffState(raw.state);
|
|
289
|
+
return {
|
|
290
|
+
path: raw.path,
|
|
291
|
+
state: normalizedState,
|
|
292
|
+
rawState: raw.state,
|
|
293
|
+
oldPath: raw.old_path ?? undefined,
|
|
294
|
+
bytes: raw.bytes,
|
|
295
|
+
isEof: raw.is_eof,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function transformBranchDiffResult(raw: GetBranchDiffResponse): GetBranchDiffResult {
|
|
300
|
+
return {
|
|
301
|
+
branch: raw.branch,
|
|
302
|
+
base: raw.base,
|
|
303
|
+
stats: raw.stats,
|
|
304
|
+
files: raw.files.map(transformFileDiff),
|
|
305
|
+
filteredFiles: raw.filtered_files.map(transformFilteredFile),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function transformCommitDiffResult(raw: GetCommitDiffResponse): GetCommitDiffResult {
|
|
310
|
+
return {
|
|
311
|
+
sha: raw.sha,
|
|
312
|
+
stats: raw.stats,
|
|
313
|
+
files: raw.files.map(transformFileDiff),
|
|
314
|
+
filteredFiles: raw.filtered_files.map(transformFilteredFile),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
64
318
|
/**
|
|
65
319
|
* Implementation of the Repo interface
|
|
66
320
|
*/
|
|
@@ -90,9 +344,10 @@ class RepoImpl implements Repo {
|
|
|
90
344
|
}
|
|
91
345
|
|
|
92
346
|
async getFileStream(options: GetFileOptions): Promise<Response> {
|
|
347
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
93
348
|
const jwt = await this.generateJWT(this.id, {
|
|
94
349
|
permissions: ['git:read'],
|
|
95
|
-
ttl
|
|
350
|
+
ttl,
|
|
96
351
|
});
|
|
97
352
|
|
|
98
353
|
const params: Record<string, string> = {
|
|
@@ -107,10 +362,11 @@ class RepoImpl implements Repo {
|
|
|
107
362
|
return this.api.get({ path: 'repos/file', params }, jwt);
|
|
108
363
|
}
|
|
109
364
|
|
|
110
|
-
async listFiles(options?: ListFilesOptions): Promise<
|
|
365
|
+
async listFiles(options?: ListFilesOptions): Promise<ListFilesResult> {
|
|
366
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
111
367
|
const jwt = await this.generateJWT(this.id, {
|
|
112
368
|
permissions: ['git:read'],
|
|
113
|
-
ttl
|
|
369
|
+
ttl,
|
|
114
370
|
});
|
|
115
371
|
|
|
116
372
|
const params: Record<string, string> | undefined = options?.ref
|
|
@@ -118,36 +374,46 @@ class RepoImpl implements Repo {
|
|
|
118
374
|
: undefined;
|
|
119
375
|
const response = await this.api.get({ path: 'repos/files', params }, jwt);
|
|
120
376
|
|
|
121
|
-
|
|
377
|
+
const raw = listFilesResponseSchema.parse(await response.json());
|
|
378
|
+
return { paths: raw.paths, ref: raw.ref };
|
|
122
379
|
}
|
|
123
380
|
|
|
124
|
-
async listBranches(options?: ListBranchesOptions): Promise<
|
|
381
|
+
async listBranches(options?: ListBranchesOptions): Promise<ListBranchesResult> {
|
|
382
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
125
383
|
const jwt = await this.generateJWT(this.id, {
|
|
126
384
|
permissions: ['git:read'],
|
|
127
|
-
ttl
|
|
385
|
+
ttl,
|
|
128
386
|
});
|
|
129
387
|
|
|
388
|
+
const cursor = options?.cursor;
|
|
389
|
+
const limit = options?.limit;
|
|
390
|
+
|
|
130
391
|
let params: Record<string, string> | undefined;
|
|
131
392
|
|
|
132
|
-
if (
|
|
393
|
+
if (typeof cursor === 'string' || typeof limit === 'number') {
|
|
133
394
|
params = {};
|
|
134
|
-
if (
|
|
135
|
-
params.cursor =
|
|
395
|
+
if (typeof cursor === 'string') {
|
|
396
|
+
params.cursor = cursor;
|
|
136
397
|
}
|
|
137
|
-
if (typeof
|
|
138
|
-
params.limit =
|
|
398
|
+
if (typeof limit === 'number') {
|
|
399
|
+
params.limit = limit.toString();
|
|
139
400
|
}
|
|
140
401
|
}
|
|
141
402
|
|
|
142
403
|
const response = await this.api.get({ path: 'repos/branches', params }, jwt);
|
|
143
404
|
|
|
144
|
-
|
|
405
|
+
const raw = listBranchesResponseSchema.parse(await response.json());
|
|
406
|
+
return transformListBranchesResult({
|
|
407
|
+
...raw,
|
|
408
|
+
next_cursor: raw.next_cursor ?? undefined,
|
|
409
|
+
});
|
|
145
410
|
}
|
|
146
411
|
|
|
147
|
-
async listCommits(options?: ListCommitsOptions): Promise<
|
|
412
|
+
async listCommits(options?: ListCommitsOptions): Promise<ListCommitsResult> {
|
|
413
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
148
414
|
const jwt = await this.generateJWT(this.id, {
|
|
149
415
|
permissions: ['git:read'],
|
|
150
|
-
ttl
|
|
416
|
+
ttl,
|
|
151
417
|
});
|
|
152
418
|
|
|
153
419
|
let params: Record<string, string> | undefined;
|
|
@@ -167,13 +433,18 @@ class RepoImpl implements Repo {
|
|
|
167
433
|
|
|
168
434
|
const response = await this.api.get({ path: 'repos/commits', params }, jwt);
|
|
169
435
|
|
|
170
|
-
|
|
436
|
+
const raw = listCommitsResponseSchema.parse(await response.json());
|
|
437
|
+
return transformListCommitsResult({
|
|
438
|
+
...raw,
|
|
439
|
+
next_cursor: raw.next_cursor ?? undefined,
|
|
440
|
+
});
|
|
171
441
|
}
|
|
172
442
|
|
|
173
|
-
async getBranchDiff(options: GetBranchDiffOptions): Promise<
|
|
443
|
+
async getBranchDiff(options: GetBranchDiffOptions): Promise<GetBranchDiffResult> {
|
|
444
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
174
445
|
const jwt = await this.generateJWT(this.id, {
|
|
175
446
|
permissions: ['git:read'],
|
|
176
|
-
ttl
|
|
447
|
+
ttl,
|
|
177
448
|
});
|
|
178
449
|
|
|
179
450
|
const params: Record<string, string> = {
|
|
@@ -186,13 +457,15 @@ class RepoImpl implements Repo {
|
|
|
186
457
|
|
|
187
458
|
const response = await this.api.get({ path: 'repos/branches/diff', params }, jwt);
|
|
188
459
|
|
|
189
|
-
|
|
460
|
+
const raw = branchDiffResponseSchema.parse(await response.json());
|
|
461
|
+
return transformBranchDiffResult(raw);
|
|
190
462
|
}
|
|
191
463
|
|
|
192
|
-
async getCommitDiff(options: GetCommitDiffOptions): Promise<
|
|
464
|
+
async getCommitDiff(options: GetCommitDiffOptions): Promise<GetCommitDiffResult> {
|
|
465
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
193
466
|
const jwt = await this.generateJWT(this.id, {
|
|
194
467
|
permissions: ['git:read'],
|
|
195
|
-
ttl
|
|
468
|
+
ttl,
|
|
196
469
|
});
|
|
197
470
|
|
|
198
471
|
const params: Record<string, string> = {
|
|
@@ -201,28 +474,15 @@ class RepoImpl implements Repo {
|
|
|
201
474
|
|
|
202
475
|
const response = await this.api.get({ path: 'repos/diff', params }, jwt);
|
|
203
476
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
async getCommit(options: GetCommitOptions): Promise<GetCommitResponse> {
|
|
208
|
-
const jwt = await this.generateJWT(this.id, {
|
|
209
|
-
permissions: ['git:read'],
|
|
210
|
-
ttl: options?.ttl ?? 1 * 60 * 60, // 1hr in seconds
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
const params: Record<string, string> = {
|
|
214
|
-
repo: this.id,
|
|
215
|
-
sha: options.sha,
|
|
216
|
-
};
|
|
217
|
-
const response = await this.api.get({ path: 'commit', params }, jwt);
|
|
218
|
-
|
|
219
|
-
return (await response.json()) as GetCommitResponse;
|
|
477
|
+
const raw = commitDiffResponseSchema.parse(await response.json());
|
|
478
|
+
return transformCommitDiffResult(raw);
|
|
220
479
|
}
|
|
221
480
|
|
|
222
481
|
async pullUpstream(options: PullUpstreamOptions): Promise<void> {
|
|
482
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
223
483
|
const jwt = await this.generateJWT(this.id, {
|
|
224
484
|
permissions: ['git:write'],
|
|
225
|
-
ttl
|
|
485
|
+
ttl,
|
|
226
486
|
});
|
|
227
487
|
|
|
228
488
|
const body: Record<string, string> = {};
|
|
@@ -239,6 +499,108 @@ class RepoImpl implements Repo {
|
|
|
239
499
|
|
|
240
500
|
return;
|
|
241
501
|
}
|
|
502
|
+
|
|
503
|
+
async resetCommit(options: ResetCommitOptions): Promise<ResetCommitResult> {
|
|
504
|
+
const targetBranch = options?.targetBranch?.trim();
|
|
505
|
+
if (!targetBranch) {
|
|
506
|
+
throw new Error('resetCommit targetBranch is required');
|
|
507
|
+
}
|
|
508
|
+
if (targetBranch.startsWith('refs/')) {
|
|
509
|
+
throw new Error('resetCommit targetBranch must not include refs/ prefix');
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const targetCommitSha = options?.targetCommitSha?.trim();
|
|
513
|
+
if (!targetCommitSha) {
|
|
514
|
+
throw new Error('resetCommit targetCommitSha is required');
|
|
515
|
+
}
|
|
516
|
+
const commitMessage = options?.commitMessage?.trim();
|
|
517
|
+
|
|
518
|
+
const authorName = options.author?.name?.trim();
|
|
519
|
+
const authorEmail = options.author?.email?.trim();
|
|
520
|
+
if (!authorName || !authorEmail) {
|
|
521
|
+
throw new Error('resetCommit author name and email are required');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const ttl = resolveCommitTtlSeconds(options);
|
|
525
|
+
const jwt = await this.generateJWT(this.id, {
|
|
526
|
+
permissions: ['git:write'],
|
|
527
|
+
ttl,
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const metadata: Record<string, unknown> = {
|
|
531
|
+
target_branch: targetBranch,
|
|
532
|
+
target_commit_sha: targetCommitSha,
|
|
533
|
+
author: {
|
|
534
|
+
name: authorName,
|
|
535
|
+
email: authorEmail,
|
|
536
|
+
},
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
if (commitMessage) {
|
|
540
|
+
metadata.commit_message = commitMessage;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const expectedHeadSha = options.expectedHeadSha?.trim();
|
|
544
|
+
if (expectedHeadSha) {
|
|
545
|
+
metadata.expected_head_sha = expectedHeadSha;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (options.committer) {
|
|
549
|
+
const committerName = options.committer.name?.trim();
|
|
550
|
+
const committerEmail = options.committer.email?.trim();
|
|
551
|
+
if (!committerName || !committerEmail) {
|
|
552
|
+
throw new Error('resetCommit committer name and email are required when provided');
|
|
553
|
+
}
|
|
554
|
+
metadata.committer = {
|
|
555
|
+
name: committerName,
|
|
556
|
+
email: committerEmail,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const response = await this.api.post({ path: 'repos/reset-commits', body: { metadata } }, jwt, {
|
|
561
|
+
allowedStatus: [...RESET_COMMIT_ALLOWED_STATUS],
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
const payload = await response.json();
|
|
565
|
+
const parsed = parseResetCommitPayload(payload);
|
|
566
|
+
if (parsed && 'ack' in parsed) {
|
|
567
|
+
return buildResetCommitResult(parsed.ack);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const failure = parsed && 'failure' in parsed ? parsed.failure : undefined;
|
|
571
|
+
const status = failure?.status ?? httpStatusToResetStatus(response.status);
|
|
572
|
+
const message =
|
|
573
|
+
failure?.message ??
|
|
574
|
+
`Reset commit failed with HTTP ${response.status}` +
|
|
575
|
+
(response.statusText ? ` ${response.statusText}` : '');
|
|
576
|
+
|
|
577
|
+
throw new RefUpdateError(message, {
|
|
578
|
+
status,
|
|
579
|
+
refUpdate: failure?.refUpdate,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
createCommit(options: CreateCommitOptions): CommitBuilder {
|
|
584
|
+
const version = this.options.apiVersion ?? API_VERSION;
|
|
585
|
+
const baseUrl = this.options.apiBaseUrl ?? API_BASE_URL;
|
|
586
|
+
const transport = new FetchCommitTransport({ baseUrl, version });
|
|
587
|
+
const ttl = resolveCommitTtlSeconds(options);
|
|
588
|
+
const builderOptions: CreateCommitOptions = {
|
|
589
|
+
...options,
|
|
590
|
+
ttl,
|
|
591
|
+
};
|
|
592
|
+
const getAuthToken = () =>
|
|
593
|
+
this.generateJWT(this.id, {
|
|
594
|
+
permissions: ['git:write'],
|
|
595
|
+
ttl,
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
return createCommitBuilder({
|
|
599
|
+
options: builderOptions,
|
|
600
|
+
getAuthToken,
|
|
601
|
+
transport,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
242
604
|
}
|
|
243
605
|
|
|
244
606
|
export class GitStorage {
|
|
@@ -272,6 +634,7 @@ export class GitStorage {
|
|
|
272
634
|
const resolvedApiVersion = options.apiVersion ?? GitStorage.overrides.apiVersion ?? API_VERSION;
|
|
273
635
|
const resolvedStorageBaseUrl =
|
|
274
636
|
options.storageBaseUrl ?? GitStorage.overrides.storageBaseUrl ?? STORAGE_BASE_URL;
|
|
637
|
+
const resolvedDefaultTtl = options.defaultTTL ?? GitStorage.overrides.defaultTTL;
|
|
275
638
|
|
|
276
639
|
this.api = getApiInstance(resolvedApiBaseUrl, resolvedApiVersion);
|
|
277
640
|
|
|
@@ -281,6 +644,7 @@ export class GitStorage {
|
|
|
281
644
|
apiBaseUrl: resolvedApiBaseUrl,
|
|
282
645
|
apiVersion: resolvedApiVersion,
|
|
283
646
|
storageBaseUrl: resolvedStorageBaseUrl,
|
|
647
|
+
defaultTTL: resolvedDefaultTtl,
|
|
284
648
|
};
|
|
285
649
|
}
|
|
286
650
|
|
|
@@ -294,10 +658,10 @@ export class GitStorage {
|
|
|
294
658
|
*/
|
|
295
659
|
async createRepo(options?: CreateRepoOptions): Promise<Repo> {
|
|
296
660
|
const repoId = options?.id || crypto.randomUUID();
|
|
297
|
-
|
|
661
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
298
662
|
const jwt = await this.generateJWT(repoId, {
|
|
299
663
|
permissions: ['repo:write'],
|
|
300
|
-
ttl
|
|
664
|
+
ttl,
|
|
301
665
|
});
|
|
302
666
|
|
|
303
667
|
const baseRepoOptions = options?.baseRepo
|
|
@@ -338,7 +702,7 @@ export class GitStorage {
|
|
|
338
702
|
async findOne(options: FindOneOptions): Promise<Repo | null> {
|
|
339
703
|
const jwt = await this.generateJWT(options.id, {
|
|
340
704
|
permissions: ['git:read'],
|
|
341
|
-
ttl:
|
|
705
|
+
ttl: DEFAULT_TOKEN_TTL_SECONDS,
|
|
342
706
|
});
|
|
343
707
|
|
|
344
708
|
// Allow 404 to indicate "not found" without throwing
|
|
@@ -366,7 +730,7 @@ export class GitStorage {
|
|
|
366
730
|
private async generateJWT(repoId: string, options?: GetRemoteURLOptions): Promise<string> {
|
|
367
731
|
// Default permissions and TTL
|
|
368
732
|
const permissions = options?.permissions || ['git:write', 'git:read'];
|
|
369
|
-
const ttl = options
|
|
733
|
+
const ttl = resolveInvocationTtlSeconds(options, this.options.defaultTTL ?? 365 * 24 * 60 * 60);
|
|
370
734
|
|
|
371
735
|
// Create the JWT payload
|
|
372
736
|
const now = Math.floor(Date.now() / 1000);
|