@sentry/junior-github 0.114.0 → 0.116.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/SETUP.md CHANGED
@@ -116,11 +116,11 @@ githubPlugin({
116
116
  });
117
117
  ```
118
118
 
119
- Junior records these permissions as plugin capabilities. Installation-read token requests remain read-only by requesting read-capable configured permissions at `read` level and omitting GitHub permission fields that have no `read` value. Installation-write token requests intentionally omit the `permissions` field, so GitHub applies the complete permission envelope approved on the App installation. GitHub remains the source of truth for whether a permission name or level exists.
119
+ Installation-read token requests remain read-only by requesting read-capable configured permissions at `read` level and omitting GitHub permission fields that have no `read` value. Installation-write token requests intentionally omit the `permissions` field, so GitHub applies the complete permission envelope approved on the App installation. GitHub remains the source of truth for whether a permission name or level exists.
120
120
 
121
121
  GitHub App user-to-server tokens do not use OAuth scopes as their permission model. Their effective access is limited by the GitHub App's installed permissions, the app installation's repository access, and the requesting user's own GitHub access. Repository-scoped installation tokens instead use the App permission envelope and installation repository access without borrowing the requesting user's authority. GitHub returns an empty `scope` value for user-to-server tokens, so Junior cannot verify granted scopes from the token response.
122
122
 
123
- If you pass `additionalUserScopes`, Junior includes those values in the authorization URL and records the requested scope string as a local reauthorization contract. This does not expand or prove GitHub API permissions. Configure provider-enforced access in the GitHub App settings; `appPermissions` only declares capabilities and controls read-token downscoping:
123
+ If you pass `additionalUserScopes`, Junior includes those values in the authorization URL and records the requested scope string as a local reauthorization contract. This does not expand or prove GitHub API permissions. Configure provider-enforced access in the GitHub App settings; `appPermissions` only controls read-token downscoping:
124
124
 
125
125
  ```ts
126
126
  githubPlugin({
@@ -0,0 +1,90 @@
1
+ import type { PluginCredentialResult, PluginGrant, PluginProviderAccount, PluginStoredTokens, IssueCredentialHookContext } from "@sentry/junior-plugin-api";
2
+ import { type GitHubAppPermissions } from "./permissions.js";
3
+ export type JsonRecord = Record<string, unknown>;
4
+ export type GitHubGrantName = "installation-read" | "installation-write" | "user-read" | "user-write";
5
+ export type GitHubGrantReason = "github.api-read" | "github.asset-upload" | "github.git-read" | "github.graphql-read" | "github.installation-write" | "github.user-read" | "github.user-write";
6
+ export type GitHubGrant = PluginGrant & {
7
+ name: GitHubGrantName;
8
+ reason: GitHubGrantReason;
9
+ };
10
+ interface GitHubRequestParams {
11
+ body?: unknown;
12
+ method?: string;
13
+ token: string;
14
+ }
15
+ interface UserCredentialOptions {
16
+ clientIdEnv: string;
17
+ clientSecretEnv: string;
18
+ userScope?: string;
19
+ }
20
+ interface InstallationCredentialBaseOptions {
21
+ appIdEnv: string;
22
+ installationIdEnv: string;
23
+ privateKeyEnv: string;
24
+ }
25
+ type InstallationCredentialOptions = InstallationCredentialBaseOptions & ({
26
+ loadPermissions?: never;
27
+ permissions?: GitHubAppPermissions;
28
+ repositories: string[];
29
+ } | {
30
+ loadPermissions?: never;
31
+ permissions: GitHubAppPermissions;
32
+ repositories?: never;
33
+ } | {
34
+ loadPermissions: LoadInstallationReadPermissions;
35
+ permissions?: never;
36
+ repositories?: never;
37
+ });
38
+ type LoadInstallationReadPermissions = (input: {
39
+ appJwt: string;
40
+ installationId: number;
41
+ }) => Promise<Record<string, "read">>;
42
+ interface GitHubRepository {
43
+ name: string;
44
+ owner: string;
45
+ }
46
+ export declare const GITHUB_APP_ID_ENV = "GITHUB_APP_ID";
47
+ export declare const GITHUB_APP_PRIVATE_KEY_ENV = "GITHUB_APP_PRIVATE_KEY";
48
+ export declare const GITHUB_INSTALLATION_ID_ENV = "GITHUB_INSTALLATION_ID";
49
+ export declare const GITHUB_AUTH_TOKEN_ENV = "GITHUB_TOKEN";
50
+ export declare const GITHUB_AUTH_TOKEN_PLACEHOLDER = "ghp_host_managed_credential";
51
+ export declare const GITHUB_GRAPHQL_RESPONSE_BODY_LIMIT_BYTES: number;
52
+ export declare const HTTP_READ_METHODS: Set<string>;
53
+ export declare const USER_TOKEN_GRANTS: Set<string>;
54
+ export declare const CREATE_TOOL_ROUTING_GUIDANCE = "This is a Junior tool-routing denial, not a GitHub permission failure. Do not ask the user for GitHub permissions; retry with the required Junior tool.";
55
+ export declare const USER_WRITE_REQUIREMENTS: string[];
56
+ export declare class GitHubPluginSetupError extends Error {
57
+ constructor(message: string);
58
+ }
59
+ /** Return whether a provider value is a JSON object. */
60
+ export declare function isRecord(value: unknown): value is JsonRecord;
61
+ /** Read a non-empty GitHub plugin environment value. */
62
+ export declare function readEnv(name: string): string | undefined;
63
+ /** Read a required GitHub plugin environment value. */
64
+ export declare function requireEnv(name: string): string;
65
+ /** Normalize configured GitHub OAuth scopes. */
66
+ export declare function normalizeScopeList(scopes?: string[]): string[];
67
+ /** Send an authenticated request to the GitHub API. */
68
+ export declare function githubRequest(apiBase: string, path: string, params: GitHubRequestParams): Promise<unknown>;
69
+ /** Return a credential result for invalid GitHub App configuration. */
70
+ export declare function credentialUnavailable(message: string): PluginCredentialResult;
71
+ /** Parse a GitHub repository from an API or Git URL. */
72
+ export declare function githubRepositoryFromUrl(upstreamUrl: URL): GitHubRepository | undefined;
73
+ /** Build the stable lease scope for a GitHub repository. */
74
+ export declare function githubRepositoryLeaseScope(repository: GitHubRepository): string;
75
+ /** Parse the repository bound to an installation-write lease. */
76
+ export declare function githubRepositoryFromLeaseScope(leaseScope: string | undefined): GitHubRepository;
77
+ /** Resolve the GitHub account associated with stored user tokens. */
78
+ export declare function resolveUserAccount(tokens: PluginStoredTokens): Promise<PluginProviderAccount>;
79
+ /** Issue a bounded GitHub user credential for an approved grant. */
80
+ export declare function issueUserCredential(ctx: IssueCredentialHookContext, options: UserCredentialOptions): Promise<PluginCredentialResult>;
81
+ /** Issue a bounded raw token for plugin-owned GitHub API calls. */
82
+ export declare function issueInstallationToken(options: InstallationCredentialOptions): Promise<{
83
+ expiresAtMs: number;
84
+ token: string;
85
+ }>;
86
+ /** Issue a bounded GitHub App installation credential. */
87
+ export declare function issueInstallationCredential(options: InstallationCredentialOptions): Promise<PluginCredentialResult>;
88
+ /** Cache the installation's read permissions for one lease period. */
89
+ export declare function createPermissionCache(): LoadInstallationReadPermissions;
90
+ export {};
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  export declare const githubPullRequestStateSchema: z.ZodEnum<{
3
- merged: "merged";
4
3
  open: "open";
4
+ merged: "merged";
5
5
  closed_unmerged: "closed_unmerged";
6
6
  }>;
7
7
  export type GitHubPullRequestState = z.output<typeof githubPullRequestStateSchema>;
@@ -308,7 +308,7 @@ export declare const juniorGitHubPullRequests: import("drizzle-orm/pg-core").PgT
308
308
  tableName: "junior_github_pull_requests";
309
309
  dataType: "string";
310
310
  columnType: "PgText";
311
- data: "merged" | "open" | "closed_unmerged";
311
+ data: "open" | "merged" | "closed_unmerged";
312
312
  driverParam: string;
313
313
  notNull: true;
314
314
  hasDefault: false;
@@ -320,7 +320,7 @@ export declare const juniorGitHubPullRequests: import("drizzle-orm/pg-core").PgT
320
320
  identity: undefined;
321
321
  generated: undefined;
322
322
  }, {}, {
323
- $type: "merged" | "open" | "closed_unmerged";
323
+ $type: "open" | "merged" | "closed_unmerged";
324
324
  }>;
325
325
  commitComposition: import("drizzle-orm/pg-core").PgColumn<{
326
326
  name: "commit_composition";
@@ -857,7 +857,7 @@ export declare const githubSqlSchema: {
857
857
  tableName: "junior_github_pull_requests";
858
858
  dataType: "string";
859
859
  columnType: "PgText";
860
- data: "merged" | "open" | "closed_unmerged";
860
+ data: "open" | "merged" | "closed_unmerged";
861
861
  driverParam: string;
862
862
  notNull: true;
863
863
  hasDefault: false;
@@ -869,7 +869,7 @@ export declare const githubSqlSchema: {
869
869
  identity: undefined;
870
870
  generated: undefined;
871
871
  }, {}, {
872
- $type: "merged" | "open" | "closed_unmerged";
872
+ $type: "open" | "merged" | "closed_unmerged";
873
873
  }>;
874
874
  commitComposition: import("drizzle-orm/pg-core").PgColumn<{
875
875
  name: "commit_composition";
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Git identity and commit-hook setup for GitHub sandboxes.
3
+ */
4
+ import type { Actor, SandboxPrepareHookContext } from "@sentry/junior-plugin-api";
5
+ /**
6
+ * Build `Co-Authored-By` trailers crediting human run actors.
7
+ *
8
+ * `run.actors` is attribution only (see `multi-actor-runs.md`): a steerer
9
+ * without a resolvable name and email is silently omitted rather than
10
+ * denying the commit. Dedupes by identity and resolved email so the same human
11
+ * under two display profiles, or an actor matching the bot identity, only ever
12
+ * produces one line.
13
+ */
14
+ export declare function additionalActorCoauthorTrailers(args: {
15
+ actors?: Actor[];
16
+ botEmail: string;
17
+ }): string[];
18
+ /** Build the hook that replaces model-supplied commit attribution. */
19
+ export declare function prepareCommitMsgHook(): string;
20
+ /** Set one global Git option inside the prepared sandbox. */
21
+ export declare function configureGit(ctx: SandboxPrepareHookContext, key: string, value: string): Promise<void>;
package/dist/index.d.ts CHANGED
@@ -1,40 +1,2 @@
1
- import { type PluginRegistration } from "@sentry/junior-plugin-api";
2
- import { type GitHubAppPermissions } from "./permissions.js";
1
+ export { githubPlugin, type GitHubPluginOptions } from "./plugin.js";
3
2
  export type { GitHubAppPermissionLevel } from "./permissions.js";
4
- /** Configure the built-in GitHub plugin manifest and hooks. */
5
- export interface GitHubPluginOptions {
6
- /**
7
- * Extra OAuth `scope` values to request during GitHub App user authorization.
8
- *
9
- * GitHub App user tokens report empty scopes, so Junior treats this as a
10
- * local reauthorization contract only. Effective access still comes from the
11
- * app permissions, installation repositories, and requesting user's access.
12
- */
13
- additionalUserScopes?: string[];
14
- /**
15
- * GitHub App permissions Junior should expose as capabilities and downscope
16
- * to read for installation-read tokens.
17
- *
18
- * Keys may use GitHub permission names with underscores or hyphens. Junior
19
- * records these as plugin capabilities. Installation-write tokens inherit
20
- * the App installation's complete permission envelope.
21
- * GitHub remains the source of truth for whether a permission exists.
22
- */
23
- appPermissions?: GitHubAppPermissions;
24
- /** Environment variable containing the GitHub App id. */
25
- appIdEnv?: string;
26
- /** Environment variable containing Junior's Git committer email. */
27
- botEmailEnv?: string;
28
- /** Environment variable containing Junior's Git committer name. */
29
- botNameEnv?: string;
30
- /** Environment variable containing the GitHub App OAuth client id. */
31
- clientIdEnv?: string;
32
- /** Environment variable containing the GitHub App OAuth client secret. */
33
- clientSecretEnv?: string;
34
- /** Environment variable containing the GitHub App installation id. */
35
- installationIdEnv?: string;
36
- /** Environment variable containing the GitHub App private key. */
37
- privateKeyEnv?: string;
38
- }
39
- /** Register GitHub runtime hooks for repository workflows. */
40
- export declare function githubPlugin(options?: GitHubPluginOptions): PluginRegistration;
package/dist/index.js CHANGED
@@ -3,8 +3,7 @@ import {
3
3
  normalizeGitHubResourceEvents
4
4
  } from "./chunk-JDNXIBCQ.js";
5
5
 
6
- // src/index.ts
7
- import { createPrivateKey, createSign } from "crypto";
6
+ // src/plugin.ts
8
7
  import {
9
8
  defineJuniorPlugin,
10
9
  EgressPolicyDenied
@@ -65,15 +64,6 @@ function readGrantPermissions(permissions) {
65
64
  }
66
65
  return readOnly;
67
66
  }
68
- function permissionCapabilities(permissions) {
69
- if (permissions === void 0) {
70
- return void 0;
71
- }
72
- return Object.entries(permissions).map(([normalizedScope, rawLevel]) => {
73
- const scope = normalizedScope.replace(/_/g, "-");
74
- return `github.${scope}.${rawLevel}`;
75
- }).sort();
76
- }
77
67
 
78
68
  // src/tools/create-issue.ts
79
69
  import {
@@ -528,6 +518,7 @@ import { z as z2 } from "zod";
528
518
  import { subscribableResourceSchema } from "@sentry/junior-plugin-api";
529
519
  var GITHUB_PULL_REQUEST_CREATE_IDEMPOTENCY_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
530
520
  var GITHUB_PULL_REQUEST_CREATE_LOCK_TTL_MS = 6e4;
521
+ var RESOURCE_LINK_LABEL_MAX_LENGTH = 256;
531
522
  var GitHubPullRequestCreateRejectedError = class extends Error {
532
523
  status;
533
524
  constructor(message, status) {
@@ -760,6 +751,20 @@ function gitHubPullRequestToolResult(input, result) {
760
751
  });
761
752
  return { ...result, ...subscribable ? { subscribable } : {} };
762
753
  }
754
+ async function annotatePullRequest(ctx, input, result) {
755
+ const repo = parseRepo2(input.repo);
756
+ const label = `${repo.owner}/${repo.name} #${result.number}: ${nonEmptyString3(input.title, "title")}`.slice(
757
+ 0,
758
+ RESOURCE_LINK_LABEL_MAX_LENGTH
759
+ );
760
+ await ctx.annotations?.upsert({
761
+ kind: "resource_link",
762
+ key: `${repo.owner.toLowerCase()}/${repo.name.toLowerCase()}#${result.number}`,
763
+ label,
764
+ url: result.url,
765
+ status: input.draft ? "draft" : "open"
766
+ });
767
+ }
763
768
  function gitHubPullRequestStructuredResult(input, result) {
764
769
  const data = gitHubPullRequestToolResult(input, result);
765
770
  return {
@@ -789,12 +794,15 @@ function createGitHubPullRequestTool(ctx) {
789
794
  async () => {
790
795
  const state = createPullRequestState(await ctx.state.get(key));
791
796
  if (state?.status === "completed") {
797
+ const completedInput = state.input ?? parsedInput;
798
+ const completedResult = {
799
+ number: state.number,
800
+ url: state.url
801
+ };
802
+ await annotatePullRequest(ctx, completedInput, completedResult);
792
803
  return gitHubPullRequestStructuredResult(
793
- state.input ?? parsedInput,
794
- {
795
- number: state.number,
796
- url: state.url
797
- }
804
+ completedInput,
805
+ completedResult
798
806
  );
799
807
  }
800
808
  if (state?.status === "pending") {
@@ -833,6 +841,7 @@ function createGitHubPullRequestTool(ctx) {
833
841
  { cause: error }
834
842
  );
835
843
  }
844
+ await annotatePullRequest(ctx, parsedInput, result);
836
845
  return gitHubPullRequestStructuredResult(parsedInput, result);
837
846
  } catch (error) {
838
847
  if (isEgressAuthRequired2(error) || isDefinitiveGitHubPullRequestCreateRejection(error)) {
@@ -1828,10 +1837,14 @@ var costWindowSchema = z10.object({
1828
1837
  }));
1829
1838
  var repositoryCostSchema = z10.object({
1830
1839
  issueCostUsd: z10.number().nonnegative().nullable(),
1840
+ medianIssueCostUsd: z10.number().nonnegative().nullable(),
1841
+ medianPullRequestCostUsd: z10.number().nonnegative().nullable(),
1831
1842
  pullRequestCostUsd: z10.number().nonnegative().nullable(),
1832
1843
  repository: z10.string().min(1)
1833
1844
  }).strict().transform((row) => ({
1834
1845
  issueCostUsd: row.issueCostUsd ?? void 0,
1846
+ medianIssueCostUsd: row.medianIssueCostUsd ?? void 0,
1847
+ medianPullRequestCostUsd: row.medianPullRequestCostUsd ?? void 0,
1835
1848
  pullRequestCostUsd: row.pullRequestCostUsd ?? void 0,
1836
1849
  repository: row.repository
1837
1850
  }));
@@ -2048,7 +2061,8 @@ async function aggregateGitHubRepositoryCosts(args) {
2048
2061
  WITH pull_request_entities AS (
2049
2062
  SELECT
2050
2063
  ${pullRequests.repositoryFullName} AS repository,
2051
- conversation_ids.ids AS ids
2064
+ conversation_ids.ids AS ids,
2065
+ ${conversationTreeCost} AS cost_usd
2052
2066
  FROM ${pullRequests}
2053
2067
  CROSS JOIN LATERAL (
2054
2068
  SELECT ${pullRequestConversationIds} AS ids
@@ -2057,7 +2071,8 @@ async function aggregateGitHubRepositoryCosts(args) {
2057
2071
  ), issue_entities AS (
2058
2072
  SELECT
2059
2073
  ${issues.repositoryFullName} AS repository,
2060
- conversation_ids.ids AS ids
2074
+ conversation_ids.ids AS ids,
2075
+ ${conversationTreeCost} AS cost_usd
2061
2076
  FROM ${issues}
2062
2077
  CROSS JOIN LATERAL (
2063
2078
  SELECT ${issueConversationIds} AS ids
@@ -2079,7 +2094,15 @@ async function aggregateGitHubRepositoryCosts(args) {
2079
2094
  WHERE pull_request_entities.repository = repositories.repository
2080
2095
  ) AS ids
2081
2096
  ) AS conversation_ids
2082
- ), 0)::double precision AS pull_request_cost_usd
2097
+ ), 0)::double precision AS pull_request_cost_usd,
2098
+ (
2099
+ SELECT percentile_cont(0.5) WITHIN GROUP (
2100
+ ORDER BY pull_request_entities.cost_usd
2101
+ )
2102
+ FROM pull_request_entities
2103
+ WHERE pull_request_entities.repository = repositories.repository
2104
+ AND pull_request_entities.cost_usd > 0
2105
+ )::double precision AS median_pull_request_cost_usd
2083
2106
  FROM repositories
2084
2107
  ), issue_totals AS (
2085
2108
  SELECT
@@ -2093,21 +2116,32 @@ async function aggregateGitHubRepositoryCosts(args) {
2093
2116
  WHERE issue_entities.repository = repositories.repository
2094
2117
  ) AS ids
2095
2118
  ) AS conversation_ids
2096
- ), 0)::double precision AS issue_cost_usd
2119
+ ), 0)::double precision AS issue_cost_usd,
2120
+ (
2121
+ SELECT percentile_cont(0.5) WITHIN GROUP (
2122
+ ORDER BY issue_entities.cost_usd
2123
+ )
2124
+ FROM issue_entities
2125
+ WHERE issue_entities.repository = repositories.repository
2126
+ AND issue_entities.cost_usd > 0
2127
+ )::double precision AS median_issue_cost_usd
2097
2128
  FROM repositories
2098
2129
  )
2099
2130
  SELECT
2100
2131
  repositories.repository AS "repository",
2101
2132
  coalesce(pull_request_totals.pull_request_cost_usd, 0)::double precision
2102
2133
  AS "pullRequestCostUsd",
2134
+ pull_request_totals.median_pull_request_cost_usd
2135
+ AS "medianPullRequestCostUsd",
2103
2136
  coalesce(issue_totals.issue_cost_usd, 0)::double precision
2104
- AS "issueCostUsd"
2137
+ AS "issueCostUsd",
2138
+ issue_totals.median_issue_cost_usd AS "medianIssueCostUsd"
2105
2139
  FROM repositories
2106
2140
  LEFT JOIN pull_request_totals
2107
2141
  ON pull_request_totals.repository = repositories.repository
2108
2142
  LEFT JOIN issue_totals
2109
2143
  ON issue_totals.repository = repositories.repository
2110
- ORDER BY "pullRequestCostUsd" DESC, "issueCostUsd" DESC, "repository" ASC
2144
+ ORDER BY "repository" ASC
2111
2145
  `);
2112
2146
  return z10.array(repositoryCostSchema).parse(queryRows(result));
2113
2147
  }
@@ -2548,7 +2582,7 @@ async function buildGitHubOutcomeReport(args) {
2548
2582
  { key: "merged", label: "Merged" },
2549
2583
  { key: "closed", label: "Closed unmerged" },
2550
2584
  { key: "mergeRate", label: "Closure merge rate" },
2551
- { key: "cost", label: "Cost" }
2585
+ { key: "medianCost", label: "Median cost" }
2552
2586
  ],
2553
2587
  records: repositories.map(({ repository, ...stats }) => ({
2554
2588
  id: repository,
@@ -2559,8 +2593,8 @@ async function buildGitHubOutcomeReport(args) {
2559
2593
  closed: String(stats.closed),
2560
2594
  juniorOnly: String(stats.juniorOnly),
2561
2595
  mergeRate: formatPercent(stats.mergeRate),
2562
- cost: formatCostUsd(
2563
- repositoryCostByName.get(repository)?.pullRequestCostUsd ?? 0
2596
+ medianCost: formatCostUsd(
2597
+ repositoryCostByName.get(repository)?.medianPullRequestCostUsd
2564
2598
  )
2565
2599
  }
2566
2600
  }))
@@ -2575,7 +2609,7 @@ async function buildGitHubOutcomeReport(args) {
2575
2609
  { key: "duplicate", label: "Duplicate" },
2576
2610
  { key: "notPlanned", label: "Not planned" },
2577
2611
  { key: "unknown", label: "Unknown reason" },
2578
- { key: "cost", label: "Cost" }
2612
+ { key: "medianCost", label: "Median cost" }
2579
2613
  ],
2580
2614
  records: issueRepositories.map(({ repository, ...stats }) => ({
2581
2615
  id: repository,
@@ -2586,8 +2620,8 @@ async function buildGitHubOutcomeReport(args) {
2586
2620
  duplicate: String(stats.closedDuplicate),
2587
2621
  notPlanned: String(stats.closedNotPlanned),
2588
2622
  unknown: String(stats.closedUnknown),
2589
- cost: formatCostUsd(
2590
- repositoryCostByName.get(repository)?.issueCostUsd ?? 0
2623
+ medianCost: formatCostUsd(
2624
+ repositoryCostByName.get(repository)?.medianIssueCostUsd
2591
2625
  )
2592
2626
  }
2593
2627
  }))
@@ -2649,87 +2683,7 @@ async function classifyGitHubPullRequestCommitComposition(args) {
2649
2683
  return foundCommit ? "junior_only" : void 0;
2650
2684
  }
2651
2685
 
2652
- // src/index.ts
2653
- var GITHUB_APP_ID_ENV = "GITHUB_APP_ID";
2654
- var GITHUB_APP_PRIVATE_KEY_ENV = "GITHUB_APP_PRIVATE_KEY";
2655
- var GITHUB_INSTALLATION_ID_ENV = "GITHUB_INSTALLATION_ID";
2656
- var GITHUB_AUTH_TOKEN_ENV = "GITHUB_TOKEN";
2657
- var GITHUB_AUTH_TOKEN_PLACEHOLDER = "ghp_host_managed_credential";
2658
- var MAX_LEASE_MS = 60 * 60 * 1e3;
2659
- var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
2660
- var USER_REFRESH_TIMEOUT_MS = 2e4;
2661
- var GITHUB_GRAPHQL_RESPONSE_BODY_LIMIT_BYTES = 64 * 1024;
2662
- var HTTP_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
2663
- var USER_TOKEN_GRANTS = /* @__PURE__ */ new Set(["user-read", "user-write"]);
2664
- var CREATE_TOOL_ROUTING_GUIDANCE = "This is a Junior tool-routing denial, not a GitHub permission failure. Do not ask the user for GitHub permissions; retry with the required Junior tool.";
2665
- var USER_WRITE_REQUIREMENTS = [
2666
- "requesting GitHub user permission to perform this operation"
2667
- ];
2668
- var GITHUB_CREDENTIAL_DOMAINS = ["api.github.com", "github.com"];
2669
- var GITHUB_ASSET_UPLOAD_CREDENTIAL_DOMAINS = [
2670
- ...GITHUB_CREDENTIAL_DOMAINS,
2671
- "uploads.github.com"
2672
- ];
2673
- var GitHubUserRefreshRejectedError = class extends Error {
2674
- constructor(message) {
2675
- super(message);
2676
- this.name = "GitHubUserRefreshRejectedError";
2677
- }
2678
- };
2679
- var GitHubRequestError = class extends Error {
2680
- status;
2681
- constructor(message, status) {
2682
- super(message);
2683
- this.name = "GitHubRequestError";
2684
- this.status = status;
2685
- }
2686
- };
2687
- var GitHubPluginSetupError = class extends Error {
2688
- constructor(message) {
2689
- super(message);
2690
- this.name = "GitHubPluginSetupError";
2691
- }
2692
- };
2693
- function isRecord(value) {
2694
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
2695
- }
2696
- function readEnv(name) {
2697
- const value = process.env[name];
2698
- if (typeof value !== "string") {
2699
- return void 0;
2700
- }
2701
- const trimmed = value.trim();
2702
- return trimmed ? trimmed : void 0;
2703
- }
2704
- function requireEnv(name) {
2705
- const value = readEnv(name);
2706
- if (!value) {
2707
- throw new GitHubPluginSetupError(`Missing ${name}`);
2708
- }
2709
- return value;
2710
- }
2711
- function normalizeScopeList(scopes) {
2712
- return [
2713
- ...new Set(
2714
- (scopes ?? []).flatMap((scope) => String(scope).split(/\s+/)).map((scope) => scope.trim()).filter(Boolean)
2715
- )
2716
- ].sort();
2717
- }
2718
- function normalizeOAuthScope(scope) {
2719
- const normalized = normalizeScopeList(scope ? [scope] : []);
2720
- return normalized.length ? normalized.join(" ") : void 0;
2721
- }
2722
- function hasRequiredOAuthScope(storedScope, requiredScope) {
2723
- const required = normalizeScopeList(requiredScope ? [requiredScope] : []);
2724
- if (required.length === 0) {
2725
- return true;
2726
- }
2727
- const stored = new Set(normalizeScopeList(storedScope ? [storedScope] : []));
2728
- if (stored.size === 0) {
2729
- return false;
2730
- }
2731
- return required.every((scope) => stored.has(scope));
2732
- }
2686
+ // src/git-config.ts
2733
2687
  function cleanIdentityPart(value) {
2734
2688
  return String(value ?? "").replaceAll("\n", " ").replaceAll("\r", " ").replace(/[<>]/g, "").trim();
2735
2689
  }
@@ -2937,6 +2891,92 @@ async function configureGit(ctx, key, value) {
2937
2891
  );
2938
2892
  }
2939
2893
  }
2894
+
2895
+ // src/credential-support.ts
2896
+ import { createPrivateKey, createSign } from "crypto";
2897
+ var GITHUB_APP_ID_ENV = "GITHUB_APP_ID";
2898
+ var GITHUB_APP_PRIVATE_KEY_ENV = "GITHUB_APP_PRIVATE_KEY";
2899
+ var GITHUB_INSTALLATION_ID_ENV = "GITHUB_INSTALLATION_ID";
2900
+ var GITHUB_AUTH_TOKEN_ENV = "GITHUB_TOKEN";
2901
+ var GITHUB_AUTH_TOKEN_PLACEHOLDER = "ghp_host_managed_credential";
2902
+ var MAX_LEASE_MS = 60 * 60 * 1e3;
2903
+ var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
2904
+ var USER_REFRESH_TIMEOUT_MS = 2e4;
2905
+ var GITHUB_GRAPHQL_RESPONSE_BODY_LIMIT_BYTES = 64 * 1024;
2906
+ var HTTP_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
2907
+ var USER_TOKEN_GRANTS = /* @__PURE__ */ new Set(["user-read", "user-write"]);
2908
+ var CREATE_TOOL_ROUTING_GUIDANCE = "This is a Junior tool-routing denial, not a GitHub permission failure. Do not ask the user for GitHub permissions; retry with the required Junior tool.";
2909
+ var USER_WRITE_REQUIREMENTS = [
2910
+ "requesting GitHub user permission to perform this operation"
2911
+ ];
2912
+ var GITHUB_CREDENTIAL_DOMAINS = ["api.github.com", "github.com"];
2913
+ var GITHUB_ASSET_UPLOAD_CREDENTIAL_DOMAINS = [
2914
+ ...GITHUB_CREDENTIAL_DOMAINS,
2915
+ "uploads.github.com"
2916
+ ];
2917
+ var GitHubUserRefreshRejectedError = class extends Error {
2918
+ constructor(message) {
2919
+ super(message);
2920
+ this.name = "GitHubUserRefreshRejectedError";
2921
+ }
2922
+ };
2923
+ var GitHubRequestError = class extends Error {
2924
+ status;
2925
+ constructor(message, status) {
2926
+ super(message);
2927
+ this.name = "GitHubRequestError";
2928
+ this.status = status;
2929
+ }
2930
+ };
2931
+ var GitHubPluginSetupError = class extends Error {
2932
+ constructor(message) {
2933
+ super(message);
2934
+ this.name = "GitHubPluginSetupError";
2935
+ }
2936
+ };
2937
+ function isRecord(value) {
2938
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
2939
+ }
2940
+ function readEnv(name) {
2941
+ const value = process.env[name];
2942
+ if (typeof value !== "string") {
2943
+ return void 0;
2944
+ }
2945
+ const trimmed = value.trim();
2946
+ return trimmed ? trimmed : void 0;
2947
+ }
2948
+ function requireEnv(name) {
2949
+ const value = readEnv(name);
2950
+ if (!value) {
2951
+ throw new GitHubPluginSetupError(`Missing ${name}`);
2952
+ }
2953
+ return value;
2954
+ }
2955
+ function normalizeScopeList(scopes) {
2956
+ return [
2957
+ ...new Set(
2958
+ (scopes ?? []).flatMap((scope) => String(scope).split(/\s+/)).map((scope) => scope.trim()).filter(Boolean)
2959
+ )
2960
+ ].sort();
2961
+ }
2962
+ function normalizeOAuthScope(scope) {
2963
+ const normalized = normalizeScopeList(scope ? [scope] : []);
2964
+ return normalized.length ? normalized.join(" ") : void 0;
2965
+ }
2966
+ function hasRequiredOAuthScope(storedScope, requiredScope) {
2967
+ const required = normalizeScopeList(requiredScope ? [requiredScope] : []);
2968
+ if (required.length === 0) {
2969
+ return true;
2970
+ }
2971
+ const stored = new Set(normalizeScopeList(storedScope ? [storedScope] : []));
2972
+ if (stored.size === 0) {
2973
+ return false;
2974
+ }
2975
+ return required.every((scope) => stored.has(scope));
2976
+ }
2977
+ function isGitHubApiUrl(upstreamUrl) {
2978
+ return upstreamUrl.hostname.toLowerCase() === "api.github.com";
2979
+ }
2940
2980
  function base64Url(input) {
2941
2981
  return Buffer.from(input).toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
2942
2982
  }
@@ -3437,6 +3477,8 @@ function createPermissionCache() {
3437
3477
  return await pending;
3438
3478
  };
3439
3479
  }
3480
+
3481
+ // src/plugin.ts
3440
3482
  function githubSmartHttpAccess(upstreamUrl) {
3441
3483
  const pathname = upstreamUrl.pathname.toLowerCase();
3442
3484
  const service = upstreamUrl.searchParams.get("service")?.toLowerCase();
@@ -3455,14 +3497,14 @@ function githubSmartHttpAccess(upstreamUrl) {
3455
3497
  function isGitHubGraphqlUrl(upstreamUrl) {
3456
3498
  return upstreamUrl.hostname.toLowerCase() === "api.github.com" && upstreamUrl.pathname.toLowerCase().endsWith("/graphql");
3457
3499
  }
3458
- function isGitHubApiUrl(upstreamUrl) {
3500
+ function isGitHubApiUrl2(upstreamUrl) {
3459
3501
  return upstreamUrl.hostname.toLowerCase() === "api.github.com";
3460
3502
  }
3461
3503
  function isGitHubAssetUploadRequest(method, upstreamUrl) {
3462
3504
  return method === "POST" && upstreamUrl.hostname.toLowerCase() === "uploads.github.com" && upstreamUrl.pathname === "/user-attachments/assets";
3463
3505
  }
3464
3506
  function githubUserReadReason(method, upstreamUrl) {
3465
- if (method !== "GET" || !isGitHubApiUrl(upstreamUrl)) {
3507
+ if (method !== "GET" || !isGitHubApiUrl2(upstreamUrl)) {
3466
3508
  return void 0;
3467
3509
  }
3468
3510
  return upstreamUrl.pathname.toLowerCase() === "/user" ? "github.user-read" : void 0;
@@ -3590,7 +3632,7 @@ function shouldInspectGitHubGraphqlResponse(ctx) {
3590
3632
  }
3591
3633
  function githubApiWriteGrantName(method, upstreamUrl) {
3592
3634
  const pathname = upstreamUrl.pathname.toLowerCase();
3593
- if (!isGitHubApiUrl(upstreamUrl)) {
3635
+ if (!isGitHubApiUrl2(upstreamUrl)) {
3594
3636
  return void 0;
3595
3637
  }
3596
3638
  if (method === "POST" && /^\/repos\/[^/]+\/[^/]+\/actions\/workflows\/[^/]+\/dispatches$/.test(
@@ -3632,10 +3674,10 @@ function githubApiWriteGrantName(method, upstreamUrl) {
3632
3674
  return void 0;
3633
3675
  }
3634
3676
  function isGitHubIssueCreateRestRequest(method, upstreamUrl) {
3635
- return method === "POST" && isGitHubApiUrl(upstreamUrl) && /^\/repos\/[^/]+\/[^/]+\/issues$/.test(upstreamUrl.pathname.toLowerCase());
3677
+ return method === "POST" && isGitHubApiUrl2(upstreamUrl) && /^\/repos\/[^/]+\/[^/]+\/issues$/.test(upstreamUrl.pathname.toLowerCase());
3636
3678
  }
3637
3679
  function isGitHubPullCreateRestRequest(method, upstreamUrl) {
3638
- return method === "POST" && isGitHubApiUrl(upstreamUrl) && /^\/repos\/[^/]+\/[^/]+\/pulls$/.test(upstreamUrl.pathname.toLowerCase());
3680
+ return method === "POST" && isGitHubApiUrl2(upstreamUrl) && /^\/repos\/[^/]+\/[^/]+\/pulls$/.test(upstreamUrl.pathname.toLowerCase());
3639
3681
  }
3640
3682
  function isGitHubIssueCreateGraphqlMutation(method, upstreamUrl, bodyText) {
3641
3683
  if (method !== "POST" || !isGitHubGraphqlUrl(upstreamUrl)) {
@@ -3794,7 +3836,6 @@ function githubPlugin(options = {}) {
3794
3836
  const declaredAppPermissions = normalizePermissions(options.appPermissions);
3795
3837
  const declaredReadPermissions = declaredAppPermissions ? readGrantPermissions(declaredAppPermissions) : void 0;
3796
3838
  const loadReadPermissions = createPermissionCache();
3797
- const appCapabilities = permissionCapabilities(declaredAppPermissions);
3798
3839
  const userScopes = normalizeScopeList(options.additionalUserScopes);
3799
3840
  const userScope = userScopes.length ? userScopes.join(" ") : void 0;
3800
3841
  return defineJuniorPlugin({
@@ -3803,7 +3844,6 @@ function githubPlugin(options = {}) {
3803
3844
  name: "github",
3804
3845
  displayName: "GitHub",
3805
3846
  description: "GitHub issue, pull request, and repository workflows via GitHub App",
3806
- ...appCapabilities ? { capabilities: appCapabilities } : {},
3807
3847
  configKeys: ["org", "repo"],
3808
3848
  domains: ["api.github.com", "github.com", "uploads.github.com"],
3809
3849
  envVars: {
@@ -21,14 +21,20 @@ declare const costWindowSchema: z.ZodPipe<z.ZodObject<{
21
21
  }>>;
22
22
  declare const repositoryCostSchema: z.ZodPipe<z.ZodObject<{
23
23
  issueCostUsd: z.ZodNullable<z.ZodNumber>;
24
+ medianIssueCostUsd: z.ZodNullable<z.ZodNumber>;
25
+ medianPullRequestCostUsd: z.ZodNullable<z.ZodNumber>;
24
26
  pullRequestCostUsd: z.ZodNullable<z.ZodNumber>;
25
27
  repository: z.ZodString;
26
28
  }, z.core.$strict>, z.ZodTransform<{
27
29
  issueCostUsd: number | undefined;
30
+ medianIssueCostUsd: number | undefined;
31
+ medianPullRequestCostUsd: number | undefined;
28
32
  pullRequestCostUsd: number | undefined;
29
33
  repository: string;
30
34
  }, {
31
35
  issueCostUsd: number | null;
36
+ medianIssueCostUsd: number | null;
37
+ medianPullRequestCostUsd: number | null;
32
38
  pullRequestCostUsd: number | null;
33
39
  repository: string;
34
40
  }>>;
@@ -1,8 +1,6 @@
1
1
  export type GitHubAppPermissionLevel = "admin" | "read" | "write";
2
2
  export type GitHubAppPermissions = Record<string, GitHubAppPermissionLevel>;
3
- /** Validate the configured GitHub App capability and read-token declaration. */
3
+ /** Validate the configured GitHub App read-token permission declaration. */
4
4
  export declare function normalizePermissions(permissions: GitHubAppPermissions | undefined): GitHubAppPermissions | undefined;
5
5
  /** Build the read-only installation-token permission body. */
6
6
  export declare function readGrantPermissions(permissions: Record<string, unknown> | undefined): Record<string, "read">;
7
- /** Expose configured permissions as plugin capabilities for host policy checks. */
8
- export declare function permissionCapabilities(permissions: GitHubAppPermissions | undefined): string[] | undefined;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * GitHub plugin runtime boundary.
3
+ *
4
+ * This module composes GitHub hooks and owns GitHub egress policy.
5
+ */
6
+ import { type PluginRegistration } from "@sentry/junior-plugin-api";
7
+ import { type GitHubAppPermissions } from "./permissions.js";
8
+ /** Configure the built-in GitHub plugin manifest and hooks. */
9
+ export interface GitHubPluginOptions {
10
+ /**
11
+ * Extra OAuth `scope` values to request during GitHub App user authorization.
12
+ *
13
+ * GitHub App user tokens report empty scopes, so Junior treats this as a
14
+ * local reauthorization contract only. Effective access still comes from the
15
+ * app permissions, installation repositories, and requesting user's access.
16
+ */
17
+ additionalUserScopes?: string[];
18
+ /**
19
+ * GitHub App permissions Junior should downscope to read for
20
+ * installation-read tokens.
21
+ *
22
+ * Keys may use GitHub permission names with underscores or hyphens.
23
+ * Installation-write tokens inherit the App installation's complete
24
+ * permission envelope. GitHub remains the source of truth for whether a
25
+ * permission exists.
26
+ */
27
+ appPermissions?: GitHubAppPermissions;
28
+ /** Environment variable containing the GitHub App id. */
29
+ appIdEnv?: string;
30
+ /** Environment variable containing Junior's Git committer email. */
31
+ botEmailEnv?: string;
32
+ /** Environment variable containing Junior's Git committer name. */
33
+ botNameEnv?: string;
34
+ /** Environment variable containing the GitHub App OAuth client id. */
35
+ clientIdEnv?: string;
36
+ /** Environment variable containing the GitHub App OAuth client secret. */
37
+ clientSecretEnv?: string;
38
+ /** Environment variable containing the GitHub App installation id. */
39
+ installationIdEnv?: string;
40
+ /** Environment variable containing the GitHub App private key. */
41
+ privateKeyEnv?: string;
42
+ }
43
+ /** Register GitHub runtime hooks for repository workflows. */
44
+ export declare function githubPlugin(options?: GitHubPluginOptions): PluginRegistration;
@@ -15,8 +15,8 @@ declare const githubPullRequestOutcomeInputSchema: z.ZodObject<{
15
15
  repositoryFullName: z.ZodString;
16
16
  repositoryId: z.ZodString;
17
17
  state: z.ZodEnum<{
18
- merged: "merged";
19
18
  open: "open";
19
+ merged: "merged";
20
20
  closed_unmerged: "closed_unmerged";
21
21
  }>;
22
22
  updatedAt: z.ZodDate;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-github",
3
- "version": "0.114.0",
3
+ "version": "0.116.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -31,7 +31,7 @@
31
31
  "@sinclair/typebox": "^0.34.49",
32
32
  "drizzle-orm": "^0.45.2",
33
33
  "zod": "^4.4.3",
34
- "@sentry/junior-plugin-api": "0.114.0"
34
+ "@sentry/junior-plugin-api": "0.116.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^25.9.1",
@@ -5,7 +5,8 @@
5
5
  - `intercom/2x-skills@59213af`, `plugins/pr-tools/skills/attach-github-assets/SKILL.md` — upstream runtime intent and supported use cases; primary source; high confidence; MIT.
6
6
  - `intercom/2x-skills@59213af`, `plugins/pr-tools/skills/attach-github-assets/scripts/upload.sh` — upstream endpoint, MIME mapping, and response contract; primary implementation source; high confidence; MIT.
7
7
  - `intercom/2x-skills@59213af`, `plugins/pr-tools/LICENSE` — upstream copyright and license notice; authoritative legal source; high confidence.
8
- - `packages/junior-github/src/index.ts` — local GitHub credential and egress boundary; authoritative local source; high confidence.
8
+ - `packages/junior-github/src/credential-support.ts` — local GitHub credential boundary; authoritative local source; high confidence.
9
+ - `packages/junior-github/src/plugin.ts` — local GitHub egress boundary; authoritative local source; high confidence.
9
10
  - `packages/junior-github/skills/github-code/SKILL.md` — local repository targeting and credential guidance; authoritative local convention; high confidence.
10
11
  - GitHub REST API documentation — no documented user-attachment upload operation found; official source; medium confidence for absence.
11
12