@pierre/storage 0.0.11 → 0.1.1

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/dist/index.cjs CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  var jose = require('jose');
4
4
  var snakecaseKeys = require('snakecase-keys');
5
+ var zod = require('zod');
5
6
 
6
7
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
8
 
@@ -9,6 +10,150 @@ var snakecaseKeys__default = /*#__PURE__*/_interopDefault(snakecaseKeys);
9
10
 
10
11
  // src/index.ts
11
12
 
13
+ // src/errors.ts
14
+ var RefUpdateError = class extends Error {
15
+ status;
16
+ reason;
17
+ refUpdate;
18
+ constructor(message, options) {
19
+ super(message);
20
+ this.name = "RefUpdateError";
21
+ this.status = options.status;
22
+ this.reason = options.reason ?? inferRefUpdateReason(options.status);
23
+ this.refUpdate = options.refUpdate;
24
+ }
25
+ };
26
+ var REF_REASON_MAP = {
27
+ precondition_failed: "precondition_failed",
28
+ conflict: "conflict",
29
+ not_found: "not_found",
30
+ invalid: "invalid",
31
+ timeout: "timeout",
32
+ unauthorized: "unauthorized",
33
+ forbidden: "forbidden",
34
+ unavailable: "unavailable",
35
+ internal: "internal",
36
+ failed: "failed",
37
+ ok: "unknown"
38
+ };
39
+ function inferRefUpdateReason(status) {
40
+ if (!status) {
41
+ return "unknown";
42
+ }
43
+ const trimmed = status.trim();
44
+ if (trimmed === "") {
45
+ return "unknown";
46
+ }
47
+ const label = trimmed.toLowerCase();
48
+ return REF_REASON_MAP[label] ?? "unknown";
49
+ }
50
+ var listFilesResponseSchema = zod.z.object({
51
+ paths: zod.z.array(zod.z.string()),
52
+ ref: zod.z.string()
53
+ });
54
+ var branchInfoSchema = zod.z.object({
55
+ cursor: zod.z.string(),
56
+ name: zod.z.string(),
57
+ head_sha: zod.z.string(),
58
+ created_at: zod.z.string()
59
+ });
60
+ var listBranchesResponseSchema = zod.z.object({
61
+ branches: zod.z.array(branchInfoSchema),
62
+ next_cursor: zod.z.string().nullable().optional(),
63
+ has_more: zod.z.boolean()
64
+ });
65
+ var commitInfoRawSchema = zod.z.object({
66
+ sha: zod.z.string(),
67
+ message: zod.z.string(),
68
+ author_name: zod.z.string(),
69
+ author_email: zod.z.string(),
70
+ committer_name: zod.z.string(),
71
+ committer_email: zod.z.string(),
72
+ date: zod.z.string()
73
+ });
74
+ var listCommitsResponseSchema = zod.z.object({
75
+ commits: zod.z.array(commitInfoRawSchema),
76
+ next_cursor: zod.z.string().nullable().optional(),
77
+ has_more: zod.z.boolean()
78
+ });
79
+ var diffStatsSchema = zod.z.object({
80
+ files: zod.z.number(),
81
+ additions: zod.z.number(),
82
+ deletions: zod.z.number(),
83
+ changes: zod.z.number()
84
+ });
85
+ var diffFileRawSchema = zod.z.object({
86
+ path: zod.z.string(),
87
+ state: zod.z.string(),
88
+ old_path: zod.z.string().nullable().optional(),
89
+ raw: zod.z.string(),
90
+ bytes: zod.z.number(),
91
+ is_eof: zod.z.boolean()
92
+ });
93
+ var filteredFileRawSchema = zod.z.object({
94
+ path: zod.z.string(),
95
+ state: zod.z.string(),
96
+ old_path: zod.z.string().nullable().optional(),
97
+ bytes: zod.z.number(),
98
+ is_eof: zod.z.boolean()
99
+ });
100
+ var branchDiffResponseSchema = zod.z.object({
101
+ branch: zod.z.string(),
102
+ base: zod.z.string(),
103
+ stats: diffStatsSchema,
104
+ files: zod.z.array(diffFileRawSchema),
105
+ filtered_files: zod.z.array(filteredFileRawSchema)
106
+ });
107
+ var commitDiffResponseSchema = zod.z.object({
108
+ sha: zod.z.string(),
109
+ stats: diffStatsSchema,
110
+ files: zod.z.array(diffFileRawSchema),
111
+ filtered_files: zod.z.array(filteredFileRawSchema)
112
+ });
113
+ var refUpdateResultSchema = zod.z.object({
114
+ branch: zod.z.string(),
115
+ old_sha: zod.z.string(),
116
+ new_sha: zod.z.string(),
117
+ success: zod.z.boolean(),
118
+ status: zod.z.string(),
119
+ message: zod.z.string().optional()
120
+ });
121
+ var commitPackCommitSchema = zod.z.object({
122
+ commit_sha: zod.z.string(),
123
+ tree_sha: zod.z.string(),
124
+ target_branch: zod.z.string(),
125
+ pack_bytes: zod.z.number(),
126
+ blob_count: zod.z.number()
127
+ });
128
+ var restoreCommitCommitSchema = commitPackCommitSchema.omit({ blob_count: true });
129
+ var refUpdateResultWithOptionalsSchema = zod.z.object({
130
+ branch: zod.z.string().optional(),
131
+ old_sha: zod.z.string().optional(),
132
+ new_sha: zod.z.string().optional(),
133
+ success: zod.z.boolean().optional(),
134
+ status: zod.z.string(),
135
+ message: zod.z.string().optional()
136
+ });
137
+ var commitPackAckSchema = zod.z.object({
138
+ commit: commitPackCommitSchema,
139
+ result: refUpdateResultSchema
140
+ });
141
+ var restoreCommitAckSchema = zod.z.object({
142
+ commit: restoreCommitCommitSchema,
143
+ result: refUpdateResultSchema.extend({ success: zod.z.literal(true) })
144
+ });
145
+ var commitPackResponseSchema = zod.z.object({
146
+ commit: commitPackCommitSchema.partial().optional().nullable(),
147
+ result: refUpdateResultWithOptionalsSchema
148
+ });
149
+ var restoreCommitResponseSchema = zod.z.object({
150
+ commit: restoreCommitCommitSchema.partial().optional().nullable(),
151
+ result: refUpdateResultWithOptionalsSchema
152
+ });
153
+ var errorEnvelopeSchema = zod.z.object({
154
+ error: zod.z.string()
155
+ });
156
+
12
157
  // src/commit.ts
13
158
  var MAX_CHUNK_BYTES = 4 * 1024 * 1024;
14
159
  var DEFAULT_TTL_SECONDS = 60 * 60;
@@ -23,18 +168,30 @@ var CommitBuilderImpl = class {
23
168
  this.options = { ...deps.options };
24
169
  this.getAuthToken = deps.getAuthToken;
25
170
  this.transport = deps.transport;
26
- const trimmedTarget = this.options.targetRef?.trim();
171
+ const trimmedTarget = this.options.targetBranch?.trim();
27
172
  const trimmedMessage = this.options.commitMessage?.trim();
173
+ const trimmedAuthorName = this.options.author?.name?.trim();
174
+ const trimmedAuthorEmail = this.options.author?.email?.trim();
28
175
  if (!trimmedTarget) {
29
- throw new Error("createCommit targetRef is required");
176
+ throw new Error("createCommit targetBranch is required");
177
+ }
178
+ if (trimmedTarget.startsWith("refs/")) {
179
+ throw new Error("createCommit targetBranch must not include refs/ prefix");
30
180
  }
31
181
  if (!trimmedMessage) {
32
182
  throw new Error("createCommit commitMessage is required");
33
183
  }
34
- this.options.targetRef = trimmedTarget;
184
+ if (!trimmedAuthorName || !trimmedAuthorEmail) {
185
+ throw new Error("createCommit author name and email are required");
186
+ }
187
+ this.options.targetBranch = trimmedTarget;
35
188
  this.options.commitMessage = trimmedMessage;
36
- if (typeof this.options.baseRef === "string") {
37
- this.options.baseRef = this.options.baseRef.trim();
189
+ this.options.author = {
190
+ name: trimmedAuthorName,
191
+ email: trimmedAuthorEmail
192
+ };
193
+ if (typeof this.options.expectedHeadSha === "string") {
194
+ this.options.expectedHeadSha = this.options.expectedHeadSha.trim();
38
195
  }
39
196
  }
40
197
  addFile(path, source, options) {
@@ -52,11 +209,21 @@ var CommitBuilderImpl = class {
52
209
  return this;
53
210
  }
54
211
  addFileFromString(path, contents, options) {
55
- const encoding = options?.encoding;
56
- if (encoding && encoding !== "utf8" && encoding !== "utf-8") {
57
- throw new Error(`Unsupported encoding "${encoding}". Only UTF-8 is supported.`);
212
+ const encoding = options?.encoding ?? "utf8";
213
+ const normalizedEncoding = encoding === "utf-8" ? "utf8" : encoding;
214
+ let data;
215
+ if (normalizedEncoding === "utf8") {
216
+ data = new TextEncoder().encode(contents);
217
+ } else if (BufferCtor) {
218
+ data = BufferCtor.from(
219
+ contents,
220
+ normalizedEncoding
221
+ );
222
+ } else {
223
+ throw new Error(
224
+ `Unsupported encoding "${encoding}" in this environment. Non-UTF encodings require Node.js Buffer support.`
225
+ );
58
226
  }
59
- const data = new TextEncoder().encode(contents);
60
227
  return this.addFile(path, data, options);
61
228
  }
62
229
  deletePath(path) {
@@ -78,12 +245,13 @@ var CommitBuilderImpl = class {
78
245
  chunks: chunkify(op.streamFactory())
79
246
  }));
80
247
  const authorization = await this.getAuthToken();
81
- return this.transport.send({
248
+ const ack = await this.transport.send({
82
249
  authorization,
83
250
  signal: this.options.signal,
84
251
  metadata,
85
252
  blobs: blobEntries
86
253
  });
254
+ return buildCommitResult(ack);
87
255
  }
88
256
  buildMetadata() {
89
257
  const files = this.operations.map((op) => {
@@ -98,18 +266,22 @@ var CommitBuilderImpl = class {
98
266
  return entry;
99
267
  });
100
268
  const metadata = {
101
- target_ref: this.options.targetRef,
269
+ target_branch: this.options.targetBranch,
102
270
  commit_message: this.options.commitMessage,
271
+ author: {
272
+ name: this.options.author.name,
273
+ email: this.options.author.email
274
+ },
103
275
  files
104
276
  };
105
- if (this.options.baseRef) {
106
- metadata.base_ref = this.options.baseRef;
107
- }
108
- if (this.options.author) {
109
- metadata.author = { ...this.options.author };
277
+ if (this.options.expectedHeadSha) {
278
+ metadata.expected_head_sha = this.options.expectedHeadSha;
110
279
  }
111
280
  if (this.options.committer) {
112
- metadata.committer = { ...this.options.committer };
281
+ metadata.committer = {
282
+ name: this.options.committer.name,
283
+ email: this.options.committer.email
284
+ };
113
285
  }
114
286
  return metadata;
115
287
  }
@@ -145,10 +317,15 @@ var FetchCommitTransport = class {
145
317
  signal: request.signal
146
318
  });
147
319
  if (!response.ok) {
148
- const text = await response.text();
149
- throw new Error(`createCommit request failed (${response.status}): ${text}`);
320
+ const { statusMessage, statusLabel, refUpdate } = await parseCommitPackError(response);
321
+ throw new RefUpdateError(statusMessage, {
322
+ status: statusLabel,
323
+ message: statusMessage,
324
+ refUpdate
325
+ });
150
326
  }
151
- return await response.json();
327
+ const ack = commitPackAckSchema.parse(await response.json());
328
+ return ack;
152
329
  }
153
330
  };
154
331
  function toRequestBody(iterable) {
@@ -195,6 +372,34 @@ function buildMessageIterable(metadata, blobs) {
195
372
  }
196
373
  };
197
374
  }
375
+ function buildCommitResult(ack) {
376
+ const refUpdate = toRefUpdate(ack.result);
377
+ if (!ack.result.success) {
378
+ throw new RefUpdateError(
379
+ ack.result.message ?? `Commit failed with status ${ack.result.status}`,
380
+ {
381
+ status: ack.result.status,
382
+ message: ack.result.message,
383
+ refUpdate
384
+ }
385
+ );
386
+ }
387
+ return {
388
+ commitSha: ack.commit.commit_sha,
389
+ treeSha: ack.commit.tree_sha,
390
+ targetBranch: ack.commit.target_branch,
391
+ packBytes: ack.commit.pack_bytes,
392
+ blobCount: ack.commit.blob_count,
393
+ refUpdate
394
+ };
395
+ }
396
+ function toRefUpdate(result) {
397
+ return {
398
+ branch: result.branch,
399
+ oldSha: result.old_sha,
400
+ newSha: result.new_sha
401
+ };
402
+ }
198
403
  async function* chunkify(source) {
199
404
  let pending = null;
200
405
  let produced = false;
@@ -254,6 +459,10 @@ async function* toAsyncIterable(source) {
254
459
  return;
255
460
  }
256
461
  }
462
+ if (isReadableStreamLike(source)) {
463
+ yield* readReadableStream(source);
464
+ return;
465
+ }
257
466
  if (isAsyncIterable(source)) {
258
467
  for await (const chunk of source) {
259
468
  yield ensureUint8Array(chunk);
@@ -352,15 +561,104 @@ function createCommitBuilder(deps) {
352
561
  return new CommitBuilderImpl(deps);
353
562
  }
354
563
  function resolveCommitTtlSeconds(options) {
355
- return typeof options?.ttl === "number" && options.ttl > 0 ? options.ttl : DEFAULT_TTL_SECONDS;
564
+ if (typeof options?.ttl === "number" && options.ttl > 0) {
565
+ return options.ttl;
566
+ }
567
+ return DEFAULT_TTL_SECONDS;
568
+ }
569
+ async function parseCommitPackError(response) {
570
+ const fallbackMessage = `createCommit request failed (${response.status} ${response.statusText})`;
571
+ const cloned = response.clone();
572
+ let jsonBody;
573
+ try {
574
+ jsonBody = await cloned.json();
575
+ } catch {
576
+ jsonBody = void 0;
577
+ }
578
+ let textBody;
579
+ if (jsonBody === void 0) {
580
+ try {
581
+ textBody = await response.text();
582
+ } catch {
583
+ textBody = void 0;
584
+ }
585
+ }
586
+ const defaultStatus = (() => {
587
+ const inferred = inferRefUpdateReason(String(response.status));
588
+ return inferred === "unknown" ? "failed" : inferred;
589
+ })();
590
+ let statusLabel = defaultStatus;
591
+ let refUpdate;
592
+ let message;
593
+ if (jsonBody !== void 0) {
594
+ const parsedResponse = commitPackResponseSchema.safeParse(jsonBody);
595
+ if (parsedResponse.success) {
596
+ const result = parsedResponse.data.result;
597
+ if (typeof result.status === "string" && result.status.trim() !== "") {
598
+ statusLabel = result.status.trim();
599
+ }
600
+ refUpdate = toPartialRefUpdateFields(result.branch, result.old_sha, result.new_sha);
601
+ if (typeof result.message === "string" && result.message.trim() !== "") {
602
+ message = result.message.trim();
603
+ }
604
+ }
605
+ if (!message) {
606
+ const parsedError = errorEnvelopeSchema.safeParse(jsonBody);
607
+ if (parsedError.success) {
608
+ const trimmed = parsedError.data.error.trim();
609
+ if (trimmed) {
610
+ message = trimmed;
611
+ }
612
+ }
613
+ }
614
+ }
615
+ if (!message && typeof jsonBody === "string" && jsonBody.trim() !== "") {
616
+ message = jsonBody.trim();
617
+ }
618
+ if (!message && textBody && textBody.trim() !== "") {
619
+ message = textBody.trim();
620
+ }
621
+ return {
622
+ statusMessage: message ?? fallbackMessage,
623
+ statusLabel,
624
+ refUpdate
625
+ };
626
+ }
627
+ function toPartialRefUpdateFields(branch, oldSha, newSha) {
628
+ const refUpdate = {};
629
+ if (typeof branch === "string" && branch.trim() !== "") {
630
+ refUpdate.branch = branch.trim();
631
+ }
632
+ if (typeof oldSha === "string" && oldSha.trim() !== "") {
633
+ refUpdate.oldSha = oldSha.trim();
634
+ }
635
+ if (typeof newSha === "string" && newSha.trim() !== "") {
636
+ refUpdate.newSha = newSha.trim();
637
+ }
638
+ return Object.keys(refUpdate).length > 0 ? refUpdate : void 0;
356
639
  }
357
640
 
358
641
  // src/fetch.ts
642
+ var ApiError = class extends Error {
643
+ status;
644
+ statusText;
645
+ method;
646
+ url;
647
+ body;
648
+ constructor(params) {
649
+ super(params.message);
650
+ this.name = "ApiError";
651
+ this.status = params.status;
652
+ this.statusText = params.statusText;
653
+ this.method = params.method;
654
+ this.url = params.url;
655
+ this.body = params.body;
656
+ }
657
+ };
359
658
  var ApiFetcher = class {
360
659
  constructor(API_BASE_URL2, version) {
361
660
  this.API_BASE_URL = API_BASE_URL2;
362
661
  this.version = version;
363
- console.log("api fetcher created", API_BASE_URL2, version);
364
662
  }
365
663
  getBaseUrl() {
366
664
  return `${this.API_BASE_URL}/api/v${this.version}`;
@@ -390,9 +688,48 @@ var ApiFetcher = class {
390
688
  const response = await fetch(requestUrl, requestOptions);
391
689
  if (!response.ok) {
392
690
  const allowed = options?.allowedStatus ?? [];
393
- if (!allowed.includes(response.status)) {
394
- throw new Error(`Failed to fetch ${method} ${requestUrl}: ${response.statusText}`);
691
+ if (allowed.includes(response.status)) {
692
+ return response;
693
+ }
694
+ let errorBody;
695
+ let message;
696
+ const contentType = response.headers.get("content-type") ?? "";
697
+ try {
698
+ if (contentType.includes("application/json")) {
699
+ errorBody = await response.json();
700
+ } else {
701
+ const text = await response.text();
702
+ errorBody = text;
703
+ }
704
+ } catch {
705
+ try {
706
+ errorBody = await response.text();
707
+ } catch {
708
+ errorBody = void 0;
709
+ }
710
+ }
711
+ if (typeof errorBody === "string") {
712
+ const trimmed = errorBody.trim();
713
+ if (trimmed) {
714
+ message = trimmed;
715
+ }
716
+ } else if (errorBody && typeof errorBody === "object") {
717
+ const parsedError = errorEnvelopeSchema.safeParse(errorBody);
718
+ if (parsedError.success) {
719
+ const trimmed = parsedError.data.error.trim();
720
+ if (trimmed) {
721
+ message = trimmed;
722
+ }
723
+ }
395
724
  }
725
+ throw new ApiError({
726
+ message: message ?? `Request ${method} ${requestUrl} failed with status ${response.status} ${response.statusText}`,
727
+ status: response.status,
728
+ statusText: response.statusText,
729
+ method,
730
+ url: requestUrl,
731
+ body: errorBody
732
+ });
396
733
  }
397
734
  return response;
398
735
  }
@@ -565,9 +902,9 @@ async function validateWebhook(payload, headers, secret, options = {}) {
565
902
  return validationResult;
566
903
  }
567
904
  const payloadStr = typeof payload === "string" ? payload : payload.toString("utf8");
568
- let parsedPayload;
905
+ let parsedJson;
569
906
  try {
570
- parsedPayload = JSON.parse(payloadStr);
907
+ parsedJson = JSON.parse(payloadStr);
571
908
  } catch {
572
909
  return {
573
910
  valid: false,
@@ -575,25 +912,283 @@ async function validateWebhook(payload, headers, secret, options = {}) {
575
912
  timestamp: validationResult.timestamp
576
913
  };
577
914
  }
915
+ const conversion = convertWebhookPayload(String(eventType), parsedJson);
916
+ if (!conversion.valid) {
917
+ return {
918
+ valid: false,
919
+ error: conversion.error,
920
+ timestamp: validationResult.timestamp
921
+ };
922
+ }
578
923
  return {
579
924
  valid: true,
580
925
  eventType,
581
926
  timestamp: validationResult.timestamp,
582
- payload: parsedPayload
927
+ payload: conversion.payload
928
+ };
929
+ }
930
+ function convertWebhookPayload(eventType, raw) {
931
+ if (eventType === "push") {
932
+ if (!isRawWebhookPushEvent(raw)) {
933
+ return {
934
+ valid: false,
935
+ error: "Invalid push payload"
936
+ };
937
+ }
938
+ return {
939
+ valid: true,
940
+ payload: transformPushEvent(raw)
941
+ };
942
+ }
943
+ const fallbackPayload = { type: eventType, raw };
944
+ return {
945
+ valid: true,
946
+ payload: fallbackPayload
947
+ };
948
+ }
949
+ function transformPushEvent(raw) {
950
+ return {
951
+ type: "push",
952
+ repository: {
953
+ id: raw.repository.id,
954
+ url: raw.repository.url
955
+ },
956
+ ref: raw.ref,
957
+ before: raw.before,
958
+ after: raw.after,
959
+ customerId: raw.customer_id,
960
+ pushedAt: new Date(raw.pushed_at),
961
+ rawPushedAt: raw.pushed_at
583
962
  };
584
963
  }
964
+ function isRawWebhookPushEvent(value) {
965
+ if (!isRecord(value)) {
966
+ return false;
967
+ }
968
+ if (!isRecord(value.repository)) {
969
+ return false;
970
+ }
971
+ return typeof value.repository.id === "string" && typeof value.repository.url === "string" && typeof value.ref === "string" && typeof value.before === "string" && typeof value.after === "string" && typeof value.customer_id === "string" && typeof value.pushed_at === "string";
972
+ }
973
+ function isRecord(value) {
974
+ return typeof value === "object" && value !== null;
975
+ }
585
976
 
586
977
  // src/index.ts
587
978
  var API_BASE_URL = "https://api.code.storage";
588
979
  var STORAGE_BASE_URL = "code.storage";
589
980
  var API_VERSION = 1;
590
981
  var apiInstanceMap = /* @__PURE__ */ new Map();
982
+ var DEFAULT_TOKEN_TTL_SECONDS = 60 * 60;
983
+ var RESTORE_COMMIT_ALLOWED_STATUS = [
984
+ 400,
985
+ // Bad Request - validation errors
986
+ 401,
987
+ // Unauthorized - missing/invalid auth header
988
+ 403,
989
+ // Forbidden - missing git:write scope
990
+ 404,
991
+ // Not Found - repo lookup failures
992
+ 408,
993
+ // Request Timeout - client cancelled
994
+ 409,
995
+ // Conflict - concurrent ref updates
996
+ 412,
997
+ // Precondition Failed - optimistic concurrency
998
+ 422,
999
+ // Unprocessable Entity - metadata issues
1000
+ 429,
1001
+ // Too Many Requests - upstream throttling
1002
+ 499,
1003
+ // Client Closed Request - storage cancellation
1004
+ 500,
1005
+ // Internal Server Error - generic failure
1006
+ 502,
1007
+ // Bad Gateway - storage/gateway bridge issues
1008
+ 503,
1009
+ // Service Unavailable - storage selection failures
1010
+ 504
1011
+ // Gateway Timeout - long-running storage operations
1012
+ ];
1013
+ function resolveInvocationTtlSeconds(options, defaultValue = DEFAULT_TOKEN_TTL_SECONDS) {
1014
+ if (typeof options?.ttl === "number" && options.ttl > 0) {
1015
+ return options.ttl;
1016
+ }
1017
+ return defaultValue;
1018
+ }
1019
+ function toRefUpdate2(result) {
1020
+ return {
1021
+ branch: result.branch,
1022
+ oldSha: result.old_sha,
1023
+ newSha: result.new_sha
1024
+ };
1025
+ }
1026
+ function buildRestoreCommitResult(ack) {
1027
+ const refUpdate = toRefUpdate2(ack.result);
1028
+ if (!ack.result.success) {
1029
+ throw new RefUpdateError(
1030
+ ack.result.message ?? `Restore commit failed with status ${ack.result.status}`,
1031
+ {
1032
+ status: ack.result.status,
1033
+ message: ack.result.message,
1034
+ refUpdate
1035
+ }
1036
+ );
1037
+ }
1038
+ return {
1039
+ commitSha: ack.commit.commit_sha,
1040
+ treeSha: ack.commit.tree_sha,
1041
+ targetBranch: ack.commit.target_branch,
1042
+ packBytes: ack.commit.pack_bytes,
1043
+ refUpdate
1044
+ };
1045
+ }
1046
+ function toPartialRefUpdate(branch, oldSha, newSha) {
1047
+ const refUpdate = {};
1048
+ if (typeof branch === "string" && branch.trim() !== "") {
1049
+ refUpdate.branch = branch;
1050
+ }
1051
+ if (typeof oldSha === "string" && oldSha.trim() !== "") {
1052
+ refUpdate.oldSha = oldSha;
1053
+ }
1054
+ if (typeof newSha === "string" && newSha.trim() !== "") {
1055
+ refUpdate.newSha = newSha;
1056
+ }
1057
+ return Object.keys(refUpdate).length > 0 ? refUpdate : void 0;
1058
+ }
1059
+ function parseRestoreCommitPayload(payload) {
1060
+ const ack = restoreCommitAckSchema.safeParse(payload);
1061
+ if (ack.success) {
1062
+ return { ack: ack.data };
1063
+ }
1064
+ const failure = restoreCommitResponseSchema.safeParse(payload);
1065
+ if (failure.success) {
1066
+ const result = failure.data.result;
1067
+ return {
1068
+ failure: {
1069
+ status: result.status,
1070
+ message: result.message,
1071
+ refUpdate: toPartialRefUpdate(result.branch, result.old_sha, result.new_sha)
1072
+ }
1073
+ };
1074
+ }
1075
+ return null;
1076
+ }
1077
+ function httpStatusToRestoreStatus(status) {
1078
+ switch (status) {
1079
+ case 409:
1080
+ return "conflict";
1081
+ case 412:
1082
+ return "precondition_failed";
1083
+ default:
1084
+ return `${status}`;
1085
+ }
1086
+ }
591
1087
  function getApiInstance(baseUrl, version) {
592
1088
  if (!apiInstanceMap.has(`${baseUrl}--${version}`)) {
593
1089
  apiInstanceMap.set(`${baseUrl}--${version}`, new ApiFetcher(baseUrl, version));
594
1090
  }
595
1091
  return apiInstanceMap.get(`${baseUrl}--${version}`);
596
1092
  }
1093
+ function transformBranchInfo(raw) {
1094
+ return {
1095
+ cursor: raw.cursor,
1096
+ name: raw.name,
1097
+ headSha: raw.head_sha,
1098
+ createdAt: raw.created_at
1099
+ };
1100
+ }
1101
+ function transformListBranchesResult(raw) {
1102
+ return {
1103
+ branches: raw.branches.map(transformBranchInfo),
1104
+ nextCursor: raw.next_cursor ?? void 0,
1105
+ hasMore: raw.has_more
1106
+ };
1107
+ }
1108
+ function transformCommitInfo(raw) {
1109
+ const parsedDate = new Date(raw.date);
1110
+ return {
1111
+ sha: raw.sha,
1112
+ message: raw.message,
1113
+ authorName: raw.author_name,
1114
+ authorEmail: raw.author_email,
1115
+ committerName: raw.committer_name,
1116
+ committerEmail: raw.committer_email,
1117
+ date: parsedDate,
1118
+ rawDate: raw.date
1119
+ };
1120
+ }
1121
+ function transformListCommitsResult(raw) {
1122
+ return {
1123
+ commits: raw.commits.map(transformCommitInfo),
1124
+ nextCursor: raw.next_cursor ?? void 0,
1125
+ hasMore: raw.has_more
1126
+ };
1127
+ }
1128
+ function normalizeDiffState(rawState) {
1129
+ if (!rawState) {
1130
+ return "unknown";
1131
+ }
1132
+ const leading = rawState.trim()[0]?.toUpperCase();
1133
+ switch (leading) {
1134
+ case "A":
1135
+ return "added";
1136
+ case "M":
1137
+ return "modified";
1138
+ case "D":
1139
+ return "deleted";
1140
+ case "R":
1141
+ return "renamed";
1142
+ case "C":
1143
+ return "copied";
1144
+ case "T":
1145
+ return "type_changed";
1146
+ case "U":
1147
+ return "unmerged";
1148
+ default:
1149
+ return "unknown";
1150
+ }
1151
+ }
1152
+ function transformFileDiff(raw) {
1153
+ const normalizedState = normalizeDiffState(raw.state);
1154
+ return {
1155
+ path: raw.path,
1156
+ state: normalizedState,
1157
+ rawState: raw.state,
1158
+ oldPath: raw.old_path ?? void 0,
1159
+ raw: raw.raw,
1160
+ bytes: raw.bytes,
1161
+ isEof: raw.is_eof
1162
+ };
1163
+ }
1164
+ function transformFilteredFile(raw) {
1165
+ const normalizedState = normalizeDiffState(raw.state);
1166
+ return {
1167
+ path: raw.path,
1168
+ state: normalizedState,
1169
+ rawState: raw.state,
1170
+ oldPath: raw.old_path ?? void 0,
1171
+ bytes: raw.bytes,
1172
+ isEof: raw.is_eof
1173
+ };
1174
+ }
1175
+ function transformBranchDiffResult(raw) {
1176
+ return {
1177
+ branch: raw.branch,
1178
+ base: raw.base,
1179
+ stats: raw.stats,
1180
+ files: raw.files.map(transformFileDiff),
1181
+ filteredFiles: raw.filtered_files.map(transformFilteredFile)
1182
+ };
1183
+ }
1184
+ function transformCommitDiffResult(raw) {
1185
+ return {
1186
+ sha: raw.sha,
1187
+ stats: raw.stats,
1188
+ files: raw.files.map(transformFileDiff),
1189
+ filteredFiles: raw.filtered_files.map(transformFilteredFile)
1190
+ };
1191
+ }
597
1192
  var RepoImpl = class {
598
1193
  constructor(id, options, generateJWT) {
599
1194
  this.id = id;
@@ -613,10 +1208,10 @@ var RepoImpl = class {
613
1208
  return url.toString();
614
1209
  }
615
1210
  async getFileStream(options) {
1211
+ const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
616
1212
  const jwt = await this.generateJWT(this.id, {
617
1213
  permissions: ["git:read"],
618
- ttl: options?.ttl ?? 1 * 60 * 60
619
- // 1hr in seconds
1214
+ ttl
620
1215
  });
621
1216
  const params = {
622
1217
  path: options.path
@@ -627,39 +1222,46 @@ var RepoImpl = class {
627
1222
  return this.api.get({ path: "repos/file", params }, jwt);
628
1223
  }
629
1224
  async listFiles(options) {
1225
+ const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
630
1226
  const jwt = await this.generateJWT(this.id, {
631
1227
  permissions: ["git:read"],
632
- ttl: options?.ttl ?? 1 * 60 * 60
633
- // 1hr in seconds
1228
+ ttl
634
1229
  });
635
1230
  const params = options?.ref ? { ref: options.ref } : void 0;
636
1231
  const response = await this.api.get({ path: "repos/files", params }, jwt);
637
- return await response.json();
1232
+ const raw = listFilesResponseSchema.parse(await response.json());
1233
+ return { paths: raw.paths, ref: raw.ref };
638
1234
  }
639
1235
  async listBranches(options) {
1236
+ const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
640
1237
  const jwt = await this.generateJWT(this.id, {
641
1238
  permissions: ["git:read"],
642
- ttl: options?.ttl ?? 1 * 60 * 60
643
- // 1hr in seconds
1239
+ ttl
644
1240
  });
1241
+ const cursor = options?.cursor;
1242
+ const limit = options?.limit;
645
1243
  let params;
646
- if (options?.cursor || !options?.limit) {
1244
+ if (typeof cursor === "string" || typeof limit === "number") {
647
1245
  params = {};
648
- if (options?.cursor) {
649
- params.cursor = options.cursor;
1246
+ if (typeof cursor === "string") {
1247
+ params.cursor = cursor;
650
1248
  }
651
- if (typeof options?.limit == "number") {
652
- params.limit = options.limit.toString();
1249
+ if (typeof limit === "number") {
1250
+ params.limit = limit.toString();
653
1251
  }
654
1252
  }
655
1253
  const response = await this.api.get({ path: "repos/branches", params }, jwt);
656
- return await response.json();
1254
+ const raw = listBranchesResponseSchema.parse(await response.json());
1255
+ return transformListBranchesResult({
1256
+ ...raw,
1257
+ next_cursor: raw.next_cursor ?? void 0
1258
+ });
657
1259
  }
658
1260
  async listCommits(options) {
1261
+ const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
659
1262
  const jwt = await this.generateJWT(this.id, {
660
1263
  permissions: ["git:read"],
661
- ttl: options?.ttl ?? 1 * 60 * 60
662
- // 1hr in seconds
1264
+ ttl
663
1265
  });
664
1266
  let params;
665
1267
  if (options?.branch || options?.cursor || options?.limit) {
@@ -675,13 +1277,17 @@ var RepoImpl = class {
675
1277
  }
676
1278
  }
677
1279
  const response = await this.api.get({ path: "repos/commits", params }, jwt);
678
- return await response.json();
1280
+ const raw = listCommitsResponseSchema.parse(await response.json());
1281
+ return transformListCommitsResult({
1282
+ ...raw,
1283
+ next_cursor: raw.next_cursor ?? void 0
1284
+ });
679
1285
  }
680
1286
  async getBranchDiff(options) {
1287
+ const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
681
1288
  const jwt = await this.generateJWT(this.id, {
682
1289
  permissions: ["git:read"],
683
- ttl: options?.ttl ?? 1 * 60 * 60
684
- // 1hr in seconds
1290
+ ttl
685
1291
  });
686
1292
  const params = {
687
1293
  branch: options.branch
@@ -690,38 +1296,27 @@ var RepoImpl = class {
690
1296
  params.base = options.base;
691
1297
  }
692
1298
  const response = await this.api.get({ path: "repos/branches/diff", params }, jwt);
693
- return await response.json();
1299
+ const raw = branchDiffResponseSchema.parse(await response.json());
1300
+ return transformBranchDiffResult(raw);
694
1301
  }
695
1302
  async getCommitDiff(options) {
1303
+ const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
696
1304
  const jwt = await this.generateJWT(this.id, {
697
1305
  permissions: ["git:read"],
698
- ttl: options?.ttl ?? 1 * 60 * 60
699
- // 1hr in seconds
1306
+ ttl
700
1307
  });
701
1308
  const params = {
702
1309
  sha: options.sha
703
1310
  };
704
1311
  const response = await this.api.get({ path: "repos/diff", params }, jwt);
705
- return await response.json();
706
- }
707
- async getCommit(options) {
708
- const jwt = await this.generateJWT(this.id, {
709
- permissions: ["git:read"],
710
- ttl: options?.ttl ?? 1 * 60 * 60
711
- // 1hr in seconds
712
- });
713
- const params = {
714
- repo: this.id,
715
- sha: options.sha
716
- };
717
- const response = await this.api.get({ path: "commit", params }, jwt);
718
- return await response.json();
1312
+ const raw = commitDiffResponseSchema.parse(await response.json());
1313
+ return transformCommitDiffResult(raw);
719
1314
  }
720
1315
  async pullUpstream(options) {
1316
+ const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
721
1317
  const jwt = await this.generateJWT(this.id, {
722
1318
  permissions: ["git:write"],
723
- ttl: options?.ttl ?? 1 * 60 * 60
724
- // 1hr in seconds
1319
+ ttl
725
1320
  });
726
1321
  const body = {};
727
1322
  if (options.ref) {
@@ -733,15 +1328,83 @@ var RepoImpl = class {
733
1328
  }
734
1329
  return;
735
1330
  }
1331
+ async restoreCommit(options) {
1332
+ const targetBranch = options?.targetBranch?.trim();
1333
+ if (!targetBranch) {
1334
+ throw new Error("restoreCommit targetBranch is required");
1335
+ }
1336
+ if (targetBranch.startsWith("refs/")) {
1337
+ throw new Error("restoreCommit targetBranch must not include refs/ prefix");
1338
+ }
1339
+ const targetCommitSha = options?.targetCommitSha?.trim();
1340
+ if (!targetCommitSha) {
1341
+ throw new Error("restoreCommit targetCommitSha is required");
1342
+ }
1343
+ const commitMessage = options?.commitMessage?.trim();
1344
+ const authorName = options.author?.name?.trim();
1345
+ const authorEmail = options.author?.email?.trim();
1346
+ if (!authorName || !authorEmail) {
1347
+ throw new Error("restoreCommit author name and email are required");
1348
+ }
1349
+ const ttl = resolveCommitTtlSeconds(options);
1350
+ const jwt = await this.generateJWT(this.id, {
1351
+ permissions: ["git:write"],
1352
+ ttl
1353
+ });
1354
+ const metadata = {
1355
+ target_branch: targetBranch,
1356
+ target_commit_sha: targetCommitSha,
1357
+ author: {
1358
+ name: authorName,
1359
+ email: authorEmail
1360
+ }
1361
+ };
1362
+ if (commitMessage) {
1363
+ metadata.commit_message = commitMessage;
1364
+ }
1365
+ const expectedHeadSha = options.expectedHeadSha?.trim();
1366
+ if (expectedHeadSha) {
1367
+ metadata.expected_head_sha = expectedHeadSha;
1368
+ }
1369
+ if (options.committer) {
1370
+ const committerName = options.committer.name?.trim();
1371
+ const committerEmail = options.committer.email?.trim();
1372
+ if (!committerName || !committerEmail) {
1373
+ throw new Error("restoreCommit committer name and email are required when provided");
1374
+ }
1375
+ metadata.committer = {
1376
+ name: committerName,
1377
+ email: committerEmail
1378
+ };
1379
+ }
1380
+ const response = await this.api.post({ path: "repos/reset-commits", body: { metadata } }, jwt, {
1381
+ allowedStatus: [...RESTORE_COMMIT_ALLOWED_STATUS]
1382
+ });
1383
+ const payload = await response.json();
1384
+ const parsed = parseRestoreCommitPayload(payload);
1385
+ if (parsed && "ack" in parsed) {
1386
+ return buildRestoreCommitResult(parsed.ack);
1387
+ }
1388
+ const failure = parsed && "failure" in parsed ? parsed.failure : void 0;
1389
+ const status = failure?.status ?? httpStatusToRestoreStatus(response.status);
1390
+ const message = failure?.message ?? `Restore commit failed with HTTP ${response.status}` + (response.statusText ? ` ${response.statusText}` : "");
1391
+ throw new RefUpdateError(message, {
1392
+ status,
1393
+ refUpdate: failure?.refUpdate
1394
+ });
1395
+ }
736
1396
  createCommit(options) {
737
1397
  const version = this.options.apiVersion ?? API_VERSION;
738
1398
  const baseUrl = this.options.apiBaseUrl ?? API_BASE_URL;
739
1399
  const transport = new FetchCommitTransport({ baseUrl, version });
740
- const ttlSeconds = resolveCommitTtlSeconds(options);
741
- const builderOptions = { ...options, ttl: ttlSeconds };
1400
+ const ttl = resolveCommitTtlSeconds(options);
1401
+ const builderOptions = {
1402
+ ...options,
1403
+ ttl
1404
+ };
742
1405
  const getAuthToken = () => this.generateJWT(this.id, {
743
1406
  permissions: ["git:write"],
744
- ttl: ttlSeconds
1407
+ ttl
745
1408
  });
746
1409
  return createCommitBuilder({
747
1410
  options: builderOptions,
@@ -769,13 +1432,15 @@ var GitStorage = class _GitStorage {
769
1432
  const resolvedApiBaseUrl = options.apiBaseUrl ?? _GitStorage.overrides.apiBaseUrl ?? API_BASE_URL;
770
1433
  const resolvedApiVersion = options.apiVersion ?? _GitStorage.overrides.apiVersion ?? API_VERSION;
771
1434
  const resolvedStorageBaseUrl = options.storageBaseUrl ?? _GitStorage.overrides.storageBaseUrl ?? STORAGE_BASE_URL;
1435
+ const resolvedDefaultTtl = options.defaultTTL ?? _GitStorage.overrides.defaultTTL;
772
1436
  this.api = getApiInstance(resolvedApiBaseUrl, resolvedApiVersion);
773
1437
  this.options = {
774
1438
  key: options.key,
775
1439
  name: options.name,
776
1440
  apiBaseUrl: resolvedApiBaseUrl,
777
1441
  apiVersion: resolvedApiVersion,
778
- storageBaseUrl: resolvedStorageBaseUrl
1442
+ storageBaseUrl: resolvedStorageBaseUrl,
1443
+ defaultTTL: resolvedDefaultTtl
779
1444
  };
780
1445
  }
781
1446
  static override(options) {
@@ -787,10 +1452,10 @@ var GitStorage = class _GitStorage {
787
1452
  */
788
1453
  async createRepo(options) {
789
1454
  const repoId = options?.id || crypto.randomUUID();
1455
+ const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
790
1456
  const jwt = await this.generateJWT(repoId, {
791
1457
  permissions: ["repo:write"],
792
- ttl: options?.ttl ?? 1 * 60 * 60
793
- // 1hr in seconds
1458
+ ttl
794
1459
  });
795
1460
  const baseRepoOptions = options?.baseRepo ? {
796
1461
  provider: "github",
@@ -818,7 +1483,7 @@ var GitStorage = class _GitStorage {
818
1483
  async findOne(options) {
819
1484
  const jwt = await this.generateJWT(options.id, {
820
1485
  permissions: ["git:read"],
821
- ttl: 1 * 60 * 60
1486
+ ttl: DEFAULT_TOKEN_TTL_SECONDS
822
1487
  });
823
1488
  const resp = await this.api.get("repo", jwt, { allowedStatus: [404] });
824
1489
  if (resp.status === 404) {
@@ -839,7 +1504,7 @@ var GitStorage = class _GitStorage {
839
1504
  */
840
1505
  async generateJWT(repoId, options) {
841
1506
  const permissions = options?.permissions || ["git:write", "git:read"];
842
- const ttl = options?.ttl || 365 * 24 * 60 * 60;
1507
+ const ttl = resolveInvocationTtlSeconds(options, this.options.defaultTTL ?? 365 * 24 * 60 * 60);
843
1508
  const now = Math.floor(Date.now() / 1e3);
844
1509
  const payload = {
845
1510
  iss: this.options.name,
@@ -858,7 +1523,9 @@ function createClient(options) {
858
1523
  return new GitStorage(options);
859
1524
  }
860
1525
 
1526
+ exports.ApiError = ApiError;
861
1527
  exports.GitStorage = GitStorage;
1528
+ exports.RefUpdateError = RefUpdateError;
862
1529
  exports.createClient = createClient;
863
1530
  exports.parseSignatureHeader = parseSignatureHeader;
864
1531
  exports.validateWebhook = validateWebhook;