@pierre/storage 0.0.5 → 0.0.7

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
@@ -1,18 +1,267 @@
1
1
  'use strict';
2
2
 
3
3
  var jose = require('jose');
4
+ var snakecaseKeys = require('snakecase-keys');
5
+
6
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
+
8
+ var snakecaseKeys__default = /*#__PURE__*/_interopDefault(snakecaseKeys);
9
+
10
+ // src/index.ts
11
+
12
+ // src/fetch.ts
13
+ var ApiFetcher = class {
14
+ constructor(API_BASE_URL2, version) {
15
+ this.API_BASE_URL = API_BASE_URL2;
16
+ this.version = version;
17
+ console.log("api fetcher created", API_BASE_URL2, version);
18
+ }
19
+ getBaseUrl() {
20
+ return `${this.API_BASE_URL}/api/v${this.version}`;
21
+ }
22
+ getRequestUrl(path) {
23
+ if (typeof path === "string") {
24
+ return `${this.getBaseUrl()}/${path}`;
25
+ } else if (path.params) {
26
+ const paramStr = new URLSearchParams(path.params).toString();
27
+ return `${this.getBaseUrl()}/${path.path}${paramStr ? `?${paramStr}` : ""}`;
28
+ } else {
29
+ return `${this.getBaseUrl()}/${path.path}`;
30
+ }
31
+ }
32
+ async fetch(path, method, jwt, options) {
33
+ const requestUrl = this.getRequestUrl(path);
34
+ const requestOptions = {
35
+ method,
36
+ headers: {
37
+ Authorization: `Bearer ${jwt}`,
38
+ "Content-Type": "application/json"
39
+ }
40
+ };
41
+ if (method !== "GET" && typeof path !== "string" && path.body) {
42
+ requestOptions.body = JSON.stringify(path.body);
43
+ }
44
+ const response = await fetch(requestUrl, requestOptions);
45
+ if (!response.ok) {
46
+ const allowed = options?.allowedStatus ?? [];
47
+ if (!allowed.includes(response.status)) {
48
+ throw new Error(`Failed to fetch ${method} ${requestUrl}: ${response.statusText}`);
49
+ }
50
+ }
51
+ return response;
52
+ }
53
+ async get(path, jwt, options) {
54
+ return this.fetch(path, "GET", jwt, options);
55
+ }
56
+ async post(path, jwt, options) {
57
+ return this.fetch(path, "POST", jwt, options);
58
+ }
59
+ async put(path, jwt, options) {
60
+ return this.fetch(path, "PUT", jwt, options);
61
+ }
62
+ async delete(path, jwt, options) {
63
+ return this.fetch(path, "DELETE", jwt, options);
64
+ }
65
+ };
66
+
67
+ // src/util.ts
68
+ function timingSafeEqual(a, b) {
69
+ const bufferA = typeof a === "string" ? new TextEncoder().encode(a) : a;
70
+ const bufferB = typeof b === "string" ? new TextEncoder().encode(b) : b;
71
+ if (bufferA.length !== bufferB.length) return false;
72
+ let result = 0;
73
+ for (let i = 0; i < bufferA.length; i++) {
74
+ result |= bufferA[i] ^ bufferB[i];
75
+ }
76
+ return result === 0;
77
+ }
78
+ async function getEnvironmentCrypto() {
79
+ if (!globalThis.crypto) {
80
+ const { webcrypto } = await import('crypto');
81
+ return webcrypto;
82
+ }
83
+ return globalThis.crypto;
84
+ }
85
+ async function createHmac(algorithm, secret, data) {
86
+ if (!secret || secret.length === 0) {
87
+ throw new Error("Secret is required");
88
+ }
89
+ const crypto2 = await getEnvironmentCrypto();
90
+ const encoder = new TextEncoder();
91
+ const key = await crypto2.subtle.importKey(
92
+ "raw",
93
+ encoder.encode(secret),
94
+ { name: "HMAC", hash: "SHA-256" },
95
+ false,
96
+ ["sign"]
97
+ );
98
+ const signature = await crypto2.subtle.sign("HMAC", key, encoder.encode(data));
99
+ return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
100
+ }
101
+
102
+ // src/webhook.ts
103
+ var DEFAULT_MAX_AGE_SECONDS = 300;
104
+ function parseSignatureHeader(header) {
105
+ if (!header || typeof header !== "string") {
106
+ return null;
107
+ }
108
+ let timestamp = "";
109
+ let signature = "";
110
+ const elements = header.split(",");
111
+ for (const element of elements) {
112
+ const trimmedElement = element.trim();
113
+ const parts = trimmedElement.split("=", 2);
114
+ if (parts.length !== 2) {
115
+ continue;
116
+ }
117
+ const [key, value] = parts;
118
+ switch (key) {
119
+ case "t":
120
+ timestamp = value;
121
+ break;
122
+ case "sha256":
123
+ signature = value;
124
+ break;
125
+ }
126
+ }
127
+ if (!timestamp || !signature) {
128
+ return null;
129
+ }
130
+ return { timestamp, signature };
131
+ }
132
+ async function validateWebhookSignature(payload, signatureHeader, secret, options = {}) {
133
+ if (!secret || secret.length === 0) {
134
+ return {
135
+ valid: false,
136
+ error: "Empty secret is not allowed"
137
+ };
138
+ }
139
+ const parsed = parseSignatureHeader(signatureHeader);
140
+ if (!parsed) {
141
+ return {
142
+ valid: false,
143
+ error: "Invalid signature header format"
144
+ };
145
+ }
146
+ const timestamp = Number.parseInt(parsed.timestamp, 10);
147
+ if (isNaN(timestamp)) {
148
+ return {
149
+ valid: false,
150
+ error: "Invalid timestamp in signature"
151
+ };
152
+ }
153
+ const maxAge = options.maxAgeSeconds ?? DEFAULT_MAX_AGE_SECONDS;
154
+ if (maxAge > 0) {
155
+ const now = Math.floor(Date.now() / 1e3);
156
+ const age = now - timestamp;
157
+ if (age > maxAge) {
158
+ return {
159
+ valid: false,
160
+ error: `Webhook timestamp too old (${age} seconds)`,
161
+ timestamp
162
+ };
163
+ }
164
+ if (age < -60) {
165
+ return {
166
+ valid: false,
167
+ error: "Webhook timestamp is in the future",
168
+ timestamp
169
+ };
170
+ }
171
+ }
172
+ const payloadStr = typeof payload === "string" ? payload : payload.toString("utf8");
173
+ const signedData = `${parsed.timestamp}.${payloadStr}`;
174
+ const expectedSignature = await createHmac("sha256", secret, signedData);
175
+ const expectedBuffer = Buffer.from(expectedSignature);
176
+ const actualBuffer = Buffer.from(parsed.signature);
177
+ if (expectedBuffer.length !== actualBuffer.length) {
178
+ return {
179
+ valid: false,
180
+ error: "Invalid signature",
181
+ timestamp
182
+ };
183
+ }
184
+ const signaturesMatch = timingSafeEqual(expectedBuffer, actualBuffer);
185
+ if (!signaturesMatch) {
186
+ return {
187
+ valid: false,
188
+ error: "Invalid signature",
189
+ timestamp
190
+ };
191
+ }
192
+ return {
193
+ valid: true,
194
+ timestamp
195
+ };
196
+ }
197
+ async function validateWebhook(payload, headers, secret, options = {}) {
198
+ const signatureHeader = headers["x-pierre-signature"] || headers["X-Pierre-Signature"];
199
+ if (!signatureHeader || Array.isArray(signatureHeader)) {
200
+ return {
201
+ valid: false,
202
+ error: "Missing or invalid X-Pierre-Signature header"
203
+ };
204
+ }
205
+ const eventType = headers["x-pierre-event"] || headers["X-Pierre-Event"];
206
+ if (!eventType || Array.isArray(eventType)) {
207
+ return {
208
+ valid: false,
209
+ error: "Missing or invalid X-Pierre-Event header"
210
+ };
211
+ }
212
+ const validationResult = await validateWebhookSignature(
213
+ payload,
214
+ signatureHeader,
215
+ secret,
216
+ options
217
+ );
218
+ if (!validationResult.valid) {
219
+ return validationResult;
220
+ }
221
+ const payloadStr = typeof payload === "string" ? payload : payload.toString("utf8");
222
+ let parsedPayload;
223
+ try {
224
+ parsedPayload = JSON.parse(payloadStr);
225
+ } catch {
226
+ return {
227
+ valid: false,
228
+ error: "Invalid JSON payload",
229
+ timestamp: validationResult.timestamp
230
+ };
231
+ }
232
+ return {
233
+ valid: true,
234
+ eventType,
235
+ timestamp: validationResult.timestamp,
236
+ payload: parsedPayload
237
+ };
238
+ }
4
239
 
5
240
  // src/index.ts
6
241
  var API_BASE_URL = "https://api.git.storage";
7
242
  var STORAGE_BASE_URL = "git.storage";
243
+ var API_VERSION = 1;
244
+ var apiInstanceMap = /* @__PURE__ */ new Map();
245
+ function getApiInstance(baseUrl, version) {
246
+ if (!apiInstanceMap.has(`${baseUrl}--${version}`)) {
247
+ apiInstanceMap.set(`${baseUrl}--${version}`, new ApiFetcher(baseUrl, version));
248
+ }
249
+ return apiInstanceMap.get(`${baseUrl}--${version}`);
250
+ }
8
251
  var RepoImpl = class {
9
252
  constructor(id, options, generateJWT) {
10
253
  this.id = id;
11
254
  this.options = options;
12
255
  this.generateJWT = generateJWT;
256
+ this.api = getApiInstance(
257
+ this.options.apiBaseUrl ?? API_BASE_URL,
258
+ this.options.apiVersion ?? API_VERSION
259
+ );
13
260
  }
261
+ api;
14
262
  async getRemoteURL(urlOptions) {
15
- const url = new URL(`https://${this.options.name}.${STORAGE_BASE_URL}/${this.id}.git`);
263
+ const storageBaseUrl = this.options.storageBaseUrl ?? STORAGE_BASE_URL;
264
+ const url = new URL(`https://${this.options.name}.${storageBaseUrl}/${this.id}.git`);
16
265
  url.username = `t`;
17
266
  url.password = await this.generateJWT(this.id, urlOptions);
18
267
  return url.toString();
@@ -20,161 +269,130 @@ var RepoImpl = class {
20
269
  async getFile(options) {
21
270
  const jwt = await this.generateJWT(this.id, {
22
271
  permissions: ["git:read"],
23
- ttl: 1 * 60 * 60
272
+ ttl: options?.ttl ?? 1 * 60 * 60
24
273
  // 1hr in seconds
25
274
  });
26
- const url = new URL(`${API_BASE_URL}/api/v1/repos/file`);
27
- url.searchParams.set("path", options.path);
275
+ const params = {
276
+ path: options.path
277
+ };
28
278
  if (options.ref) {
29
- url.searchParams.set("ref", options.ref);
30
- }
31
- const response = await fetch(url.toString(), {
32
- method: "GET",
33
- headers: {
34
- Authorization: `Bearer ${jwt}`
35
- }
36
- });
37
- if (!response.ok) {
38
- throw new Error(`Failed to get file: ${response.statusText}`);
279
+ params.ref = options.ref;
39
280
  }
281
+ const response = await this.api.get({ path: "repos/file", params }, jwt);
40
282
  return await response.json();
41
283
  }
42
284
  async listFiles(options) {
43
285
  const jwt = await this.generateJWT(this.id, {
44
286
  permissions: ["git:read"],
45
- ttl: 1 * 60 * 60
287
+ ttl: options?.ttl ?? 1 * 60 * 60
46
288
  // 1hr in seconds
47
289
  });
48
- const url = new URL(`${API_BASE_URL}/api/v1/repos/files`);
49
- if (options?.ref) {
50
- url.searchParams.set("ref", options.ref);
51
- }
52
- const response = await fetch(url.toString(), {
53
- method: "GET",
54
- headers: {
55
- Authorization: `Bearer ${jwt}`
56
- }
57
- });
58
- if (!response.ok) {
59
- throw new Error(`Failed to list files: ${response.statusText}`);
60
- }
290
+ const params = options?.ref ? { ref: options.ref } : void 0;
291
+ const response = await this.api.get({ path: "repos/files", params }, jwt);
61
292
  return await response.json();
62
293
  }
63
294
  async listBranches(options) {
64
295
  const jwt = await this.generateJWT(this.id, {
65
296
  permissions: ["git:read"],
66
- ttl: 1 * 60 * 60
297
+ ttl: options?.ttl ?? 1 * 60 * 60
67
298
  // 1hr in seconds
68
299
  });
69
- const url = new URL(`${API_BASE_URL}/api/v1/repos/branches`);
70
- if (options?.cursor) {
71
- url.searchParams.set("cursor", options.cursor);
72
- }
73
- if (options?.limit) {
74
- url.searchParams.set("limit", options.limit.toString());
75
- }
76
- const response = await fetch(url.toString(), {
77
- method: "GET",
78
- headers: {
79
- Authorization: `Bearer ${jwt}`
300
+ let params;
301
+ if (options?.cursor || !options?.limit) {
302
+ params = {};
303
+ if (options?.cursor) {
304
+ params.cursor = options.cursor;
305
+ }
306
+ if (typeof options?.limit == "number") {
307
+ params.limit = options.limit.toString();
80
308
  }
81
- });
82
- if (!response.ok) {
83
- throw new Error(`Failed to list branches: ${response.statusText}`);
84
309
  }
310
+ const response = await this.api.get({ path: "repos/branches", params }, jwt);
85
311
  return await response.json();
86
312
  }
87
313
  async listCommits(options) {
88
314
  const jwt = await this.generateJWT(this.id, {
89
315
  permissions: ["git:read"],
90
- ttl: 1 * 60 * 60
316
+ ttl: options?.ttl ?? 1 * 60 * 60
91
317
  // 1hr in seconds
92
318
  });
93
- const url = new URL(`${API_BASE_URL}/api/v1/repos/commits`);
94
- if (options?.branch) {
95
- url.searchParams.set("branch", options.branch);
96
- }
97
- if (options?.cursor) {
98
- url.searchParams.set("cursor", options.cursor);
99
- }
100
- if (options?.limit) {
101
- url.searchParams.set("limit", options.limit.toString());
102
- }
103
- const response = await fetch(url.toString(), {
104
- method: "GET",
105
- headers: {
106
- Authorization: `Bearer ${jwt}`
319
+ let params;
320
+ if (options?.branch || options?.cursor || options?.limit) {
321
+ params = {};
322
+ if (options?.branch) {
323
+ params.branch = options.branch;
324
+ }
325
+ if (options?.cursor) {
326
+ params.cursor = options.cursor;
327
+ }
328
+ if (typeof options?.limit == "number") {
329
+ params.limit = options.limit.toString();
107
330
  }
108
- });
109
- if (!response.ok) {
110
- throw new Error(`Failed to list commits: ${response.statusText}`);
111
331
  }
332
+ const response = await this.api.get({ path: "repos/commits", params }, jwt);
112
333
  return await response.json();
113
334
  }
114
335
  async getBranchDiff(options) {
115
336
  const jwt = await this.generateJWT(this.id, {
116
337
  permissions: ["git:read"],
117
- ttl: 1 * 60 * 60
338
+ ttl: options?.ttl ?? 1 * 60 * 60
118
339
  // 1hr in seconds
119
340
  });
120
- const url = new URL(`${API_BASE_URL}/api/v1/repos/branches/diff`);
121
- url.searchParams.set("branch", options.branch);
341
+ const params = {
342
+ branch: options.branch
343
+ };
122
344
  if (options.base) {
123
- url.searchParams.set("base", options.base);
124
- }
125
- const response = await fetch(url.toString(), {
126
- method: "GET",
127
- headers: {
128
- Authorization: `Bearer ${jwt}`
129
- }
130
- });
131
- if (!response.ok) {
132
- throw new Error(`Failed to get branch diff: ${response.statusText}`);
345
+ params.base = options.base;
133
346
  }
347
+ const response = await this.api.get({ path: "repos/branches/diff", params }, jwt);
134
348
  return await response.json();
135
349
  }
136
350
  async getCommitDiff(options) {
137
351
  const jwt = await this.generateJWT(this.id, {
138
352
  permissions: ["git:read"],
139
- ttl: 1 * 60 * 60
353
+ ttl: options?.ttl ?? 1 * 60 * 60
140
354
  // 1hr in seconds
141
355
  });
142
- const url = new URL(`${API_BASE_URL}/api/v1/repos/diff`);
143
- url.searchParams.set("sha", options.sha);
144
- const response = await fetch(url.toString(), {
145
- method: "GET",
146
- headers: {
147
- Authorization: `Bearer ${jwt}`
148
- }
149
- });
150
- if (!response.ok) {
151
- throw new Error(`Failed to get commit diff: ${response.statusText}`);
152
- }
356
+ const params = {
357
+ sha: options.sha
358
+ };
359
+ const response = await this.api.get({ path: "repos/diff", params }, jwt);
153
360
  return await response.json();
154
361
  }
155
362
  async getCommit(options) {
156
363
  const jwt = await this.generateJWT(this.id, {
157
364
  permissions: ["git:read"],
158
- ttl: 1 * 60 * 60
365
+ ttl: options?.ttl ?? 1 * 60 * 60
159
366
  // 1hr in seconds
160
367
  });
161
- const url = new URL(`${API_BASE_URL}/commit`);
162
- url.searchParams.set("repo", this.id);
163
- url.searchParams.set("sha", options.sha);
164
- const response = await fetch(url.toString(), {
165
- method: "GET",
166
- headers: {
167
- Authorization: `Bearer ${jwt}`
168
- }
368
+ const params = {
369
+ repo: this.id,
370
+ sha: options.sha
371
+ };
372
+ const response = await this.api.get({ path: "commit", params }, jwt);
373
+ return await response.json();
374
+ }
375
+ async repull(options) {
376
+ const jwt = await this.generateJWT(this.id, {
377
+ permissions: ["git:write"],
378
+ ttl: options?.ttl ?? 1 * 60 * 60
379
+ // 1hr in seconds
169
380
  });
170
- if (!response.ok) {
171
- throw new Error(`Failed to get commit: ${response.statusText}`);
381
+ const body = {};
382
+ if (options.ref) {
383
+ body.ref = options.ref;
172
384
  }
173
- return await response.json();
385
+ const response = await this.api.post({ path: "repos/repull", body }, jwt);
386
+ if (response.status !== 202) {
387
+ throw new Error(`Repull failed: ${response.status} ${await response.text()}`);
388
+ }
389
+ return;
174
390
  }
175
391
  };
176
- var GitStorage = class {
392
+ var GitStorage = class _GitStorage {
393
+ static overrides = {};
177
394
  options;
395
+ api;
178
396
  constructor(options) {
179
397
  if (!options || options.name === void 0 || options.key === void 0 || options.name === null || options.key === null) {
180
398
  throw new Error(
@@ -187,11 +405,21 @@ var GitStorage = class {
187
405
  if (typeof options.key !== "string" || options.key.trim() === "") {
188
406
  throw new Error("GitStorage key must be a non-empty string.");
189
407
  }
408
+ const resolvedApiBaseUrl = options.apiBaseUrl ?? _GitStorage.overrides.apiBaseUrl ?? API_BASE_URL;
409
+ const resolvedApiVersion = options.apiVersion ?? _GitStorage.overrides.apiVersion ?? API_VERSION;
410
+ const resolvedStorageBaseUrl = options.storageBaseUrl ?? _GitStorage.overrides.storageBaseUrl ?? STORAGE_BASE_URL;
411
+ this.api = getApiInstance(resolvedApiBaseUrl, resolvedApiVersion);
190
412
  this.options = {
191
413
  key: options.key,
192
- name: options.name
414
+ name: options.name,
415
+ apiBaseUrl: resolvedApiBaseUrl,
416
+ apiVersion: resolvedApiVersion,
417
+ storageBaseUrl: resolvedStorageBaseUrl
193
418
  };
194
419
  }
420
+ static override(options) {
421
+ this.overrides = Object.assign({}, this.overrides, options);
422
+ }
195
423
  /**
196
424
  * Create a new repository
197
425
  * @returns The created repository
@@ -200,17 +428,24 @@ var GitStorage = class {
200
428
  const repoId = options?.id || crypto.randomUUID();
201
429
  const jwt = await this.generateJWT(repoId, {
202
430
  permissions: ["repo:write"],
203
- ttl: 1 * 60 * 60
431
+ ttl: options?.ttl ?? 1 * 60 * 60
204
432
  // 1hr in seconds
205
433
  });
206
- const response = await fetch(`${API_BASE_URL}/api/v1/repos`, {
207
- method: "POST",
208
- headers: {
209
- Authorization: `Bearer ${jwt}`
434
+ const baseRepoOptions = options?.baseRepo ? {
435
+ provider: "github",
436
+ ...snakecaseKeys__default.default(options.baseRepo)
437
+ } : null;
438
+ const defaultBranch = options?.defaultBranch ?? "main";
439
+ const createRepoPath = baseRepoOptions || defaultBranch ? {
440
+ path: "repos",
441
+ body: {
442
+ ...baseRepoOptions && { base_repo: baseRepoOptions },
443
+ default_branch: defaultBranch
210
444
  }
211
- });
212
- if (!response.ok) {
213
- throw new Error(`Failed to create repository: ${response.statusText}`);
445
+ } : "repos";
446
+ const resp = await this.api.post(createRepoPath, jwt, { allowedStatus: [409] });
447
+ if (resp.status === 409) {
448
+ throw new Error("Repository already exists");
214
449
  }
215
450
  return new RepoImpl(repoId, this.options, this.generateJWT.bind(this));
216
451
  }
@@ -220,6 +455,14 @@ var GitStorage = class {
220
455
  * @returns The found repository
221
456
  */
222
457
  async findOne(options) {
458
+ const jwt = await this.generateJWT(options.id, {
459
+ permissions: ["git:read"],
460
+ ttl: 1 * 60 * 60
461
+ });
462
+ const resp = await this.api.get("repo", jwt, { allowedStatus: [404] });
463
+ if (resp.status === 404) {
464
+ return null;
465
+ }
223
466
  return new RepoImpl(options.id, this.options, this.generateJWT.bind(this));
224
467
  }
225
468
  /**
@@ -256,5 +499,8 @@ function createClient(options) {
256
499
 
257
500
  exports.GitStorage = GitStorage;
258
501
  exports.createClient = createClient;
502
+ exports.parseSignatureHeader = parseSignatureHeader;
503
+ exports.validateWebhook = validateWebhook;
504
+ exports.validateWebhookSignature = validateWebhookSignature;
259
505
  //# sourceMappingURL=index.cjs.map
260
506
  //# sourceMappingURL=index.cjs.map