@sourceaxis/provider-github 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1112 @@
1
+ import { anonymousAuth, createAuthContext, tokenAuth } from "@sourceaxis/auth";
2
+ import { createSourceAxisClient } from "@sourceaxis/core";
3
+ import { AuthenticationError, AuthorizationError, CapabilityNotSupportedError, ConflictError, SourceAxisError, NotFoundError, ProviderError, RateLimitError, TransportError, ValidationError } from "@sourceaxis/errors";
4
+ import { deepFreeze } from "@sourceaxis/shared";
5
+ import { createTransportResponse } from "@sourceaxis/transport";
6
+ import { mapGitHubBranch, mapGitHubCommit, mapGitHubContentToBlob, mapGitHubContentToFileInfo, mapGitHubContentToTreeNode, mapGitHubIssue, mapGitHubPullRequest, mapGitHubRelease, mapGitHubRepository, mapGitHubSearchItem, mapGitHubTag, mapGitHubTree } from "./mappers.js";
7
+ export { GitHubProviderId, GitHubProviderName, GitHubProviderVersion } from "./constants.js";
8
+ import { GitHubProviderId, GitHubProviderName, GitHubProviderVersion } from "./constants.js";
9
+ const foundationCapabilityDescriptors = deepFreeze([
10
+ {
11
+ name: "files",
12
+ operations: [
13
+ "readText",
14
+ "readJson",
15
+ "readBinary",
16
+ "download",
17
+ "exists",
18
+ "metadata",
19
+ "getMetadata"
20
+ ],
21
+ status: "supported"
22
+ },
23
+ {
24
+ name: "tree",
25
+ operations: ["list", "get", "tree"],
26
+ status: "supported"
27
+ },
28
+ {
29
+ name: "history",
30
+ operations: ["list", "get", "file"],
31
+ status: "supported"
32
+ },
33
+ {
34
+ name: "search",
35
+ operations: ["text", "query"],
36
+ status: "supported"
37
+ },
38
+ {
39
+ name: "branches",
40
+ operations: ["list", "get"],
41
+ status: "supported"
42
+ },
43
+ {
44
+ name: "tags",
45
+ operations: ["list", "get"],
46
+ status: "supported"
47
+ },
48
+ {
49
+ name: "issues",
50
+ operations: ["list", "get"],
51
+ status: "supported"
52
+ },
53
+ {
54
+ name: "pullRequests",
55
+ operations: ["list", "get"],
56
+ status: "supported"
57
+ },
58
+ {
59
+ name: "releases",
60
+ operations: ["list", "get"],
61
+ status: "supported"
62
+ }
63
+ ]);
64
+ export const GitHubProviderCapabilities = deepFreeze({
65
+ branches: foundationCapabilityDescriptors[4],
66
+ files: foundationCapabilityDescriptors[0],
67
+ history: foundationCapabilityDescriptors[2],
68
+ issues: foundationCapabilityDescriptors[6],
69
+ pullRequests: foundationCapabilityDescriptors[7],
70
+ releases: foundationCapabilityDescriptors[8],
71
+ search: foundationCapabilityDescriptors[3],
72
+ tags: foundationCapabilityDescriptors[5],
73
+ tree: foundationCapabilityDescriptors[1]
74
+ });
75
+ /**
76
+ * Creates the GitHub provider implementation for explicit Core registration.
77
+ */
78
+ export function createGitHubProvider(config = {}) {
79
+ return new GitHubProvider(config);
80
+ }
81
+ /**
82
+ * Creates the GitHub provider with a short, discoverable name for application setup.
83
+ */
84
+ export function githubProvider(config = {}) {
85
+ return createGitHubProvider(config);
86
+ }
87
+ /**
88
+ * Creates a SourceAxis client config fragment containing the GitHub provider.
89
+ */
90
+ export function createGitHubProviderConfig(config = {}) {
91
+ return {
92
+ providers: [createGitHubProvider(config)]
93
+ };
94
+ }
95
+ /**
96
+ * Creates a GitHub-scoped token authentication strategy for SourceAxis clients.
97
+ */
98
+ export function githubTokenAuth(token, options = {}) {
99
+ return Object.freeze({
100
+ type: "token",
101
+ async authenticate(request) {
102
+ if (request?.provider !== undefined && request.provider !== GitHubProviderId) {
103
+ return createAuthContext(anonymousAuth({ provider: request.provider }));
104
+ }
105
+ return createAuthContext(tokenAuth({ ...options, provider: GitHubProviderId, token }));
106
+ }
107
+ });
108
+ }
109
+ /**
110
+ * Creates a SourceAxis client with the GitHub provider registered.
111
+ */
112
+ export function createGitHubClient(config = {}) {
113
+ const { authentication, token, ...providerConfig } = config;
114
+ const resolvedProviderConfig = {
115
+ ...providerConfig,
116
+ transport: providerConfig.transport ?? createGitHubHttpTransport()
117
+ };
118
+ return createSourceAxisClient({
119
+ ...createGitHubProviderConfig(resolvedProviderConfig),
120
+ authentication: authentication ?? (token === undefined ? undefined : githubTokenAuth(token))
121
+ });
122
+ }
123
+ export class GitHubProvider {
124
+ #config;
125
+ #hosts;
126
+ info;
127
+ constructor(config = {}) {
128
+ this.#hosts = deepFreeze(normalizeHosts(config.hosts ?? ["github.com"]));
129
+ this.#config = Object.freeze({
130
+ ...config,
131
+ hosts: this.#hosts
132
+ });
133
+ this.info = deepFreeze({
134
+ capabilities: GitHubProviderCapabilities,
135
+ id: GitHubProviderId,
136
+ metadata: config.metadata,
137
+ name: GitHubProviderName,
138
+ priority: config.priority,
139
+ version: GitHubProviderVersion
140
+ });
141
+ }
142
+ async supports(locator) {
143
+ const location = parseGitHubRepositoryUrl(locator.url, this.#hosts);
144
+ if (location === undefined) {
145
+ return deepFreeze({ confidence: "none", provider: GitHubProviderId });
146
+ }
147
+ return deepFreeze({
148
+ confidence: "exact",
149
+ metadata: { provider: GitHubProviderId, extra: { host: location.host } },
150
+ provider: GitHubProviderId,
151
+ repository: toRepositoryIdentity(location)
152
+ });
153
+ }
154
+ async createSession(request) {
155
+ return runGitHubProviderOperation("provider.session.create", async () => {
156
+ const location = resolveRepositoryLocation(request.repository, this.#hosts);
157
+ const dependencies = resolveDependencies(this.#config, request.context);
158
+ const context = createGitHubSessionContext(request.context, dependencies);
159
+ const requestContext = {
160
+ transport: dependencies.transport
161
+ };
162
+ if (context.authenticationContext !== undefined) {
163
+ requestContext.authentication = context.authenticationContext;
164
+ }
165
+ if (context.metadata !== undefined) {
166
+ requestContext.metadata = context.metadata;
167
+ }
168
+ const requestClient = createGitHubRequestClient(requestContext);
169
+ const fallback = toRepositoryInfo(location);
170
+ const repository = await readRepositoryInfo(requestClient, location, fallback);
171
+ return new GitHubProviderSession({
172
+ context,
173
+ requestClient,
174
+ repository
175
+ });
176
+ });
177
+ }
178
+ }
179
+ export class GitHubProviderSession {
180
+ #context;
181
+ #diagnosticSequence = 0;
182
+ #requestClient;
183
+ #state = "active";
184
+ capabilities;
185
+ provider = deepFreeze({
186
+ capabilities: GitHubProviderCapabilities,
187
+ id: GitHubProviderId,
188
+ name: GitHubProviderName,
189
+ version: GitHubProviderVersion
190
+ });
191
+ repository;
192
+ constructor(input) {
193
+ this.#context = Object.freeze(input.context);
194
+ this.#requestClient = input.requestClient;
195
+ this.repository = deepFreeze(input.repository);
196
+ this.capabilities = createFoundationCapabilities(this.repository.identity, this.#requestClient, () => this.ensureActive());
197
+ }
198
+ get state() {
199
+ return this.#state;
200
+ }
201
+ get context() {
202
+ return this.#context;
203
+ }
204
+ async getCapabilities() {
205
+ this.ensureActive();
206
+ return foundationCapabilityDescriptors;
207
+ }
208
+ async dispose() {
209
+ if (this.#state === "disposed") {
210
+ return;
211
+ }
212
+ this.#state = "disposed";
213
+ await this.publishDiagnostic("provider.session.dispose");
214
+ }
215
+ ensureActive() {
216
+ if (this.#state === "disposed") {
217
+ throw new ProviderError("GitHub provider session has been disposed", {
218
+ diagnostics: {
219
+ operation: { operation: "provider.session.lifecycle" },
220
+ provider: { provider: GitHubProviderId },
221
+ repository: formatRepositoryDiagnostics(this.repository.identity)
222
+ },
223
+ retryability: "Never"
224
+ });
225
+ }
226
+ }
227
+ async publishDiagnostic(name) {
228
+ try {
229
+ await this.#context.diagnostics?.publish(deepFreeze({
230
+ context: {
231
+ provider: GitHubProviderId,
232
+ repository: `${this.repository.identity.owner}/${this.repository.identity.name}`
233
+ },
234
+ id: `github-${name}-${++this.#diagnosticSequence}`,
235
+ kind: "provider",
236
+ name,
237
+ schemaVersion: "1.0",
238
+ timestamp: new Date().toISOString()
239
+ }));
240
+ }
241
+ catch {
242
+ // Diagnostics are observational and must not influence provider lifecycle.
243
+ }
244
+ }
245
+ }
246
+ function createGitHubRequestClient(context) {
247
+ return deepFreeze({
248
+ async request(request) {
249
+ const transportRequest = toTransportRequest(request, context);
250
+ try {
251
+ const transportContext = {};
252
+ if (context.metadata !== undefined) {
253
+ transportContext.metadata = context.metadata;
254
+ }
255
+ const response = await context.transport.execute(transportRequest, transportContext);
256
+ return fromTransportResponse(response);
257
+ }
258
+ catch (error) {
259
+ throw mapGitHubError(error, "github.transport.request");
260
+ }
261
+ }
262
+ });
263
+ }
264
+ async function readRepositoryInfo(requestClient, location, fallback) {
265
+ const response = await requestGitHubResponse(requestClient, "github.repository.get", `/repos/${encodePathPart(location.owner)}/${encodePathPart(location.name)}`);
266
+ if (response.data === undefined || response.status === 204) {
267
+ return fallback;
268
+ }
269
+ return mapGitHubRepository(response.data, fallback.identity);
270
+ }
271
+ async function readContent(requestClient, repository, path, operation) {
272
+ const response = await requestGitHub(requestClient, operation, `/repos/${encodePathPart(repository.owner)}/${encodePathPart(repository.name)}/contents/${encodeRepositoryPath(path)}`);
273
+ if (Array.isArray(response)) {
274
+ throw new ValidationError("GitHub content response is a directory, not a file", {
275
+ diagnostics: {
276
+ operation: { operation },
277
+ provider: { provider: GitHubProviderId },
278
+ repository: formatRepositoryDiagnostics(repository)
279
+ }
280
+ });
281
+ }
282
+ return response;
283
+ }
284
+ async function requestGitHub(requestClient, operation, url) {
285
+ const response = await requestGitHubResponse(requestClient, operation, url);
286
+ if (response.data === undefined) {
287
+ throw new ProviderError("GitHub response did not include a response body", {
288
+ diagnostics: {
289
+ operation: { operation },
290
+ provider: { provider: GitHubProviderId, status: response.status }
291
+ },
292
+ retryability: "Maybe"
293
+ });
294
+ }
295
+ return response.data;
296
+ }
297
+ async function requestGitHubResponse(requestClient, operation, url) {
298
+ try {
299
+ const response = await requestClient.request({ method: "GET", url });
300
+ if (response.status === 404) {
301
+ throw new NotFoundError("GitHub resource was not found", {
302
+ diagnostics: {
303
+ operation: { operation },
304
+ provider: { provider: GitHubProviderId, status: response.status }
305
+ },
306
+ retryability: "Never"
307
+ });
308
+ }
309
+ if (response.status < 200 || response.status >= 300) {
310
+ throw createGitHubStatusError(response.status, operation, response.headers);
311
+ }
312
+ return response;
313
+ }
314
+ catch (error) {
315
+ throw mapGitHubError(error, operation);
316
+ }
317
+ }
318
+ export function mapGitHubError(error, operation = "github.provider") {
319
+ if (error instanceof SourceAxisError) {
320
+ return error;
321
+ }
322
+ const status = getErrorStatus(error);
323
+ const providerDiagnostics = {
324
+ provider: GitHubProviderId
325
+ };
326
+ const providerCode = getErrorCode(error);
327
+ if (providerCode !== undefined) {
328
+ providerDiagnostics.providerCode = providerCode;
329
+ }
330
+ const requestId = getErrorRequestId(error);
331
+ if (requestId !== undefined) {
332
+ providerDiagnostics.requestId = requestId;
333
+ }
334
+ if (status !== undefined) {
335
+ providerDiagnostics.status = status;
336
+ }
337
+ const diagnostics = {
338
+ operation: { operation },
339
+ provider: providerDiagnostics
340
+ };
341
+ if (status === 401) {
342
+ return new AuthenticationError("GitHub authentication failed", {
343
+ cause: error,
344
+ diagnostics,
345
+ retryability: "Never"
346
+ });
347
+ }
348
+ if (status === 403 && isRateLimitError(error)) {
349
+ return new RateLimitError("GitHub rate limit exceeded", {
350
+ cause: error,
351
+ diagnostics,
352
+ retryability: "Always"
353
+ });
354
+ }
355
+ if (status === 403) {
356
+ return new AuthorizationError("GitHub authorization failed", {
357
+ cause: error,
358
+ diagnostics,
359
+ retryability: "Never"
360
+ });
361
+ }
362
+ if (status === 404) {
363
+ return new NotFoundError("GitHub resource was not found", {
364
+ cause: error,
365
+ diagnostics,
366
+ retryability: "Never"
367
+ });
368
+ }
369
+ if (status === 409) {
370
+ return new ConflictError("GitHub reported a resource conflict", {
371
+ cause: error,
372
+ diagnostics,
373
+ retryability: "Maybe"
374
+ });
375
+ }
376
+ if (status !== undefined && status >= 500) {
377
+ return new ProviderError("GitHub provider request failed", {
378
+ cause: error,
379
+ diagnostics,
380
+ retryability: "Maybe"
381
+ });
382
+ }
383
+ if (status !== undefined) {
384
+ return new ProviderError("GitHub provider rejected the request", {
385
+ cause: error,
386
+ diagnostics,
387
+ retryability: retryabilityForStatus(status)
388
+ });
389
+ }
390
+ return new ProviderError("GitHub provider operation failed", {
391
+ cause: error,
392
+ diagnostics,
393
+ retryability: "Maybe"
394
+ });
395
+ }
396
+ function createGitHubSessionContext(context, dependencies) {
397
+ return Object.freeze({
398
+ ...context,
399
+ cache: dependencies.cache,
400
+ diagnostics: dependencies.diagnostics,
401
+ provider: {
402
+ ...context.provider,
403
+ capabilities: GitHubProviderCapabilities
404
+ },
405
+ transport: dependencies.transport
406
+ });
407
+ }
408
+ function resolveDependencies(config, context) {
409
+ const dependencies = {
410
+ transport: config.transport ?? context.transport ?? createGitHubHttpTransport()
411
+ };
412
+ const cache = config.cache ?? context.cache;
413
+ const diagnostics = config.diagnostics ?? context.diagnostics;
414
+ if (cache !== undefined) {
415
+ dependencies.cache = cache;
416
+ }
417
+ if (diagnostics !== undefined) {
418
+ dependencies.diagnostics = diagnostics;
419
+ }
420
+ return dependencies;
421
+ }
422
+ function createFoundationCapabilities(repository, requestClient, ensureActive) {
423
+ const runtime = createGitHubRuntimeCapabilities(repository, requestClient, ensureActive);
424
+ return deepFreeze({
425
+ branches: runtime.branches,
426
+ files: runtime.files,
427
+ history: runtime.history,
428
+ issues: runtime.issues,
429
+ pullRequests: runtime.pullRequests,
430
+ releases: runtime.releases,
431
+ search: runtime.search,
432
+ tags: runtime.tags,
433
+ tree: runtime.tree
434
+ });
435
+ }
436
+ function createGitHubRuntimeCapabilities(repository, requestClient, ensureActive) {
437
+ return deepFreeze({
438
+ branches: createGitHubBranchesCapability(repository, requestClient, ensureActive),
439
+ files: createGitHubFilesCapability(repository, requestClient, ensureActive),
440
+ history: createGitHubHistoryCapability(repository, requestClient, ensureActive),
441
+ issues: createGitHubIssuesCapability(repository, requestClient, ensureActive),
442
+ pullRequests: createGitHubPullRequestsCapability(repository, requestClient, ensureActive),
443
+ releases: createGitHubReleasesCapability(repository, requestClient, ensureActive),
444
+ search: createGitHubSearchCapability(repository, requestClient, ensureActive),
445
+ tags: createGitHubTagsCapability(repository, requestClient, ensureActive),
446
+ tree: createGitHubTreeCapability(repository, requestClient, ensureActive)
447
+ });
448
+ }
449
+ function createGitHubBranchesCapability(repository, requestClient, ensureActive) {
450
+ return deepFreeze({
451
+ async get(name) {
452
+ ensureActive();
453
+ validateNonEmpty(name, "Branch name must be a non-empty string");
454
+ const response = await requestGitHub(requestClient, "github.branches.get", `/repos/${encodePathPart(repository.owner)}/${encodePathPart(repository.name)}/branches/${encodePathPart(name)}`);
455
+ return mapGitHubBranch(response, repository);
456
+ },
457
+ async list(_options) {
458
+ ensureActive();
459
+ const response = await requestGitHub(requestClient, "github.branches.list", `/repos/${encodePathPart(repository.owner)}/${encodePathPart(repository.name)}/branches`);
460
+ return createPage(response.map((branch) => mapGitHubBranch(branch, repository)));
461
+ }
462
+ });
463
+ }
464
+ function createGitHubTagsCapability(repository, requestClient, ensureActive) {
465
+ return deepFreeze({
466
+ async get(name) {
467
+ ensureActive();
468
+ validateNonEmpty(name, "Tag name must be a non-empty string");
469
+ const response = await requestGitHub(requestClient, "github.tags.get", `/repos/${encodePathPart(repository.owner)}/${encodePathPart(repository.name)}/git/ref/tags/${encodePathPart(name)}`);
470
+ return mapGitHubTag(response, repository);
471
+ },
472
+ async list(_options) {
473
+ ensureActive();
474
+ const response = await requestGitHub(requestClient, "github.tags.list", `/repos/${encodePathPart(repository.owner)}/${encodePathPart(repository.name)}/git/refs/tags`);
475
+ return createPage(response.map((tag) => mapGitHubTag(tag, repository)));
476
+ }
477
+ });
478
+ }
479
+ function createGitHubHistoryCapability(repository, requestClient, ensureActive) {
480
+ return deepFreeze({
481
+ async file(path, _options) {
482
+ ensureActive();
483
+ validatePath(path);
484
+ const response = await requestGitHub(requestClient, "github.history.file", `/repos/${encodePathPart(repository.owner)}/${encodePathPart(repository.name)}/commits?path=${encodeURIComponent(path)}`);
485
+ return createPage(response.map(mapGitHubCommit));
486
+ },
487
+ async get(sha) {
488
+ ensureActive();
489
+ validateNonEmpty(sha, "Commit sha must be a non-empty string");
490
+ const response = await requestGitHub(requestClient, "github.history.get", `/repos/${encodePathPart(repository.owner)}/${encodePathPart(repository.name)}/commits/${encodePathPart(sha)}`);
491
+ return mapGitHubCommit(response);
492
+ },
493
+ async list(_options) {
494
+ ensureActive();
495
+ const response = await requestGitHub(requestClient, "github.history.list", `/repos/${encodePathPart(repository.owner)}/${encodePathPart(repository.name)}/commits`);
496
+ return createPage(response.map(mapGitHubCommit));
497
+ }
498
+ });
499
+ }
500
+ function createGitHubTreeCapability(repository, requestClient, ensureActive) {
501
+ return deepFreeze({
502
+ async get(path) {
503
+ ensureActive();
504
+ validatePath(path);
505
+ const response = await requestGitHub(requestClient, "github.tree.get", `/repos/${encodePathPart(repository.owner)}/${encodePathPart(repository.name)}/contents/${encodeRepositoryPath(path)}`);
506
+ return mapGitHubContentToTreeNode(response);
507
+ },
508
+ async list(path) {
509
+ ensureActive();
510
+ validateOptionalPath(path);
511
+ const suffix = path === undefined ? "" : `/${encodeRepositoryPath(path)}`;
512
+ const response = await requestGitHub(requestClient, "github.tree.list", `/repos/${encodePathPart(repository.owner)}/${encodePathPart(repository.name)}/contents${suffix}`);
513
+ const items = Array.isArray(response) ? response : [response];
514
+ return deepFreeze(items.map(mapGitHubContentToTreeNode));
515
+ },
516
+ async tree(path) {
517
+ ensureActive();
518
+ validateOptionalPath(path);
519
+ const ref = path ?? "HEAD";
520
+ const response = await requestGitHub(requestClient, "github.tree.tree", `/repos/${encodePathPart(repository.owner)}/${encodePathPart(repository.name)}/git/trees/${encodePathPart(ref)}`);
521
+ return mapGitHubTree(response, path ?? "");
522
+ },
523
+ walk() {
524
+ ensureActive();
525
+ return rejectUnsupportedIterable("tree", repository);
526
+ }
527
+ });
528
+ }
529
+ function createGitHubFilesCapability(repository, requestClient, ensureActive) {
530
+ return deepFreeze({
531
+ async download(path) {
532
+ ensureActive();
533
+ validatePath(path);
534
+ const response = await readContent(requestClient, repository, path, "github.files.download");
535
+ return mapGitHubContentToBlob(response);
536
+ },
537
+ async exists(path) {
538
+ ensureActive();
539
+ validatePath(path);
540
+ try {
541
+ await readContent(requestClient, repository, path, "github.files.exists");
542
+ return true;
543
+ }
544
+ catch (error) {
545
+ if (error instanceof NotFoundError) {
546
+ return false;
547
+ }
548
+ throw error;
549
+ }
550
+ },
551
+ async metadata(path) {
552
+ ensureActive();
553
+ validatePath(path);
554
+ return mapGitHubContentToFileInfo(await readContent(requestClient, repository, path, "github.files.metadata"));
555
+ },
556
+ async getMetadata(path) {
557
+ return this.metadata(path);
558
+ },
559
+ async readBinary(path) {
560
+ ensureActive();
561
+ validatePath(path);
562
+ const blob = mapGitHubContentToBlob(await readContent(requestClient, repository, path, "github.files.readBinary"));
563
+ return decodeBlobContent(blob);
564
+ },
565
+ async readJson(path) {
566
+ ensureActive();
567
+ const text = await this.readText(path);
568
+ return JSON.parse(text);
569
+ },
570
+ async readText(path) {
571
+ ensureActive();
572
+ validatePath(path);
573
+ return new TextDecoder().decode(decodeBlobContent(mapGitHubContentToBlob(await readContent(requestClient, repository, path, "github.files.readText"))));
574
+ },
575
+ async stream(path) {
576
+ ensureActive();
577
+ validatePath(path);
578
+ const content = await this.readBinary(path);
579
+ return (async function* streamContent() {
580
+ yield content;
581
+ })();
582
+ }
583
+ });
584
+ }
585
+ function createGitHubIssuesCapability(repository, requestClient, ensureActive) {
586
+ return deepFreeze({
587
+ async get(number) {
588
+ ensureActive();
589
+ validatePositiveInteger(number, "Issue number must be a positive integer");
590
+ const response = await requestGitHub(requestClient, "github.issues.get", `/repos/${encodePathPart(repository.owner)}/${encodePathPart(repository.name)}/issues/${number}`);
591
+ return mapGitHubIssue(response);
592
+ },
593
+ async list(options) {
594
+ ensureActive();
595
+ return requestGitHubPage(requestClient, "github.issues.list", withPagination(`/repos/${encodePathPart(repository.owner)}/${encodePathPart(repository.name)}/issues`, options), mapGitHubIssue);
596
+ }
597
+ });
598
+ }
599
+ function createGitHubPullRequestsCapability(repository, requestClient, ensureActive) {
600
+ return deepFreeze({
601
+ async get(number) {
602
+ ensureActive();
603
+ validatePositiveInteger(number, "Pull request number must be a positive integer");
604
+ const response = await requestGitHub(requestClient, "github.pullRequests.get", `/repos/${encodePathPart(repository.owner)}/${encodePathPart(repository.name)}/pulls/${number}`);
605
+ return mapGitHubPullRequest(response, repository);
606
+ },
607
+ async list(options) {
608
+ ensureActive();
609
+ return requestGitHubPage(requestClient, "github.pullRequests.list", withPagination(`/repos/${encodePathPart(repository.owner)}/${encodePathPart(repository.name)}/pulls`, options), (pullRequest) => mapGitHubPullRequest(pullRequest, repository));
610
+ }
611
+ });
612
+ }
613
+ function createGitHubReleasesCapability(repository, requestClient, ensureActive) {
614
+ return deepFreeze({
615
+ async get(tagName) {
616
+ ensureActive();
617
+ validateNonEmpty(tagName, "Release tag name must be a non-empty string");
618
+ const response = await requestGitHub(requestClient, "github.releases.get", `/repos/${encodePathPart(repository.owner)}/${encodePathPart(repository.name)}/releases/tags/${encodePathPart(tagName)}`);
619
+ return mapGitHubRelease(response);
620
+ },
621
+ async list(options) {
622
+ ensureActive();
623
+ return requestGitHubPage(requestClient, "github.releases.list", withPagination(`/repos/${encodePathPart(repository.owner)}/${encodePathPart(repository.name)}/releases`, options), mapGitHubRelease);
624
+ }
625
+ });
626
+ }
627
+ function createGitHubSearchCapability(repository, requestClient, ensureActive) {
628
+ return deepFreeze({
629
+ async query(query) {
630
+ ensureActive();
631
+ validateNonEmpty(query.text, "Search query text must be a non-empty string");
632
+ const pathQualifier = query.path === undefined ? "" : ` path:${query.path}`;
633
+ const response = await requestGitHub(requestClient, "github.search.query", withPagination(`/search/code?q=${encodeURIComponent(`${query.text} repo:${repository.owner}/${repository.name}${pathQualifier}`)}`, query));
634
+ return createSearchPage(response);
635
+ },
636
+ async text(query, options) {
637
+ ensureActive();
638
+ validateNonEmpty(query, "Search query text must be a non-empty string");
639
+ return this.query({ text: query, ...(options ?? {}) });
640
+ }
641
+ });
642
+ }
643
+ async function requestGitHubPage(requestClient, operation, url, map) {
644
+ const response = await requestGitHubResponse(requestClient, operation, url);
645
+ if (response.data === undefined) {
646
+ throw new ProviderError("GitHub response did not include a response body", {
647
+ diagnostics: {
648
+ operation: { operation },
649
+ provider: { provider: GitHubProviderId, status: response.status }
650
+ },
651
+ retryability: "Maybe"
652
+ });
653
+ }
654
+ return createPage(response.data.map(map), response.headers);
655
+ }
656
+ function createSearchPage(response) {
657
+ return deepFreeze({
658
+ items: (response.items ?? []).map((item) => mapGitHubSearchItem(item)),
659
+ pageInfo: {
660
+ hasNextPage: false,
661
+ hasPreviousPage: false,
662
+ totalCount: response.total_count
663
+ }
664
+ });
665
+ }
666
+ async function rejectUnsupported(capability, repository) {
667
+ throw new CapabilityNotSupportedError("GitHub provider capability operation is not supported", {
668
+ diagnostics: {
669
+ operation: { capability, operation: `github.${capability}` },
670
+ provider: { provider: GitHubProviderId },
671
+ repository: formatRepositoryDiagnostics(repository)
672
+ },
673
+ retryability: "Never"
674
+ });
675
+ }
676
+ async function* rejectUnsupportedIterable(capability, repository) {
677
+ await rejectUnsupported(capability, repository);
678
+ }
679
+ function parseGitHubRepositoryUrl(value, hosts) {
680
+ try {
681
+ const url = new URL(value);
682
+ const host = normalizeHost(url.hostname);
683
+ if (!hosts.includes(host)) {
684
+ return undefined;
685
+ }
686
+ const parts = url.pathname
687
+ .replace(/\.git$/u, "")
688
+ .split("/")
689
+ .filter((part) => part.length > 0);
690
+ const offset = host === "api.github.com" && parts[0] === "repos" ? 1 : 0;
691
+ const owner = parts[offset];
692
+ const name = parts[offset + 1];
693
+ if (owner === undefined || name === undefined) {
694
+ return undefined;
695
+ }
696
+ return { host, name, owner, url: `https://${host}/${owner}/${name}` };
697
+ }
698
+ catch {
699
+ return undefined;
700
+ }
701
+ }
702
+ function resolveRepositoryLocation(repository, hosts) {
703
+ if ("url" in repository) {
704
+ const location = parseGitHubRepositoryUrl(repository.url, hosts);
705
+ if (location === undefined) {
706
+ throw new ValidationError("Repository locator is not a supported GitHub URL", {
707
+ diagnostics: {
708
+ operation: { operation: "github.repository.resolve" },
709
+ provider: { provider: GitHubProviderId },
710
+ extra: { url: repository.url }
711
+ }
712
+ });
713
+ }
714
+ return location;
715
+ }
716
+ validateRepositoryIdentity(repository);
717
+ return {
718
+ host: hosts[0] ?? "github.com",
719
+ name: repository.name,
720
+ owner: repository.owner,
721
+ url: `https://${hosts[0] ?? "github.com"}/${repository.owner}/${repository.name}`
722
+ };
723
+ }
724
+ function toRepositoryIdentity(location) {
725
+ return deepFreeze({
726
+ name: location.name,
727
+ owner: location.owner,
728
+ provider: GitHubProviderId
729
+ });
730
+ }
731
+ function toRepositoryInfo(location) {
732
+ const identity = toRepositoryIdentity(location);
733
+ return deepFreeze({
734
+ fullName: `${identity.owner}/${identity.name}`,
735
+ identity,
736
+ metadata: { provider: GitHubProviderId, extra: { host: location.host } },
737
+ name: identity.name,
738
+ owner: { username: identity.owner },
739
+ url: location.url,
740
+ visibility: "unknown"
741
+ });
742
+ }
743
+ function toTransportRequest(request, context) {
744
+ const headers = {
745
+ accept: "application/vnd.github+json",
746
+ ...authorizationHeader(context.authentication),
747
+ ...request.headers
748
+ };
749
+ return deepFreeze({
750
+ body: request.body,
751
+ headers,
752
+ method: toTransportMethod(request.method),
753
+ metadata: {
754
+ provider: GitHubProviderId,
755
+ ...context.metadata
756
+ },
757
+ signal: request.signal,
758
+ target: request.url,
759
+ timeoutMs: request.timeoutMs
760
+ });
761
+ }
762
+ function fromTransportResponse(response) {
763
+ const data = getResponseData(response);
764
+ return deepFreeze({
765
+ data,
766
+ headers: response.headers,
767
+ status: response.status
768
+ });
769
+ }
770
+ function getResponseData(response) {
771
+ const record = response;
772
+ return record.body ?? record.data;
773
+ }
774
+ function toTransportMethod(method) {
775
+ if (method === undefined || method === "GET") {
776
+ return "read";
777
+ }
778
+ if (method === "DELETE") {
779
+ return "delete";
780
+ }
781
+ return "write";
782
+ }
783
+ function authorizationHeader(authentication) {
784
+ const credentials = authentication?.credentials;
785
+ if (credentials === undefined || credentials.kind === "anonymous" || !("token" in credentials)) {
786
+ return {};
787
+ }
788
+ return { authorization: `Bearer ${credentials.token}` };
789
+ }
790
+ function createGitHubHttpTransport() {
791
+ return deepFreeze({
792
+ async execute(request) {
793
+ const response = await executeGitHubHttpRequest(request);
794
+ return createTransportResponse(response);
795
+ }
796
+ });
797
+ }
798
+ async function executeGitHubHttpRequest(request) {
799
+ const requestUrl = toGitHubApiUrl(request.target);
800
+ const init = {
801
+ method: toHttpMethod(request.method)
802
+ };
803
+ const requestBody = toHttpRequestBody(request.body);
804
+ if (requestBody !== undefined) {
805
+ init.body = requestBody;
806
+ }
807
+ if (request.headers !== undefined) {
808
+ init.headers = request.headers;
809
+ }
810
+ if (request.signal !== undefined) {
811
+ init.signal = request.signal;
812
+ }
813
+ const response = await getFetchImplementation()(requestUrl, init);
814
+ const responseBody = await readHttpResponseBody(response);
815
+ const transportResponse = {
816
+ headers: Object.fromEntries(response.headers.entries()),
817
+ status: response.status
818
+ };
819
+ if (responseBody !== undefined) {
820
+ transportResponse.body = responseBody;
821
+ }
822
+ return transportResponse;
823
+ }
824
+ function getFetchImplementation() {
825
+ const fetchImplementation = globalThis["fetch"];
826
+ if (typeof fetchImplementation !== "function") {
827
+ throw new TransportError("GitHub provider requires a fetch-compatible runtime", {
828
+ diagnostics: {
829
+ operation: { operation: "github.transport.configure" },
830
+ provider: { provider: GitHubProviderId }
831
+ },
832
+ retryability: "Never"
833
+ });
834
+ }
835
+ return fetchImplementation.bind(globalThis);
836
+ }
837
+ function toGitHubApiUrl(target) {
838
+ if (/^https?:\/\//u.test(target)) {
839
+ return target;
840
+ }
841
+ return `https://api.github.com${target.startsWith("/") ? target : `/${target}`}`;
842
+ }
843
+ function toHttpMethod(method) {
844
+ switch (method) {
845
+ case "delete":
846
+ return "DELETE";
847
+ case "write":
848
+ return "POST";
849
+ case "read":
850
+ case "stream":
851
+ return "GET";
852
+ }
853
+ }
854
+ function toHttpRequestBody(body) {
855
+ if (body === undefined) {
856
+ return undefined;
857
+ }
858
+ if (typeof body === "string" || body instanceof Uint8Array) {
859
+ return body;
860
+ }
861
+ return JSON.stringify(body);
862
+ }
863
+ async function readHttpResponseBody(response) {
864
+ if (response.status === 204 || response.status === 205) {
865
+ return undefined;
866
+ }
867
+ const text = await response.text();
868
+ if (text.length === 0) {
869
+ return undefined;
870
+ }
871
+ const contentType = response.headers.get("content-type") ?? "";
872
+ if (contentType.includes("application/json")) {
873
+ return JSON.parse(text);
874
+ }
875
+ return text;
876
+ }
877
+ function createGitHubStatusError(status, operation, headers) {
878
+ if (status === 401) {
879
+ return new AuthenticationError("GitHub authentication failed", {
880
+ diagnostics: {
881
+ operation: { operation },
882
+ provider: { provider: GitHubProviderId, status }
883
+ },
884
+ retryability: "Never"
885
+ });
886
+ }
887
+ if (status === 403 && isRateLimitError({ headers, status })) {
888
+ return new RateLimitError("GitHub rate limit exceeded", {
889
+ diagnostics: {
890
+ operation: { operation },
891
+ provider: { provider: GitHubProviderId, status }
892
+ },
893
+ retryability: "Always"
894
+ });
895
+ }
896
+ if (status === 403) {
897
+ return new AuthorizationError("GitHub authorization failed", {
898
+ diagnostics: {
899
+ operation: { operation },
900
+ provider: { provider: GitHubProviderId, status }
901
+ },
902
+ retryability: "Never"
903
+ });
904
+ }
905
+ if (status === 404) {
906
+ return new NotFoundError("GitHub resource was not found", {
907
+ diagnostics: {
908
+ operation: { operation },
909
+ provider: { provider: GitHubProviderId, status }
910
+ },
911
+ retryability: "Never"
912
+ });
913
+ }
914
+ if (status === 409) {
915
+ return new ConflictError("GitHub reported a resource conflict", {
916
+ diagnostics: {
917
+ operation: { operation },
918
+ provider: { provider: GitHubProviderId, status }
919
+ },
920
+ retryability: "Maybe"
921
+ });
922
+ }
923
+ return new ProviderError("GitHub provider rejected the request", {
924
+ diagnostics: {
925
+ operation: { operation },
926
+ provider: { provider: GitHubProviderId, status }
927
+ },
928
+ retryability: retryabilityForStatus(status)
929
+ });
930
+ }
931
+ function createPage(items, headers) {
932
+ const link = headers?.link ?? headers?.Link;
933
+ return deepFreeze({
934
+ items,
935
+ pageInfo: parsePaginationInfo(link)
936
+ });
937
+ }
938
+ function parsePaginationInfo(link) {
939
+ if (link === undefined) {
940
+ return {
941
+ hasNextPage: false,
942
+ hasPreviousPage: false
943
+ };
944
+ }
945
+ const info = {
946
+ hasNextPage: link.includes('rel="next"'),
947
+ hasPreviousPage: link.includes('rel="prev"')
948
+ };
949
+ const next = extractPageCursor(link, "next");
950
+ const previous = extractPageCursor(link, "prev");
951
+ if (next !== undefined) {
952
+ info.endCursor = next;
953
+ }
954
+ if (previous !== undefined) {
955
+ info.startCursor = previous;
956
+ }
957
+ return info;
958
+ }
959
+ function extractPageCursor(link, relation) {
960
+ const match = new RegExp(`<[^>]*[?&]page=([^>&]+)[^>]*>;\\s*rel="${relation}"`, "u").exec(link);
961
+ return match?.[1];
962
+ }
963
+ function withPagination(url, options) {
964
+ const parameters = [];
965
+ if (options?.cursor !== undefined) {
966
+ parameters.push(`page=${encodeURIComponent(options.cursor)}`);
967
+ }
968
+ if (options?.limit !== undefined) {
969
+ parameters.push(`per_page=${encodeURIComponent(String(options.limit))}`);
970
+ }
971
+ if (parameters.length === 0) {
972
+ return url;
973
+ }
974
+ return `${url}${url.includes("?") ? "&" : "?"}${parameters.join("&")}`;
975
+ }
976
+ function decodeBlobContent(blob) {
977
+ if (blob.content instanceof Uint8Array) {
978
+ return blob.content;
979
+ }
980
+ if (blob.content === undefined) {
981
+ return new Uint8Array();
982
+ }
983
+ if (blob.encoding === "base64") {
984
+ return Buffer.from(blob.content.replace(/\s+/gu, ""), "base64");
985
+ }
986
+ return new TextEncoder().encode(blob.content);
987
+ }
988
+ function encodePathPart(value) {
989
+ return encodeURIComponent(value);
990
+ }
991
+ function encodeRepositoryPath(path) {
992
+ return path.split("/").map(encodePathPart).join("/");
993
+ }
994
+ async function runGitHubProviderOperation(operation, execute) {
995
+ try {
996
+ return await execute();
997
+ }
998
+ catch (error) {
999
+ throw mapGitHubError(error, operation);
1000
+ }
1001
+ }
1002
+ function getErrorStatus(error) {
1003
+ return getNumericProperty(error, "status") ?? getNumericProperty(error, "statusCode");
1004
+ }
1005
+ function getErrorCode(error) {
1006
+ return getStringProperty(error, "code");
1007
+ }
1008
+ function getErrorRequestId(error) {
1009
+ const headers = getHeaders(error);
1010
+ return headers?.["x-github-request-id"] ?? headers?.["x-request-id"];
1011
+ }
1012
+ function isRateLimitError(error) {
1013
+ const headers = getHeaders(error);
1014
+ return headers?.["x-ratelimit-remaining"] === "0" || getErrorCode(error) === "rate_limit";
1015
+ }
1016
+ function getHeaders(error) {
1017
+ if (error === null || typeof error !== "object" || !("headers" in error)) {
1018
+ return undefined;
1019
+ }
1020
+ const headers = error.headers;
1021
+ if (headers === null || typeof headers !== "object") {
1022
+ return undefined;
1023
+ }
1024
+ const normalized = {};
1025
+ for (const [key, value] of Object.entries(headers)) {
1026
+ if (typeof value === "string") {
1027
+ normalized[key.toLowerCase()] = value;
1028
+ }
1029
+ }
1030
+ return normalized;
1031
+ }
1032
+ function getNumericProperty(error, key) {
1033
+ if (error === null || typeof error !== "object" || !(key in error)) {
1034
+ return undefined;
1035
+ }
1036
+ const value = error[key];
1037
+ return typeof value === "number" ? value : undefined;
1038
+ }
1039
+ function getStringProperty(error, key) {
1040
+ if (error === null || typeof error !== "object" || !(key in error)) {
1041
+ return undefined;
1042
+ }
1043
+ const value = error[key];
1044
+ return typeof value === "string" ? value : undefined;
1045
+ }
1046
+ function retryabilityForStatus(status) {
1047
+ return status === 408 || status === 409 || status === 429 ? "Maybe" : "Never";
1048
+ }
1049
+ function normalizeHosts(hosts) {
1050
+ const normalized = hosts.map(normalizeHost);
1051
+ if (normalized.length === 0) {
1052
+ throw new ValidationError("GitHub provider requires at least one host", {
1053
+ diagnostics: {
1054
+ operation: { operation: "github.provider.configure" },
1055
+ provider: { provider: GitHubProviderId }
1056
+ }
1057
+ });
1058
+ }
1059
+ return [...new Set(normalized)];
1060
+ }
1061
+ function normalizeHost(host) {
1062
+ validateNonEmpty(host, "GitHub host must be a non-empty string");
1063
+ return host.toLowerCase().replace(/^www\./u, "");
1064
+ }
1065
+ function validateRepositoryIdentity(identity) {
1066
+ validateNonEmpty(identity.provider, "Repository provider must be a non-empty string");
1067
+ validateNonEmpty(identity.owner, "Repository owner must be a non-empty string");
1068
+ validateNonEmpty(identity.name, "Repository name must be a non-empty string");
1069
+ if (identity.provider !== GitHubProviderId) {
1070
+ throw new ValidationError("Repository identity does not belong to the GitHub provider", {
1071
+ diagnostics: {
1072
+ operation: { operation: "github.repository.validate" },
1073
+ provider: { provider: GitHubProviderId },
1074
+ repository: formatRepositoryDiagnostics(identity)
1075
+ }
1076
+ });
1077
+ }
1078
+ }
1079
+ function validatePath(path) {
1080
+ validateNonEmpty(path, "Repository path must be a non-empty string");
1081
+ }
1082
+ function validateOptionalPath(path) {
1083
+ if (path !== undefined) {
1084
+ validatePath(path);
1085
+ }
1086
+ }
1087
+ function validatePositiveInteger(value, message) {
1088
+ if (!Number.isInteger(value) || value <= 0) {
1089
+ throw new ValidationError(message, {
1090
+ diagnostics: {
1091
+ operation: { operation: "github.provider.validate" },
1092
+ provider: { provider: GitHubProviderId }
1093
+ }
1094
+ });
1095
+ }
1096
+ }
1097
+ function validateNonEmpty(value, message) {
1098
+ if (value.trim() === "") {
1099
+ throw new ValidationError(message, {
1100
+ diagnostics: {
1101
+ operation: { operation: "github.provider.validate" },
1102
+ provider: { provider: GitHubProviderId }
1103
+ }
1104
+ });
1105
+ }
1106
+ }
1107
+ function formatRepositoryDiagnostics(identity) {
1108
+ return {
1109
+ provider: identity.provider,
1110
+ repository: `${identity.owner}/${identity.name}`
1111
+ };
1112
+ }