@sentry/junior-github 0.114.0 → 0.115.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,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 {};
@@ -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
@@ -1828,10 +1827,14 @@ var costWindowSchema = z10.object({
1828
1827
  }));
1829
1828
  var repositoryCostSchema = z10.object({
1830
1829
  issueCostUsd: z10.number().nonnegative().nullable(),
1830
+ medianIssueCostUsd: z10.number().nonnegative().nullable(),
1831
+ medianPullRequestCostUsd: z10.number().nonnegative().nullable(),
1831
1832
  pullRequestCostUsd: z10.number().nonnegative().nullable(),
1832
1833
  repository: z10.string().min(1)
1833
1834
  }).strict().transform((row) => ({
1834
1835
  issueCostUsd: row.issueCostUsd ?? void 0,
1836
+ medianIssueCostUsd: row.medianIssueCostUsd ?? void 0,
1837
+ medianPullRequestCostUsd: row.medianPullRequestCostUsd ?? void 0,
1835
1838
  pullRequestCostUsd: row.pullRequestCostUsd ?? void 0,
1836
1839
  repository: row.repository
1837
1840
  }));
@@ -2048,7 +2051,8 @@ async function aggregateGitHubRepositoryCosts(args) {
2048
2051
  WITH pull_request_entities AS (
2049
2052
  SELECT
2050
2053
  ${pullRequests.repositoryFullName} AS repository,
2051
- conversation_ids.ids AS ids
2054
+ conversation_ids.ids AS ids,
2055
+ ${conversationTreeCost} AS cost_usd
2052
2056
  FROM ${pullRequests}
2053
2057
  CROSS JOIN LATERAL (
2054
2058
  SELECT ${pullRequestConversationIds} AS ids
@@ -2057,7 +2061,8 @@ async function aggregateGitHubRepositoryCosts(args) {
2057
2061
  ), issue_entities AS (
2058
2062
  SELECT
2059
2063
  ${issues.repositoryFullName} AS repository,
2060
- conversation_ids.ids AS ids
2064
+ conversation_ids.ids AS ids,
2065
+ ${conversationTreeCost} AS cost_usd
2061
2066
  FROM ${issues}
2062
2067
  CROSS JOIN LATERAL (
2063
2068
  SELECT ${issueConversationIds} AS ids
@@ -2079,7 +2084,15 @@ async function aggregateGitHubRepositoryCosts(args) {
2079
2084
  WHERE pull_request_entities.repository = repositories.repository
2080
2085
  ) AS ids
2081
2086
  ) AS conversation_ids
2082
- ), 0)::double precision AS pull_request_cost_usd
2087
+ ), 0)::double precision AS pull_request_cost_usd,
2088
+ (
2089
+ SELECT percentile_cont(0.5) WITHIN GROUP (
2090
+ ORDER BY pull_request_entities.cost_usd
2091
+ )
2092
+ FROM pull_request_entities
2093
+ WHERE pull_request_entities.repository = repositories.repository
2094
+ AND pull_request_entities.cost_usd > 0
2095
+ )::double precision AS median_pull_request_cost_usd
2083
2096
  FROM repositories
2084
2097
  ), issue_totals AS (
2085
2098
  SELECT
@@ -2093,21 +2106,32 @@ async function aggregateGitHubRepositoryCosts(args) {
2093
2106
  WHERE issue_entities.repository = repositories.repository
2094
2107
  ) AS ids
2095
2108
  ) AS conversation_ids
2096
- ), 0)::double precision AS issue_cost_usd
2109
+ ), 0)::double precision AS issue_cost_usd,
2110
+ (
2111
+ SELECT percentile_cont(0.5) WITHIN GROUP (
2112
+ ORDER BY issue_entities.cost_usd
2113
+ )
2114
+ FROM issue_entities
2115
+ WHERE issue_entities.repository = repositories.repository
2116
+ AND issue_entities.cost_usd > 0
2117
+ )::double precision AS median_issue_cost_usd
2097
2118
  FROM repositories
2098
2119
  )
2099
2120
  SELECT
2100
2121
  repositories.repository AS "repository",
2101
2122
  coalesce(pull_request_totals.pull_request_cost_usd, 0)::double precision
2102
2123
  AS "pullRequestCostUsd",
2124
+ pull_request_totals.median_pull_request_cost_usd
2125
+ AS "medianPullRequestCostUsd",
2103
2126
  coalesce(issue_totals.issue_cost_usd, 0)::double precision
2104
- AS "issueCostUsd"
2127
+ AS "issueCostUsd",
2128
+ issue_totals.median_issue_cost_usd AS "medianIssueCostUsd"
2105
2129
  FROM repositories
2106
2130
  LEFT JOIN pull_request_totals
2107
2131
  ON pull_request_totals.repository = repositories.repository
2108
2132
  LEFT JOIN issue_totals
2109
2133
  ON issue_totals.repository = repositories.repository
2110
- ORDER BY "pullRequestCostUsd" DESC, "issueCostUsd" DESC, "repository" ASC
2134
+ ORDER BY "repository" ASC
2111
2135
  `);
2112
2136
  return z10.array(repositoryCostSchema).parse(queryRows(result));
2113
2137
  }
@@ -2548,7 +2572,7 @@ async function buildGitHubOutcomeReport(args) {
2548
2572
  { key: "merged", label: "Merged" },
2549
2573
  { key: "closed", label: "Closed unmerged" },
2550
2574
  { key: "mergeRate", label: "Closure merge rate" },
2551
- { key: "cost", label: "Cost" }
2575
+ { key: "medianCost", label: "Median cost" }
2552
2576
  ],
2553
2577
  records: repositories.map(({ repository, ...stats }) => ({
2554
2578
  id: repository,
@@ -2559,8 +2583,8 @@ async function buildGitHubOutcomeReport(args) {
2559
2583
  closed: String(stats.closed),
2560
2584
  juniorOnly: String(stats.juniorOnly),
2561
2585
  mergeRate: formatPercent(stats.mergeRate),
2562
- cost: formatCostUsd(
2563
- repositoryCostByName.get(repository)?.pullRequestCostUsd ?? 0
2586
+ medianCost: formatCostUsd(
2587
+ repositoryCostByName.get(repository)?.medianPullRequestCostUsd
2564
2588
  )
2565
2589
  }
2566
2590
  }))
@@ -2575,7 +2599,7 @@ async function buildGitHubOutcomeReport(args) {
2575
2599
  { key: "duplicate", label: "Duplicate" },
2576
2600
  { key: "notPlanned", label: "Not planned" },
2577
2601
  { key: "unknown", label: "Unknown reason" },
2578
- { key: "cost", label: "Cost" }
2602
+ { key: "medianCost", label: "Median cost" }
2579
2603
  ],
2580
2604
  records: issueRepositories.map(({ repository, ...stats }) => ({
2581
2605
  id: repository,
@@ -2586,8 +2610,8 @@ async function buildGitHubOutcomeReport(args) {
2586
2610
  duplicate: String(stats.closedDuplicate),
2587
2611
  notPlanned: String(stats.closedNotPlanned),
2588
2612
  unknown: String(stats.closedUnknown),
2589
- cost: formatCostUsd(
2590
- repositoryCostByName.get(repository)?.issueCostUsd ?? 0
2613
+ medianCost: formatCostUsd(
2614
+ repositoryCostByName.get(repository)?.medianIssueCostUsd
2591
2615
  )
2592
2616
  }
2593
2617
  }))
@@ -2649,87 +2673,7 @@ async function classifyGitHubPullRequestCommitComposition(args) {
2649
2673
  return foundCommit ? "junior_only" : void 0;
2650
2674
  }
2651
2675
 
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
- }
2676
+ // src/git-config.ts
2733
2677
  function cleanIdentityPart(value) {
2734
2678
  return String(value ?? "").replaceAll("\n", " ").replaceAll("\r", " ").replace(/[<>]/g, "").trim();
2735
2679
  }
@@ -2937,6 +2881,92 @@ async function configureGit(ctx, key, value) {
2937
2881
  );
2938
2882
  }
2939
2883
  }
2884
+
2885
+ // src/credential-support.ts
2886
+ import { createPrivateKey, createSign } from "crypto";
2887
+ var GITHUB_APP_ID_ENV = "GITHUB_APP_ID";
2888
+ var GITHUB_APP_PRIVATE_KEY_ENV = "GITHUB_APP_PRIVATE_KEY";
2889
+ var GITHUB_INSTALLATION_ID_ENV = "GITHUB_INSTALLATION_ID";
2890
+ var GITHUB_AUTH_TOKEN_ENV = "GITHUB_TOKEN";
2891
+ var GITHUB_AUTH_TOKEN_PLACEHOLDER = "ghp_host_managed_credential";
2892
+ var MAX_LEASE_MS = 60 * 60 * 1e3;
2893
+ var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
2894
+ var USER_REFRESH_TIMEOUT_MS = 2e4;
2895
+ var GITHUB_GRAPHQL_RESPONSE_BODY_LIMIT_BYTES = 64 * 1024;
2896
+ var HTTP_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
2897
+ var USER_TOKEN_GRANTS = /* @__PURE__ */ new Set(["user-read", "user-write"]);
2898
+ 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.";
2899
+ var USER_WRITE_REQUIREMENTS = [
2900
+ "requesting GitHub user permission to perform this operation"
2901
+ ];
2902
+ var GITHUB_CREDENTIAL_DOMAINS = ["api.github.com", "github.com"];
2903
+ var GITHUB_ASSET_UPLOAD_CREDENTIAL_DOMAINS = [
2904
+ ...GITHUB_CREDENTIAL_DOMAINS,
2905
+ "uploads.github.com"
2906
+ ];
2907
+ var GitHubUserRefreshRejectedError = class extends Error {
2908
+ constructor(message) {
2909
+ super(message);
2910
+ this.name = "GitHubUserRefreshRejectedError";
2911
+ }
2912
+ };
2913
+ var GitHubRequestError = class extends Error {
2914
+ status;
2915
+ constructor(message, status) {
2916
+ super(message);
2917
+ this.name = "GitHubRequestError";
2918
+ this.status = status;
2919
+ }
2920
+ };
2921
+ var GitHubPluginSetupError = class extends Error {
2922
+ constructor(message) {
2923
+ super(message);
2924
+ this.name = "GitHubPluginSetupError";
2925
+ }
2926
+ };
2927
+ function isRecord(value) {
2928
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
2929
+ }
2930
+ function readEnv(name) {
2931
+ const value = process.env[name];
2932
+ if (typeof value !== "string") {
2933
+ return void 0;
2934
+ }
2935
+ const trimmed = value.trim();
2936
+ return trimmed ? trimmed : void 0;
2937
+ }
2938
+ function requireEnv(name) {
2939
+ const value = readEnv(name);
2940
+ if (!value) {
2941
+ throw new GitHubPluginSetupError(`Missing ${name}`);
2942
+ }
2943
+ return value;
2944
+ }
2945
+ function normalizeScopeList(scopes) {
2946
+ return [
2947
+ ...new Set(
2948
+ (scopes ?? []).flatMap((scope) => String(scope).split(/\s+/)).map((scope) => scope.trim()).filter(Boolean)
2949
+ )
2950
+ ].sort();
2951
+ }
2952
+ function normalizeOAuthScope(scope) {
2953
+ const normalized = normalizeScopeList(scope ? [scope] : []);
2954
+ return normalized.length ? normalized.join(" ") : void 0;
2955
+ }
2956
+ function hasRequiredOAuthScope(storedScope, requiredScope) {
2957
+ const required = normalizeScopeList(requiredScope ? [requiredScope] : []);
2958
+ if (required.length === 0) {
2959
+ return true;
2960
+ }
2961
+ const stored = new Set(normalizeScopeList(storedScope ? [storedScope] : []));
2962
+ if (stored.size === 0) {
2963
+ return false;
2964
+ }
2965
+ return required.every((scope) => stored.has(scope));
2966
+ }
2967
+ function isGitHubApiUrl(upstreamUrl) {
2968
+ return upstreamUrl.hostname.toLowerCase() === "api.github.com";
2969
+ }
2940
2970
  function base64Url(input) {
2941
2971
  return Buffer.from(input).toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
2942
2972
  }
@@ -3437,6 +3467,8 @@ function createPermissionCache() {
3437
3467
  return await pending;
3438
3468
  };
3439
3469
  }
3470
+
3471
+ // src/plugin.ts
3440
3472
  function githubSmartHttpAccess(upstreamUrl) {
3441
3473
  const pathname = upstreamUrl.pathname.toLowerCase();
3442
3474
  const service = upstreamUrl.searchParams.get("service")?.toLowerCase();
@@ -3455,14 +3487,14 @@ function githubSmartHttpAccess(upstreamUrl) {
3455
3487
  function isGitHubGraphqlUrl(upstreamUrl) {
3456
3488
  return upstreamUrl.hostname.toLowerCase() === "api.github.com" && upstreamUrl.pathname.toLowerCase().endsWith("/graphql");
3457
3489
  }
3458
- function isGitHubApiUrl(upstreamUrl) {
3490
+ function isGitHubApiUrl2(upstreamUrl) {
3459
3491
  return upstreamUrl.hostname.toLowerCase() === "api.github.com";
3460
3492
  }
3461
3493
  function isGitHubAssetUploadRequest(method, upstreamUrl) {
3462
3494
  return method === "POST" && upstreamUrl.hostname.toLowerCase() === "uploads.github.com" && upstreamUrl.pathname === "/user-attachments/assets";
3463
3495
  }
3464
3496
  function githubUserReadReason(method, upstreamUrl) {
3465
- if (method !== "GET" || !isGitHubApiUrl(upstreamUrl)) {
3497
+ if (method !== "GET" || !isGitHubApiUrl2(upstreamUrl)) {
3466
3498
  return void 0;
3467
3499
  }
3468
3500
  return upstreamUrl.pathname.toLowerCase() === "/user" ? "github.user-read" : void 0;
@@ -3590,7 +3622,7 @@ function shouldInspectGitHubGraphqlResponse(ctx) {
3590
3622
  }
3591
3623
  function githubApiWriteGrantName(method, upstreamUrl) {
3592
3624
  const pathname = upstreamUrl.pathname.toLowerCase();
3593
- if (!isGitHubApiUrl(upstreamUrl)) {
3625
+ if (!isGitHubApiUrl2(upstreamUrl)) {
3594
3626
  return void 0;
3595
3627
  }
3596
3628
  if (method === "POST" && /^\/repos\/[^/]+\/[^/]+\/actions\/workflows\/[^/]+\/dispatches$/.test(
@@ -3632,10 +3664,10 @@ function githubApiWriteGrantName(method, upstreamUrl) {
3632
3664
  return void 0;
3633
3665
  }
3634
3666
  function isGitHubIssueCreateRestRequest(method, upstreamUrl) {
3635
- return method === "POST" && isGitHubApiUrl(upstreamUrl) && /^\/repos\/[^/]+\/[^/]+\/issues$/.test(upstreamUrl.pathname.toLowerCase());
3667
+ return method === "POST" && isGitHubApiUrl2(upstreamUrl) && /^\/repos\/[^/]+\/[^/]+\/issues$/.test(upstreamUrl.pathname.toLowerCase());
3636
3668
  }
3637
3669
  function isGitHubPullCreateRestRequest(method, upstreamUrl) {
3638
- return method === "POST" && isGitHubApiUrl(upstreamUrl) && /^\/repos\/[^/]+\/[^/]+\/pulls$/.test(upstreamUrl.pathname.toLowerCase());
3670
+ return method === "POST" && isGitHubApiUrl2(upstreamUrl) && /^\/repos\/[^/]+\/[^/]+\/pulls$/.test(upstreamUrl.pathname.toLowerCase());
3639
3671
  }
3640
3672
  function isGitHubIssueCreateGraphqlMutation(method, upstreamUrl, bodyText) {
3641
3673
  if (method !== "POST" || !isGitHubGraphqlUrl(upstreamUrl)) {
@@ -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
  }>>;
@@ -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 expose as capabilities and downscope
20
+ * to read for installation-read tokens.
21
+ *
22
+ * Keys may use GitHub permission names with underscores or hyphens. Junior
23
+ * records these as plugin capabilities. Installation-write tokens inherit
24
+ * the App installation's complete permission envelope.
25
+ * GitHub remains the source of truth for whether a 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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-github",
3
- "version": "0.114.0",
3
+ "version": "0.115.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.115.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