@pierre/storage 0.0.3 → 0.0.6

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 ADDED
@@ -0,0 +1,71 @@
1
+ import type { ValidAPIVersion, ValidMethod, ValidPath } from './types';
2
+
3
+ interface RequestOptions {
4
+ allowedStatus?: number[];
5
+ }
6
+
7
+ export class ApiFetcher {
8
+ constructor(
9
+ private readonly API_BASE_URL: string,
10
+ private readonly version: ValidAPIVersion,
11
+ ) {
12
+ console.log('api fetcher created', API_BASE_URL, version);
13
+ }
14
+
15
+ private getBaseUrl() {
16
+ return `${this.API_BASE_URL}/api/v${this.version}`;
17
+ }
18
+
19
+ private getRequestUrl(path: ValidPath) {
20
+ if (typeof path === 'string') {
21
+ return `${this.getBaseUrl()}/${path}`;
22
+ } else if (path.params) {
23
+ const paramStr = new URLSearchParams(path.params).toString();
24
+ return `${this.getBaseUrl()}/${path.path}${paramStr ? `?${paramStr}` : ''}`;
25
+ } else {
26
+ return `${this.getBaseUrl()}/${path.path}`;
27
+ }
28
+ }
29
+
30
+ private async fetch(path: ValidPath, method: ValidMethod, jwt: string, options?: RequestOptions) {
31
+ const requestUrl = this.getRequestUrl(path);
32
+
33
+ const requestOptions: RequestInit = {
34
+ method,
35
+ headers: {
36
+ Authorization: `Bearer ${jwt}`,
37
+ 'Content-Type': 'application/json',
38
+ },
39
+ };
40
+
41
+ if (method !== 'GET' && typeof path !== 'string' && path.body) {
42
+ requestOptions.body = JSON.stringify(path.body);
43
+ }
44
+
45
+ const response = await fetch(requestUrl, requestOptions);
46
+
47
+ if (!response.ok) {
48
+ const allowed = options?.allowedStatus ?? [];
49
+ if (!allowed.includes(response.status)) {
50
+ throw new Error(`Failed to fetch ${method} ${requestUrl}: ${response.statusText}`);
51
+ }
52
+ }
53
+ return response;
54
+ }
55
+
56
+ async get(path: ValidPath, jwt: string, options?: RequestOptions) {
57
+ return this.fetch(path, 'GET', jwt, options);
58
+ }
59
+
60
+ async post(path: ValidPath, jwt: string, options?: RequestOptions) {
61
+ return this.fetch(path, 'POST', jwt, options);
62
+ }
63
+
64
+ async put(path: ValidPath, jwt: string, options?: RequestOptions) {
65
+ return this.fetch(path, 'PUT', jwt, options);
66
+ }
67
+
68
+ async delete(path: ValidPath, jwt: string, options?: RequestOptions) {
69
+ return this.fetch(path, 'DELETE', jwt, options);
70
+ }
71
+ }
package/src/index.ts CHANGED
@@ -5,50 +5,248 @@
5
5
  */
6
6
 
7
7
  import { importPKCS8, SignJWT } from 'jose';
8
+ import snakecaseKeys from 'snakecase-keys';
9
+ import { ApiFetcher } from './fetch';
10
+ import type {
11
+ CreateRepoOptions,
12
+ FindOneOptions,
13
+ GetBranchDiffOptions,
14
+ GetBranchDiffResponse,
15
+ GetCommitDiffOptions,
16
+ GetCommitDiffResponse,
17
+ GetCommitOptions,
18
+ GetCommitResponse,
19
+ GetFileOptions,
20
+ GetFileResponse,
21
+ GetRemoteURLOptions,
22
+ GitStorageOptions,
23
+ ListBranchesOptions,
24
+ ListBranchesResponse,
25
+ ListCommitsOptions,
26
+ ListCommitsResponse,
27
+ ListFilesOptions,
28
+ ListFilesResponse,
29
+ OverrideableGitStorageOptions,
30
+ Repo,
31
+ RepullOptions,
32
+ ValidAPIVersion,
33
+ } from './types';
8
34
 
9
35
  /**
10
36
  * Type definitions for Pierre Git Storage SDK
11
37
  */
12
38
 
13
- export interface GitStorageOptions {
14
- key: string;
15
- name: string;
16
- }
39
+ // Import additional types from types.ts
40
+ export * from './types';
17
41
 
18
- export interface GetRemoteURLOptions {
19
- permissions?: ('git:write' | 'git:read' | 'repo:write')[];
20
- ttl?: number;
21
- }
42
+ // Export webhook validation utilities
43
+ export { parseSignatureHeader, validateWebhook, validateWebhookSignature } from './webhook';
22
44
 
23
- export interface Repo {
24
- id: string;
25
- getRemoteURL(options?: GetRemoteURLOptions): Promise<string>;
26
- }
45
+ /**
46
+ * Git Storage API
47
+ */
27
48
 
28
- export interface FindOneOptions {
29
- id: string;
30
- }
49
+ declare const __STORAGE_BASE_URL__: string;
50
+ declare const __API_BASE_URL__: string;
31
51
 
32
- export interface CreateRepoOptions {
33
- id?: string;
34
- }
52
+ const API_BASE_URL = __API_BASE_URL__;
53
+ const STORAGE_BASE_URL = __STORAGE_BASE_URL__;
54
+ const API_VERSION: ValidAPIVersion = 1;
35
55
 
36
- export interface CreateRepoResponse {
37
- repo_id: string;
38
- url: string;
56
+ const apiInstanceMap = new Map<string, ApiFetcher>();
57
+
58
+ function getApiInstance(baseUrl: string, version: ValidAPIVersion) {
59
+ if (!apiInstanceMap.has(`${baseUrl}--${version}`)) {
60
+ apiInstanceMap.set(`${baseUrl}--${version}`, new ApiFetcher(baseUrl, version));
61
+ }
62
+ return apiInstanceMap.get(`${baseUrl}--${version}`)!;
39
63
  }
40
64
 
41
65
  /**
42
- * Git Storage API
66
+ * Implementation of the Repo interface
43
67
  */
44
- declare const __API_BASE_URL__: string;
45
- declare const __STORAGE_BASE_URL__: string;
68
+ class RepoImpl implements Repo {
69
+ private readonly api: ApiFetcher;
46
70
 
47
- const API_BASE_URL = __API_BASE_URL__;
48
- const STORAGE_BASE_URL = __STORAGE_BASE_URL__;
71
+ constructor(
72
+ public readonly id: string,
73
+ private readonly options: GitStorageOptions,
74
+ private readonly generateJWT: (
75
+ repoId: string,
76
+ options?: GetRemoteURLOptions,
77
+ ) => Promise<string>,
78
+ ) {
79
+ this.api = getApiInstance(
80
+ this.options.apiBaseUrl ?? API_BASE_URL,
81
+ this.options.apiVersion ?? API_VERSION,
82
+ );
83
+ }
84
+
85
+ async getRemoteURL(urlOptions?: GetRemoteURLOptions): Promise<string> {
86
+ const storageBaseUrl = this.options.storageBaseUrl ?? STORAGE_BASE_URL;
87
+ const url = new URL(`https://${this.options.name}.${storageBaseUrl}/${this.id}.git`);
88
+ url.username = `t`;
89
+ url.password = await this.generateJWT(this.id, urlOptions);
90
+ return url.toString();
91
+ }
92
+
93
+ async getFile(options: GetFileOptions): Promise<GetFileResponse> {
94
+ const jwt = await this.generateJWT(this.id, {
95
+ permissions: ['git:read'],
96
+ ttl: options?.ttl ?? 1 * 60 * 60, // 1hr in seconds
97
+ });
98
+
99
+ const params: Record<string, string> = {
100
+ path: options.path,
101
+ };
102
+
103
+ if (options.ref) {
104
+ params.ref = options.ref;
105
+ }
106
+
107
+ const response = await this.api.get({ path: 'repos/file', params }, jwt);
108
+
109
+ return (await response.json()) as GetFileResponse;
110
+ }
111
+
112
+ async listFiles(options?: ListFilesOptions): Promise<ListFilesResponse> {
113
+ const jwt = await this.generateJWT(this.id, {
114
+ permissions: ['git:read'],
115
+ ttl: options?.ttl ?? 1 * 60 * 60, // 1hr in seconds
116
+ });
117
+
118
+ const params: Record<string, string> | undefined = options?.ref
119
+ ? { ref: options.ref }
120
+ : undefined;
121
+ const response = await this.api.get({ path: 'repos/files', params }, jwt);
122
+
123
+ return (await response.json()) as ListFilesResponse;
124
+ }
125
+
126
+ async listBranches(options?: ListBranchesOptions): Promise<ListBranchesResponse> {
127
+ const jwt = await this.generateJWT(this.id, {
128
+ permissions: ['git:read'],
129
+ ttl: options?.ttl ?? 1 * 60 * 60, // 1hr in seconds
130
+ });
131
+
132
+ let params: Record<string, string> | undefined;
133
+
134
+ if (options?.cursor || !options?.limit) {
135
+ params = {};
136
+ if (options?.cursor) {
137
+ params.cursor = options.cursor;
138
+ }
139
+ if (typeof options?.limit == 'number') {
140
+ params.limit = options.limit.toString();
141
+ }
142
+ }
143
+
144
+ const response = await this.api.get({ path: 'repos/branches', params }, jwt);
145
+
146
+ return (await response.json()) as ListBranchesResponse;
147
+ }
148
+
149
+ async listCommits(options?: ListCommitsOptions): Promise<ListCommitsResponse> {
150
+ const jwt = await this.generateJWT(this.id, {
151
+ permissions: ['git:read'],
152
+ ttl: options?.ttl ?? 1 * 60 * 60, // 1hr in seconds
153
+ });
154
+
155
+ let params: Record<string, string> | undefined;
156
+
157
+ if (options?.branch || options?.cursor || options?.limit) {
158
+ params = {};
159
+ if (options?.branch) {
160
+ params.branch = options.branch;
161
+ }
162
+ if (options?.cursor) {
163
+ params.cursor = options.cursor;
164
+ }
165
+ if (typeof options?.limit == 'number') {
166
+ params.limit = options.limit.toString();
167
+ }
168
+ }
169
+
170
+ const response = await this.api.get({ path: 'repos/commits', params }, jwt);
171
+
172
+ return (await response.json()) as ListCommitsResponse;
173
+ }
174
+
175
+ async getBranchDiff(options: GetBranchDiffOptions): Promise<GetBranchDiffResponse> {
176
+ const jwt = await this.generateJWT(this.id, {
177
+ permissions: ['git:read'],
178
+ ttl: options?.ttl ?? 1 * 60 * 60, // 1hr in seconds
179
+ });
180
+
181
+ const params: Record<string, string> = {
182
+ branch: options.branch,
183
+ };
184
+
185
+ if (options.base) {
186
+ params.base = options.base;
187
+ }
188
+
189
+ const response = await this.api.get({ path: 'repos/branches/diff', params }, jwt);
190
+
191
+ return (await response.json()) as GetBranchDiffResponse;
192
+ }
193
+
194
+ async getCommitDiff(options: GetCommitDiffOptions): Promise<GetCommitDiffResponse> {
195
+ const jwt = await this.generateJWT(this.id, {
196
+ permissions: ['git:read'],
197
+ ttl: options?.ttl ?? 1 * 60 * 60, // 1hr in seconds
198
+ });
199
+
200
+ const params: Record<string, string> = {
201
+ sha: options.sha,
202
+ };
203
+
204
+ const response = await this.api.get({ path: 'repos/diff', params }, jwt);
205
+
206
+ return (await response.json()) as GetCommitDiffResponse;
207
+ }
208
+
209
+ async getCommit(options: GetCommitOptions): Promise<GetCommitResponse> {
210
+ const jwt = await this.generateJWT(this.id, {
211
+ permissions: ['git:read'],
212
+ ttl: options?.ttl ?? 1 * 60 * 60, // 1hr in seconds
213
+ });
214
+
215
+ const params: Record<string, string> = {
216
+ repo: this.id,
217
+ sha: options.sha,
218
+ };
219
+ const response = await this.api.get({ path: 'commit', params }, jwt);
220
+
221
+ return (await response.json()) as GetCommitResponse;
222
+ }
223
+
224
+ async repull(options: RepullOptions): Promise<void> {
225
+ const jwt = await this.generateJWT(this.id, {
226
+ permissions: ['git:write'],
227
+ ttl: options?.ttl ?? 1 * 60 * 60, // 1hr in seconds
228
+ });
229
+
230
+ const body: Record<string, string> = {};
231
+
232
+ if (options.ref) {
233
+ body.ref = options.ref;
234
+ }
235
+
236
+ const response = await this.api.post({ path: 'repos/repull', body }, jwt);
237
+
238
+ if (response.status !== 202) {
239
+ throw new Error(`Repull failed: ${response.status} ${await response.text()}`);
240
+ }
241
+
242
+ return;
243
+ }
244
+ }
49
245
 
50
246
  export class GitStorage {
247
+ private static overrides: OverrideableGitStorageOptions = {};
51
248
  private options: GitStorageOptions;
249
+ private api: ApiFetcher;
52
250
 
53
251
  constructor(options: GitStorageOptions) {
54
252
  if (
@@ -71,12 +269,27 @@ export class GitStorage {
71
269
  throw new Error('GitStorage key must be a non-empty string.');
72
270
  }
73
271
 
272
+ const resolvedApiBaseUrl =
273
+ options.apiBaseUrl ?? GitStorage.overrides.apiBaseUrl ?? API_BASE_URL;
274
+ const resolvedApiVersion = options.apiVersion ?? GitStorage.overrides.apiVersion ?? API_VERSION;
275
+ const resolvedStorageBaseUrl =
276
+ options.storageBaseUrl ?? GitStorage.overrides.storageBaseUrl ?? STORAGE_BASE_URL;
277
+
278
+ this.api = getApiInstance(resolvedApiBaseUrl, resolvedApiVersion);
279
+
74
280
  this.options = {
75
281
  key: options.key,
76
282
  name: options.name,
283
+ apiBaseUrl: resolvedApiBaseUrl,
284
+ apiVersion: resolvedApiVersion,
285
+ storageBaseUrl: resolvedStorageBaseUrl,
77
286
  };
78
287
  }
79
288
 
289
+ static override(options: OverrideableGitStorageOptions): void {
290
+ this.overrides = Object.assign({}, this.overrides, options);
291
+ }
292
+
80
293
  /**
81
294
  * Create a new repository
82
295
  * @returns The created repository
@@ -86,29 +299,32 @@ export class GitStorage {
86
299
 
87
300
  const jwt = await this.generateJWT(repoId, {
88
301
  permissions: ['repo:write'],
89
- ttl: 1 * 60 * 60, // 1hr in seconds
302
+ ttl: options?.ttl ?? 1 * 60 * 60, // 1hr in seconds
90
303
  });
91
304
 
92
- const response = await fetch(`${API_BASE_URL}/api/v1/repos`, {
93
- method: 'POST',
94
- headers: {
95
- Authorization: `Bearer ${jwt}`,
96
- },
97
- });
305
+ const baseRepoOptions = options?.baseRepo
306
+ ? {
307
+ provider: 'github',
308
+ ...snakecaseKeys(options.baseRepo as unknown as Record<string, unknown>),
309
+ }
310
+ : null;
98
311
 
99
- if (!response.ok) {
100
- throw new Error(`Failed to create repository: ${response.statusText}`);
312
+ const createRepoPath = baseRepoOptions
313
+ ? {
314
+ path: 'repos',
315
+ body: {
316
+ base_repo: baseRepoOptions,
317
+ },
318
+ }
319
+ : 'repos';
320
+
321
+ // Allow 409 so we can map it to a clearer error message
322
+ const resp = await this.api.post(createRepoPath, jwt, { allowedStatus: [409] });
323
+ if (resp.status === 409) {
324
+ throw new Error('Repository already exists');
101
325
  }
102
326
 
103
- return {
104
- id: repoId,
105
- getRemoteURL: async (urlOptions?: GetRemoteURLOptions): Promise<string> => {
106
- const url = new URL(`https://${this.options.name}.${STORAGE_BASE_URL}/${repoId}.git`);
107
- url.username = `t`;
108
- url.password = await this.generateJWT(repoId, urlOptions);
109
- return url.toString();
110
- },
111
- };
327
+ return new RepoImpl(repoId, this.options, this.generateJWT.bind(this));
112
328
  }
113
329
 
114
330
  /**
@@ -117,15 +333,19 @@ export class GitStorage {
117
333
  * @returns The found repository
118
334
  */
119
335
  async findOne(options: FindOneOptions): Promise<Repo | null> {
120
- return {
121
- id: options.id,
122
- getRemoteURL: async (urlOptions?: GetRemoteURLOptions): Promise<string> => {
123
- const url = new URL(`https://${this.options.name}.${STORAGE_BASE_URL}/${options.id}.git`);
124
- url.username = `t`;
125
- url.password = await this.generateJWT(options.id, urlOptions);
126
- return url.toString();
127
- },
128
- };
336
+ const jwt = await this.generateJWT(options.id, {
337
+ permissions: ['git:read'],
338
+ ttl: 1 * 60 * 60,
339
+ });
340
+
341
+ // Allow 404 to indicate "not found" without throwing
342
+ const resp = await this.api.get('repo', jwt, { allowedStatus: [404] });
343
+ if (resp.status === 404) {
344
+ return null;
345
+ }
346
+ // On 200, we could validate response, but RepoImpl only needs the repo URL/id
347
+ // const body = await resp.json(); // not required for now
348
+ return new RepoImpl(options.id, this.options, this.generateJWT.bind(this));
129
349
  }
130
350
 
131
351
  /**
package/src/types.ts CHANGED
@@ -2,11 +2,20 @@
2
2
  * Type definitions for Pierre Git Storage SDK
3
3
  */
4
4
 
5
- export interface GitStorageOptions {
5
+ export interface OverrideableGitStorageOptions {
6
+ apiBaseUrl?: string;
7
+ storageBaseUrl?: string;
8
+ apiVersion?: ValidAPIVersion;
9
+ }
10
+
11
+ export interface GitStorageOptions extends OverrideableGitStorageOptions {
6
12
  key: string;
7
13
  name: string;
14
+ defaultTTL?: number;
8
15
  }
9
16
 
17
+ export type ValidAPIVersion = 1;
18
+
10
19
  export interface GetRemoteURLOptions {
11
20
  permissions?: ('git:write' | 'git:read' | 'repo:write')[];
12
21
  ttl?: number;
@@ -15,17 +24,243 @@ export interface GetRemoteURLOptions {
15
24
  export interface Repo {
16
25
  id: string;
17
26
  getRemoteURL(options?: GetRemoteURLOptions): Promise<string>;
27
+ getFile(options: GetFileOptions): Promise<GetFileResponse>;
28
+ listFiles(options?: ListFilesOptions): Promise<ListFilesResponse>;
29
+ listBranches(options?: ListBranchesOptions): Promise<ListBranchesResponse>;
30
+ listCommits(options?: ListCommitsOptions): Promise<ListCommitsResponse>;
31
+ getBranchDiff(options: GetBranchDiffOptions): Promise<GetBranchDiffResponse>;
32
+ getCommitDiff(options: GetCommitDiffOptions): Promise<GetCommitDiffResponse>;
33
+ getCommit(options: GetCommitOptions): Promise<GetCommitResponse>;
34
+ }
35
+
36
+ export type ValidMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
37
+ type SimplePath = string;
38
+ type ComplexPath = {
39
+ path: string;
40
+ params?: Record<string, string>;
41
+ body?: Record<string, any>;
42
+ };
43
+ export type ValidPath = SimplePath | ComplexPath;
44
+
45
+ interface GitStorageInvocationOptions {
46
+ ttl?: number;
18
47
  }
19
48
 
20
49
  export interface FindOneOptions {
21
50
  id: string;
22
51
  }
23
52
 
24
- export interface CreateRepoOptions {
53
+ export type SupportedRepoProvider = 'github';
54
+
55
+ export interface BaseRepo {
56
+ /**
57
+ * @default github
58
+ */
59
+ provider?: SupportedRepoProvider;
60
+ owner: string;
61
+ name: string;
62
+ defaultBranch?: string;
63
+ }
64
+
65
+ export interface CreateRepoOptions extends GitStorageInvocationOptions {
25
66
  id?: string;
67
+ baseRepo?: BaseRepo;
26
68
  }
27
69
 
28
70
  export interface CreateRepoResponse {
29
71
  repo_id: string;
30
72
  url: string;
31
73
  }
74
+
75
+ // Get File API types
76
+ export interface GetFileOptions extends GitStorageInvocationOptions {
77
+ path: string;
78
+ ref?: string;
79
+ }
80
+
81
+ export interface GetFileResponse {
82
+ path: string;
83
+ ref: string;
84
+ content: string;
85
+ size: number;
86
+ is_binary: boolean;
87
+ }
88
+
89
+ export interface RepullOptions extends GitStorageInvocationOptions {
90
+ ref?: string;
91
+ }
92
+
93
+ // List Files API types
94
+ export interface ListFilesOptions extends GitStorageInvocationOptions {
95
+ ref?: string;
96
+ }
97
+
98
+ export interface ListFilesResponse {
99
+ paths: string[];
100
+ ref: string;
101
+ }
102
+
103
+ // List Branches API types
104
+ export interface ListBranchesOptions extends GitStorageInvocationOptions {
105
+ cursor?: string;
106
+ limit?: number;
107
+ }
108
+
109
+ export interface BranchInfo {
110
+ cursor: string;
111
+ name: string;
112
+ head_sha: string;
113
+ created_at: string;
114
+ }
115
+
116
+ export interface ListBranchesResponse {
117
+ branches: BranchInfo[];
118
+ next_cursor?: string;
119
+ has_more: boolean;
120
+ }
121
+
122
+ // List Commits API types
123
+ export interface ListCommitsOptions extends GitStorageInvocationOptions {
124
+ branch?: string;
125
+ cursor?: string;
126
+ limit?: number;
127
+ }
128
+
129
+ export interface CommitInfo {
130
+ sha: string;
131
+ message: string;
132
+ author_name: string;
133
+ author_email: string;
134
+ committer_name: string;
135
+ committer_email: string;
136
+ date: string;
137
+ }
138
+
139
+ export interface ListCommitsResponse {
140
+ commits: CommitInfo[];
141
+ next_cursor?: string;
142
+ has_more: boolean;
143
+ }
144
+
145
+ // Branch Diff API types
146
+ export interface GetBranchDiffOptions extends GitStorageInvocationOptions {
147
+ branch: string;
148
+ base?: string;
149
+ }
150
+
151
+ export interface GetBranchDiffResponse {
152
+ branch: string;
153
+ base: string;
154
+ stats: DiffStats;
155
+ files: FileDiff[];
156
+ filtered_files: FilteredFile[];
157
+ }
158
+
159
+ // Commit Diff API types
160
+ export interface GetCommitDiffOptions extends GitStorageInvocationOptions {
161
+ sha: string;
162
+ }
163
+
164
+ export interface GetCommitDiffResponse {
165
+ sha: string;
166
+ stats: DiffStats;
167
+ files: FileDiff[];
168
+ filtered_files: FilteredFile[];
169
+ }
170
+
171
+ // Shared diff types
172
+ export interface DiffStats {
173
+ files: number;
174
+ additions: number;
175
+ deletions: number;
176
+ changes: number;
177
+ }
178
+
179
+ export interface FileDiff {
180
+ path: string;
181
+ state: string;
182
+ old_path?: string;
183
+ bytes: number;
184
+ is_eof: boolean;
185
+ diff: string;
186
+ }
187
+
188
+ export interface FilteredFile {
189
+ path: string;
190
+ state: string;
191
+ old_path?: string;
192
+ bytes: number;
193
+ is_eof: boolean;
194
+ }
195
+
196
+ // Get Commit API types
197
+ export interface GetCommitOptions extends GitStorageInvocationOptions {
198
+ sha: string;
199
+ }
200
+
201
+ export interface GetCommitResponse {
202
+ sha: string;
203
+ message: string;
204
+ author: {
205
+ name: string;
206
+ email: string;
207
+ };
208
+ committer: {
209
+ name: string;
210
+ email: string;
211
+ };
212
+ date: string;
213
+ stats: {
214
+ additions: number;
215
+ deletions: number;
216
+ total: number;
217
+ };
218
+ }
219
+
220
+ // Webhook types
221
+ export interface WebhookValidationOptions {
222
+ /**
223
+ * Maximum age of webhook in seconds (default: 300 seconds / 5 minutes)
224
+ * Set to 0 to disable timestamp validation
225
+ */
226
+ maxAgeSeconds?: number;
227
+ }
228
+
229
+ export interface WebhookValidationResult {
230
+ /**
231
+ * Whether the webhook signature and timestamp are valid
232
+ */
233
+ valid: boolean;
234
+ /**
235
+ * Error message if validation failed
236
+ */
237
+ error?: string;
238
+ /**
239
+ * The parsed webhook event type (e.g., "push")
240
+ */
241
+ eventType?: string;
242
+ /**
243
+ * The timestamp from the signature (Unix seconds)
244
+ */
245
+ timestamp?: number;
246
+ }
247
+
248
+ // Webhook event payloads
249
+ export interface WebhookPushEvent {
250
+ repository: {
251
+ id: string;
252
+ url: string;
253
+ };
254
+ ref: string;
255
+ before: string;
256
+ after: string;
257
+ customer_id: string;
258
+ pushed_at: string; // RFC3339 timestamp
259
+ }
260
+
261
+ export type WebhookEventPayload = WebhookPushEvent;
262
+
263
+ export interface ParsedWebhookSignature {
264
+ timestamp: string;
265
+ signature: string;
266
+ }