@pierre/storage 1.2.2 → 1.4.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/package.json CHANGED
@@ -1,45 +1,45 @@
1
1
  {
2
- "name": "@pierre/storage",
3
- "version": "1.2.2",
4
- "description": "Pierre Git Storage SDK",
5
- "repository": {
6
- "type": "git",
7
- "url": "https://github.com/pierrecomputer/sdk"
8
- },
9
- "license": "MIT",
10
- "type": "module",
11
- "main": "./dist/index.cjs",
12
- "module": "./dist/index.js",
13
- "types": "./dist/index.d.ts",
14
- "exports": {
15
- ".": {
16
- "types": "./dist/index.d.ts",
17
- "import": "./dist/index.js",
18
- "require": "./dist/index.cjs",
19
- "default": "./dist/index.js"
20
- }
21
- },
22
- "files": [
23
- "dist",
24
- "src"
25
- ],
26
- "scripts": {
27
- "build": "tsup",
28
- "dev": "tsup --watch",
29
- "prepublishOnly": "pnpm build"
30
- },
31
- "dependencies": {
32
- "jose": "^5.10.0",
33
- "snakecase-keys": "^9.0.2",
34
- "zod": "^3.23.8"
35
- },
36
- "devDependencies": {
37
- "@types/node": "^22.0.0",
38
- "tsup": "8.5.0",
39
- "typescript": "5.8.3",
40
- "vitest": "3.2.4"
41
- },
42
- "publishConfig": {
43
- "access": "public"
44
- }
2
+ "name": "@pierre/storage",
3
+ "version": "1.4.0",
4
+ "description": "Pierre Git Storage SDK",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/pierrecomputer/sdk"
8
+ },
9
+ "license": "MIT",
10
+ "type": "module",
11
+ "main": "./dist/index.cjs",
12
+ "module": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js",
18
+ "require": "./dist/index.cjs",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "src"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "dev": "tsup --watch",
29
+ "prepublishOnly": "pnpm build"
30
+ },
31
+ "dependencies": {
32
+ "jose": "^5.10.0",
33
+ "snakecase-keys": "^9.0.2",
34
+ "zod": "^3.23.8"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^22.0.0",
38
+ "tsup": "8.5.0",
39
+ "typescript": "5.8.3",
40
+ "vitest": "3.2.4"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ }
45
45
  }
package/src/index.ts CHANGED
@@ -19,6 +19,8 @@ import {
19
19
  branchDiffResponseSchema,
20
20
  commitDiffResponseSchema,
21
21
  createBranchResponseSchema,
22
+ createTagResponseSchema,
23
+ deleteTagResponseSchema,
22
24
  errorEnvelopeSchema,
23
25
  grepResponseSchema,
24
26
  listBranchesResponseSchema,
@@ -26,6 +28,7 @@ import {
26
28
  listFilesResponseSchema,
27
29
  listFilesWithMetadataResponseSchema,
28
30
  listReposResponseSchema,
31
+ listTagsResponseSchema,
29
32
  noteReadResponseSchema,
30
33
  noteWriteResponseSchema,
31
34
  restoreCommitAckSchema,
@@ -44,8 +47,16 @@ import type {
44
47
  CreateBranchResult,
45
48
  CreateCommitFromDiffOptions,
46
49
  CreateCommitOptions,
50
+ CreateTagOptions,
51
+ CreateTagResponse,
52
+ CreateTagResult,
53
+ CreateGitCredentialOptions,
47
54
  CreateNoteOptions,
48
55
  CreateRepoOptions,
56
+ DeleteTagOptions,
57
+ DeleteTagResponse,
58
+ DeleteTagResult,
59
+ DeleteGitCredentialOptions,
49
60
  DeleteNoteOptions,
50
61
  DeleteRepoOptions,
51
62
  DeleteRepoResult,
@@ -54,6 +65,7 @@ import type {
54
65
  FileDiff,
55
66
  FilteredFile,
56
67
  FindOneOptions,
68
+ GenericGitBaseRepo,
57
69
  GetBranchDiffOptions,
58
70
  GetBranchDiffResponse,
59
71
  GetBranchDiffResult,
@@ -64,6 +76,8 @@ import type {
64
76
  GetNoteOptions,
65
77
  GetNoteResult,
66
78
  GetRemoteURLOptions,
79
+ GitCredential,
80
+ GitHubBaseRepo,
67
81
  GitStorageOptions,
68
82
  GrepFileMatch,
69
83
  GrepLine,
@@ -83,6 +97,9 @@ import type {
83
97
  ListReposOptions,
84
98
  ListReposResponse,
85
99
  ListReposResult,
100
+ ListTagsOptions,
101
+ ListTagsResponse,
102
+ ListTagsResult,
86
103
  NoteWriteResult,
87
104
  PullUpstreamOptions,
88
105
  RawBranchInfo,
@@ -91,11 +108,14 @@ import type {
91
108
  RawFileWithMetadata,
92
109
  RawFileDiff,
93
110
  RawFilteredFile,
111
+ RawTagInfo,
94
112
  RefUpdate,
95
113
  RepoOptions,
96
114
  Repo,
97
115
  RestoreCommitOptions,
98
116
  RestoreCommitResult,
117
+ TagInfo,
118
+ UpdateGitCredentialOptions,
99
119
  ValidAPIVersion,
100
120
  } from './types';
101
121
 
@@ -438,6 +458,37 @@ function transformCreateBranchResult(
438
458
  };
439
459
  }
440
460
 
461
+ function transformTagInfo(raw: RawTagInfo): TagInfo {
462
+ return {
463
+ cursor: raw.cursor,
464
+ name: raw.name,
465
+ sha: raw.sha,
466
+ };
467
+ }
468
+
469
+ function transformListTagsResult(raw: ListTagsResponse): ListTagsResult {
470
+ return {
471
+ tags: raw.tags.map(transformTagInfo),
472
+ nextCursor: raw.next_cursor ?? undefined,
473
+ hasMore: raw.has_more,
474
+ };
475
+ }
476
+
477
+ function transformCreateTagResult(raw: CreateTagResponse): CreateTagResult {
478
+ return {
479
+ name: raw.name,
480
+ sha: raw.sha,
481
+ message: raw.message,
482
+ };
483
+ }
484
+
485
+ function transformDeleteTagResult(raw: DeleteTagResponse): DeleteTagResult {
486
+ return {
487
+ name: raw.name,
488
+ message: raw.message,
489
+ };
490
+ }
491
+
441
492
  function transformListReposResult(raw: ListReposResponse): ListReposResult {
442
493
  return {
443
494
  repos: raw.repos.map((repo) => ({
@@ -793,6 +844,36 @@ class RepoImpl implements Repo {
793
844
  });
794
845
  }
795
846
 
847
+ async listTags(options?: ListTagsOptions): Promise<ListTagsResult> {
848
+ const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
849
+ const jwt = await this.generateJWT(this.id, {
850
+ permissions: ['git:read'],
851
+ ttl,
852
+ });
853
+
854
+ const cursor = options?.cursor;
855
+ const limit = options?.limit;
856
+
857
+ let params: Record<string, string> | undefined;
858
+
859
+ if (typeof cursor === 'string' || typeof limit === 'number') {
860
+ params = {};
861
+ if (typeof cursor === 'string') {
862
+ params.cursor = cursor;
863
+ }
864
+ if (typeof limit === 'number') {
865
+ params.limit = limit.toString();
866
+ }
867
+ }
868
+
869
+ const response = await this.api.get({ path: 'repos/tags', params }, jwt);
870
+ const raw = listTagsResponseSchema.parse(await response.json());
871
+ return transformListTagsResult({
872
+ ...raw,
873
+ next_cursor: raw.next_cursor ?? undefined,
874
+ });
875
+ }
876
+
796
877
  async listCommits(options?: ListCommitsOptions): Promise<ListCommitsResult> {
797
878
  const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
798
879
  const jwt = await this.generateJWT(this.id, {
@@ -1209,6 +1290,57 @@ class RepoImpl implements Repo {
1209
1290
  return transformCreateBranchResult(raw);
1210
1291
  }
1211
1292
 
1293
+ async createTag(options: CreateTagOptions): Promise<CreateTagResult> {
1294
+ const name = options?.name?.trim();
1295
+ if (!name) {
1296
+ throw new Error('createTag name is required');
1297
+ }
1298
+ if (name.startsWith('refs/')) {
1299
+ throw new Error('createTag name must not start with refs/');
1300
+ }
1301
+
1302
+ const target = options?.target?.trim();
1303
+ if (!target) {
1304
+ throw new Error('createTag target is required');
1305
+ }
1306
+
1307
+ const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
1308
+ const jwt = await this.generateJWT(this.id, {
1309
+ permissions: ['git:write'],
1310
+ ttl,
1311
+ });
1312
+
1313
+ const response = await this.api.post(
1314
+ { path: 'repos/tags', body: { name, target } },
1315
+ jwt
1316
+ );
1317
+ const raw = createTagResponseSchema.parse(await response.json());
1318
+ return transformCreateTagResult(raw);
1319
+ }
1320
+
1321
+ async deleteTag(options: DeleteTagOptions): Promise<DeleteTagResult> {
1322
+ const name = options?.name?.trim();
1323
+ if (!name) {
1324
+ throw new Error('deleteTag name is required');
1325
+ }
1326
+ if (name.startsWith('refs/')) {
1327
+ throw new Error('deleteTag name must not start with refs/');
1328
+ }
1329
+
1330
+ const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
1331
+ const jwt = await this.generateJWT(this.id, {
1332
+ permissions: ['git:read', 'git:write'],
1333
+ ttl,
1334
+ });
1335
+
1336
+ const response = await this.api.delete(
1337
+ { path: 'repos/tags', body: { name } },
1338
+ jwt
1339
+ );
1340
+ const raw = deleteTagResponseSchema.parse(await response.json());
1341
+ return transformDeleteTagResult(raw);
1342
+ }
1343
+
1212
1344
  async restoreCommit(
1213
1345
  options: RestoreCommitOptions
1214
1346
  ): Promise<RestoreCommitResult> {
@@ -1437,11 +1569,16 @@ export class GitStorage {
1437
1569
  ...(baseRepo.sha ? { sha: baseRepo.sha } : {}),
1438
1570
  };
1439
1571
  } else {
1572
+ // Sync base repo: GitHub or generic git provider (gitlab, bitbucket, etc.)
1573
+ const syncRepo = baseRepo as GitHubBaseRepo | GenericGitBaseRepo;
1574
+ const { provider: _p, ...restSnakecased } = snakecaseKeys(
1575
+ baseRepo as unknown as Record<string, unknown>
1576
+ ) as Record<string, unknown>;
1440
1577
  baseRepoOptions = {
1441
- provider: 'github',
1442
- ...snakecaseKeys(baseRepo as unknown as Record<string, unknown>),
1578
+ provider: syncRepo.provider ?? 'github',
1579
+ ...restSnakecased,
1443
1580
  };
1444
- resolvedDefaultBranch = baseRepo.defaultBranch;
1581
+ resolvedDefaultBranch = syncRepo.defaultBranch;
1445
1582
  }
1446
1583
  }
1447
1584
 
@@ -1590,6 +1727,96 @@ export class GitStorage {
1590
1727
  };
1591
1728
  }
1592
1729
 
1730
+ /**
1731
+ * Create a generic git credential for a repository.
1732
+ * Used to authenticate sync operations for non-GitHub providers (GitLab, Bitbucket, etc.)
1733
+ */
1734
+ async createGitCredential(
1735
+ options: CreateGitCredentialOptions
1736
+ ): Promise<GitCredential> {
1737
+ const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
1738
+ const jwt = await this.generateJWT(options.repoId, {
1739
+ permissions: ['repo:write'],
1740
+ ttl,
1741
+ });
1742
+
1743
+ const body: Record<string, unknown> = {
1744
+ repo_id: options.repoId,
1745
+ password: options.password,
1746
+ };
1747
+ if (options.username !== undefined) {
1748
+ body.username = options.username;
1749
+ }
1750
+
1751
+ const resp = await this.api.post(
1752
+ { path: 'repos/git-credentials', body },
1753
+ jwt,
1754
+ { allowedStatus: [409] }
1755
+ );
1756
+ if (resp.status === 409) {
1757
+ throw new Error('A credential already exists for this repository');
1758
+ }
1759
+
1760
+ const data = (await resp.json()) as { id: string };
1761
+ return { id: data.id };
1762
+ }
1763
+
1764
+ /**
1765
+ * Update an existing generic git credential.
1766
+ */
1767
+ async updateGitCredential(
1768
+ options: UpdateGitCredentialOptions
1769
+ ): Promise<GitCredential> {
1770
+ const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
1771
+ const jwt = await this.generateJWT('org', {
1772
+ permissions: ['repo:write'],
1773
+ ttl,
1774
+ });
1775
+
1776
+ const body: Record<string, unknown> = {
1777
+ id: options.id,
1778
+ password: options.password,
1779
+ };
1780
+ if (options.username !== undefined) {
1781
+ body.username = options.username;
1782
+ }
1783
+
1784
+ const resp = await this.api.put(
1785
+ { path: 'repos/git-credentials', body },
1786
+ jwt,
1787
+ { allowedStatus: [404] }
1788
+ );
1789
+ if (resp.status === 404) {
1790
+ throw new Error('Credential not found');
1791
+ }
1792
+
1793
+ const data = (await resp.json()) as { id: string; created_at?: string };
1794
+ return {
1795
+ id: data.id,
1796
+ ...(data.created_at ? { createdAt: data.created_at } : {}),
1797
+ };
1798
+ }
1799
+
1800
+ /**
1801
+ * Delete a generic git credential.
1802
+ */
1803
+ async deleteGitCredential(options: DeleteGitCredentialOptions): Promise<void> {
1804
+ const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
1805
+ const jwt = await this.generateJWT('org', {
1806
+ permissions: ['repo:write'],
1807
+ ttl,
1808
+ });
1809
+
1810
+ const resp = await this.api.delete(
1811
+ { path: 'repos/git-credentials', body: { id: options.id } },
1812
+ jwt,
1813
+ { allowedStatus: [404] }
1814
+ );
1815
+ if (resp.status === 404) {
1816
+ throw new Error('Credential not found');
1817
+ }
1818
+ }
1819
+
1593
1820
  /**
1594
1821
  * Get the current configuration
1595
1822
  * @returns The client configuration
package/src/schemas.ts CHANGED
@@ -141,6 +141,29 @@ export const createBranchResponseSchema = z.object({
141
141
  commit_sha: z.string().nullable().optional(),
142
142
  });
143
143
 
144
+ export const tagInfoSchema = z.object({
145
+ cursor: z.string(),
146
+ name: z.string(),
147
+ sha: z.string(),
148
+ });
149
+
150
+ export const listTagsResponseSchema = z.object({
151
+ tags: z.array(tagInfoSchema),
152
+ next_cursor: z.string().nullable().optional(),
153
+ has_more: z.boolean(),
154
+ });
155
+
156
+ export const createTagResponseSchema = z.object({
157
+ name: z.string(),
158
+ sha: z.string(),
159
+ message: z.string(),
160
+ });
161
+
162
+ export const deleteTagResponseSchema = z.object({
163
+ name: z.string(),
164
+ message: z.string(),
165
+ });
166
+
144
167
  export const refUpdateResultSchema = z.object({
145
168
  branch: z.string(),
146
169
  old_sha: z.string(),
@@ -244,6 +267,10 @@ export type GetCommitDiffResponseRaw = z.infer<typeof commitDiffResponseSchema>;
244
267
  export type CreateBranchResponseRaw = z.infer<
245
268
  typeof createBranchResponseSchema
246
269
  >;
270
+ export type RawTagInfo = z.infer<typeof tagInfoSchema>;
271
+ export type ListTagsResponseRaw = z.infer<typeof listTagsResponseSchema>;
272
+ export type CreateTagResponseRaw = z.infer<typeof createTagResponseSchema>;
273
+ export type DeleteTagResponseRaw = z.infer<typeof deleteTagResponseSchema>;
247
274
  export type CommitPackAckRaw = z.infer<typeof commitPackAckSchema>;
248
275
  export type RestoreCommitAckRaw = z.infer<typeof restoreCommitAckSchema>;
249
276
  export type GrepResponseRaw = z.infer<typeof grepResponseSchema>;
package/src/types.ts CHANGED
@@ -3,6 +3,8 @@
3
3
  */
4
4
  import type {
5
5
  CreateBranchResponseRaw,
6
+ CreateTagResponseRaw,
7
+ DeleteTagResponseRaw,
6
8
  GetBranchDiffResponseRaw,
7
9
  GetCommitDiffResponseRaw,
8
10
  ListBranchesResponseRaw,
@@ -10,6 +12,7 @@ import type {
10
12
  ListFilesResponseRaw,
11
13
  ListFilesWithMetadataResponseRaw,
12
14
  ListReposResponseRaw,
15
+ ListTagsResponseRaw,
13
16
  NoteReadResponseRaw,
14
17
  NoteWriteResponseRaw,
15
18
  RawBranchInfo as SchemaRawBranchInfo,
@@ -20,6 +23,7 @@ import type {
20
23
  RawFilteredFile as SchemaRawFilteredFile,
21
24
  RawRepoBaseInfo as SchemaRawRepoBaseInfo,
22
25
  RawRepoInfo as SchemaRawRepoInfo,
26
+ RawTagInfo as SchemaRawTagInfo,
23
27
  } from './schemas';
24
28
 
25
29
  export interface OverrideableGitStorageOptions {
@@ -57,7 +61,10 @@ export interface Repo {
57
61
  options?: ListFilesWithMetadataOptions
58
62
  ): Promise<ListFilesWithMetadataResult>;
59
63
  listBranches(options?: ListBranchesOptions): Promise<ListBranchesResult>;
64
+ listTags(options?: ListTagsOptions): Promise<ListTagsResult>;
60
65
  listCommits(options?: ListCommitsOptions): Promise<ListCommitsResult>;
66
+ createTag(options: CreateTagOptions): Promise<CreateTagResult>;
67
+ deleteTag(options: DeleteTagOptions): Promise<DeleteTagResult>;
61
68
  getNote(options: GetNoteOptions): Promise<GetNoteResult>;
62
69
  createNote(options: CreateNoteOptions): Promise<NoteWriteResult>;
63
70
  appendNote(options: AppendNoteOptions): Promise<NoteWriteResult>;
@@ -97,7 +104,14 @@ export interface RepoOptions {
97
104
  createdAt?: string;
98
105
  }
99
106
 
100
- export type SupportedRepoProvider = 'github';
107
+ export type SupportedRepoProvider =
108
+ | 'github'
109
+ | 'gitlab'
110
+ | 'bitbucket'
111
+ | 'gitea'
112
+ | 'forgejo'
113
+ | 'codeberg'
114
+ | 'sr.ht';
101
115
 
102
116
  export interface PublicGitHubBaseRepoAuth {
103
117
  /**
@@ -110,20 +124,59 @@ export interface GitHubBaseRepo {
110
124
  /**
111
125
  * @default github
112
126
  */
113
- provider?: SupportedRepoProvider;
127
+ provider?: 'github';
114
128
  owner: string;
115
129
  name: string;
116
130
  defaultBranch?: string;
117
131
  auth?: PublicGitHubBaseRepoAuth;
118
132
  }
119
133
 
134
+ export interface GenericGitBaseRepo {
135
+ /**
136
+ * The git host provider. Must be one of the supported generic git providers.
137
+ */
138
+ provider: Exclude<SupportedRepoProvider, 'github'>;
139
+ owner: string;
140
+ name: string;
141
+ defaultBranch?: string;
142
+ /**
143
+ * Bare hostname for self-hosted instances (e.g. "gitlab.example.com").
144
+ * Falls back to the provider's default host when omitted.
145
+ */
146
+ upstreamHost?: string;
147
+ }
148
+
120
149
  export interface ForkBaseRepo {
121
150
  id: string;
122
151
  ref?: string;
123
152
  sha?: string;
124
153
  }
125
154
 
126
- export type BaseRepo = GitHubBaseRepo | ForkBaseRepo;
155
+ export type BaseRepo = GitHubBaseRepo | ForkBaseRepo | GenericGitBaseRepo;
156
+
157
+ export interface CreateGitCredentialOptions {
158
+ repoId: string;
159
+ username?: string;
160
+ password: string;
161
+ ttl?: number;
162
+ }
163
+
164
+ export interface UpdateGitCredentialOptions {
165
+ id: string;
166
+ username?: string;
167
+ password: string;
168
+ ttl?: number;
169
+ }
170
+
171
+ export interface DeleteGitCredentialOptions {
172
+ id: string;
173
+ ttl?: number;
174
+ }
175
+
176
+ export interface GitCredential {
177
+ id: string;
178
+ createdAt?: string;
179
+ }
127
180
 
128
181
  export interface ListReposOptions extends GitStorageInvocationOptions {
129
182
  cursor?: string;
@@ -275,6 +328,51 @@ export interface CreateBranchResult {
275
328
  commitSha?: string;
276
329
  }
277
330
 
331
+ export interface ListTagsOptions extends GitStorageInvocationOptions {
332
+ cursor?: string;
333
+ limit?: number;
334
+ }
335
+
336
+ export type RawTagInfo = SchemaRawTagInfo;
337
+
338
+ export interface TagInfo {
339
+ cursor: string;
340
+ name: string;
341
+ sha: string;
342
+ }
343
+
344
+ export type ListTagsResponse = ListTagsResponseRaw;
345
+
346
+ export interface ListTagsResult {
347
+ tags: TagInfo[];
348
+ nextCursor?: string;
349
+ hasMore: boolean;
350
+ }
351
+
352
+ export interface CreateTagOptions extends GitStorageInvocationOptions {
353
+ name: string;
354
+ target: string;
355
+ }
356
+
357
+ export type CreateTagResponse = CreateTagResponseRaw;
358
+
359
+ export interface CreateTagResult {
360
+ name: string;
361
+ sha: string;
362
+ message: string;
363
+ }
364
+
365
+ export interface DeleteTagOptions extends GitStorageInvocationOptions {
366
+ name: string;
367
+ }
368
+
369
+ export type DeleteTagResponse = DeleteTagResponseRaw;
370
+
371
+ export interface DeleteTagResult {
372
+ name: string;
373
+ message: string;
374
+ }
375
+
278
376
  // List Commits API types
279
377
  export interface ListCommitsOptions extends GitStorageInvocationOptions {
280
378
  branch?: string;