@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/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 (!allowed.includes(response.status)) {
50
- throw new Error(`Failed to fetch ${method} ${requestUrl}: ${response.statusText}`);
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
- GetCommitOptions,
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
- ListFilesResponse,
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: options?.ttl ?? 1 * 60 * 60, // 1hr in seconds
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<ListFilesResponse> {
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: options?.ttl ?? 1 * 60 * 60, // 1hr in seconds
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
- return (await response.json()) as ListFilesResponse;
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<ListBranchesResponse> {
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: options?.ttl ?? 1 * 60 * 60, // 1hr in seconds
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 (options?.cursor || !options?.limit) {
393
+ if (typeof cursor === 'string' || typeof limit === 'number') {
133
394
  params = {};
134
- if (options?.cursor) {
135
- params.cursor = options.cursor;
395
+ if (typeof cursor === 'string') {
396
+ params.cursor = cursor;
136
397
  }
137
- if (typeof options?.limit == 'number') {
138
- params.limit = options.limit.toString();
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
- return (await response.json()) as ListBranchesResponse;
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<ListCommitsResponse> {
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: options?.ttl ?? 1 * 60 * 60, // 1hr in seconds
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
- return (await response.json()) as ListCommitsResponse;
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<GetBranchDiffResponse> {
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: options?.ttl ?? 1 * 60 * 60, // 1hr in seconds
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
- return (await response.json()) as GetBranchDiffResponse;
460
+ const raw = branchDiffResponseSchema.parse(await response.json());
461
+ return transformBranchDiffResult(raw);
190
462
  }
191
463
 
192
- async getCommitDiff(options: GetCommitDiffOptions): Promise<GetCommitDiffResponse> {
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: options?.ttl ?? 1 * 60 * 60, // 1hr in seconds
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
- return (await response.json()) as GetCommitDiffResponse;
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: options?.ttl ?? 1 * 60 * 60, // 1hr in seconds
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: options?.ttl ?? 1 * 60 * 60, // 1hr in seconds
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: 1 * 60 * 60,
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?.ttl || 365 * 24 * 60 * 60; // 1 year in seconds
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);