@sentry/junior 0.3.0 → 0.4.1

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.
@@ -7,7 +7,7 @@ import {
7
7
  import {
8
8
  discoverInstalledPluginPackageContent,
9
9
  discoverProjectRoots
10
- } from "./chunk-SP6LV35L.js";
10
+ } from "./chunk-Z5E25LRN.js";
11
11
 
12
12
  // src/chat/config.ts
13
13
  var MIN_AGENT_TURN_TIMEOUT_MS = 10 * 1e3;
@@ -554,7 +554,9 @@ function getPrivateKey(envName) {
554
554
  );
555
555
  }
556
556
  if (key.asymmetricKeyType !== "rsa") {
557
- throw new Error(`Invalid ${envName}: GitHub App signing requires an RSA private key`);
557
+ throw new Error(
558
+ `Invalid ${envName}: GitHub App signing requires an RSA private key`
559
+ );
558
560
  }
559
561
  return key;
560
562
  }
@@ -609,7 +611,14 @@ function capabilityToPermissions(capability, pluginName) {
609
611
  function createGitHubAppBroker(manifest, credentials) {
610
612
  const tokenCache = /* @__PURE__ */ new Map();
611
613
  const provider = manifest.name;
612
- const { apiDomains, authTokenEnv, appIdEnv, privateKeyEnv, installationIdEnv } = credentials;
614
+ const {
615
+ apiDomains,
616
+ apiHeaders,
617
+ authTokenEnv,
618
+ appIdEnv,
619
+ privateKeyEnv,
620
+ installationIdEnv
621
+ } = credentials;
613
622
  const apiBase = `https://${apiDomains[0]}`;
614
623
  const placeholder = resolveAuthTokenPlaceholder(credentials);
615
624
  return {
@@ -640,6 +649,7 @@ function createGitHubAppBroker(manifest, credentials) {
640
649
  headerTransforms: apiDomains.map((domain) => ({
641
650
  domain,
642
651
  headers: {
652
+ ...apiHeaders ?? {},
643
653
  Authorization: `Bearer ${cached.token}`
644
654
  }
645
655
  })),
@@ -659,17 +669,16 @@ function createGitHubAppBroker(manifest, credentials) {
659
669
  if (repositoryName) {
660
670
  tokenRequestBody.repositories = [repositoryName];
661
671
  }
662
- const accessTokenResponse = await githubRequest(
663
- apiBase,
664
- `/app/installations/${installationId}/access_tokens`,
665
- {
666
- method: "POST",
667
- token: appJwt,
668
- body: tokenRequestBody
669
- }
670
- );
672
+ const accessTokenResponse = await githubRequest(apiBase, `/app/installations/${installationId}/access_tokens`, {
673
+ method: "POST",
674
+ token: appJwt,
675
+ body: tokenRequestBody
676
+ });
671
677
  const providerExpiresAtMs = Date.parse(accessTokenResponse.expires_at);
672
- const expiresAtMs = Math.min(providerExpiresAtMs, Date.now() + MAX_LEASE_MS);
678
+ const expiresAtMs = Math.min(
679
+ providerExpiresAtMs,
680
+ Date.now() + MAX_LEASE_MS
681
+ );
673
682
  tokenCache.set(cacheKey, {
674
683
  installationId,
675
684
  token: accessTokenResponse.token,
@@ -683,6 +692,7 @@ function createGitHubAppBroker(manifest, credentials) {
683
692
  headerTransforms: apiDomains.map((domain) => ({
684
693
  domain,
685
694
  headers: {
695
+ ...apiHeaders ?? {},
686
696
  Authorization: `Bearer ${accessTokenResponse.token}`
687
697
  }
688
698
  })),
@@ -710,6 +720,70 @@ var CredentialUnavailableError = class extends Error {
710
720
  }
711
721
  };
712
722
 
723
+ // src/chat/plugins/oauth-request.ts
724
+ var DEFAULT_TOKEN_CONTENT_TYPE = "application/x-www-form-urlencoded";
725
+ function requireNonEmptyTokenField(data, field) {
726
+ const value = data[field];
727
+ if (typeof value !== "string" || !value.trim()) {
728
+ throw new Error(`OAuth token response missing ${field}`);
729
+ }
730
+ return value;
731
+ }
732
+ function contentTypeToBody(contentType, payload) {
733
+ const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase();
734
+ if (!mediaType || mediaType === DEFAULT_TOKEN_CONTENT_TYPE) {
735
+ return new URLSearchParams(payload);
736
+ }
737
+ if (mediaType === "application/json" || mediaType.endsWith("+json")) {
738
+ return JSON.stringify(payload);
739
+ }
740
+ throw new Error(`Unsupported OAuth token Content-Type: ${contentType}`);
741
+ }
742
+ function buildOAuthTokenRequest(input) {
743
+ const headers = new Headers({ Accept: "application/json" });
744
+ for (const [name, value] of Object.entries(input.tokenExtraHeaders ?? {})) {
745
+ headers.set(name, value);
746
+ }
747
+ if (!headers.has("Content-Type")) {
748
+ headers.set("Content-Type", DEFAULT_TOKEN_CONTENT_TYPE);
749
+ }
750
+ const payload = { ...input.payload };
751
+ if (input.tokenAuthMethod === "basic") {
752
+ headers.set(
753
+ "Authorization",
754
+ `Basic ${Buffer.from(`${input.clientId}:${input.clientSecret}`).toString("base64")}`
755
+ );
756
+ } else {
757
+ payload.client_id = input.clientId;
758
+ payload.client_secret = input.clientSecret;
759
+ }
760
+ const contentType = headers.get("Content-Type") ?? DEFAULT_TOKEN_CONTENT_TYPE;
761
+ const serializedHeaders = {};
762
+ headers.forEach((value, key) => {
763
+ serializedHeaders[key] = value;
764
+ });
765
+ return {
766
+ headers: serializedHeaders,
767
+ body: contentTypeToBody(contentType, payload)
768
+ };
769
+ }
770
+ function parseOAuthTokenResponse(data) {
771
+ const accessToken = requireNonEmptyTokenField(data, "access_token");
772
+ const refreshToken = requireNonEmptyTokenField(data, "refresh_token");
773
+ const expiresIn = data.expires_in;
774
+ if (expiresIn === void 0) {
775
+ return { accessToken, refreshToken };
776
+ }
777
+ if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || expiresIn <= 0) {
778
+ throw new Error("OAuth token response returned invalid expires_in");
779
+ }
780
+ return {
781
+ accessToken,
782
+ refreshToken,
783
+ expiresAt: Date.now() + expiresIn * 1e3
784
+ };
785
+ }
786
+
713
787
  // src/chat/plugins/oauth-bearer-broker.ts
714
788
  var MAX_LEASE_MS2 = 60 * 60 * 1e3;
715
789
  var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
@@ -717,35 +791,38 @@ async function refreshAccessToken(refreshToken, oauth) {
717
791
  const clientId = process.env[oauth.clientIdEnv]?.trim();
718
792
  const clientSecret = process.env[oauth.clientSecretEnv]?.trim();
719
793
  if (!clientId || !clientSecret) {
720
- throw new Error(`Missing ${oauth.clientIdEnv} or ${oauth.clientSecretEnv} for token refresh`);
794
+ throw new Error(
795
+ `Missing ${oauth.clientIdEnv} or ${oauth.clientSecretEnv} for token refresh`
796
+ );
721
797
  }
798
+ const request = buildOAuthTokenRequest({
799
+ clientId,
800
+ clientSecret,
801
+ payload: {
802
+ grant_type: "refresh_token",
803
+ refresh_token: refreshToken
804
+ },
805
+ tokenAuthMethod: oauth.tokenAuthMethod,
806
+ tokenExtraHeaders: oauth.tokenExtraHeaders
807
+ });
722
808
  const response = await fetch(oauth.tokenEndpoint, {
723
809
  method: "POST",
724
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
725
- body: new URLSearchParams({
726
- grant_type: "refresh_token",
727
- refresh_token: refreshToken,
728
- client_id: clientId,
729
- client_secret: clientSecret
730
- })
810
+ headers: request.headers,
811
+ body: request.body
731
812
  });
732
813
  if (!response.ok) {
733
814
  throw new Error(`Token refresh failed: ${response.status}`);
734
815
  }
735
816
  const data = await response.json();
736
- if (!data.access_token || !data.refresh_token || typeof data.expires_in !== "number") {
737
- throw new Error("Token refresh returned malformed response");
738
- }
739
- return {
740
- accessToken: data.access_token,
741
- refreshToken: data.refresh_token,
742
- expiresIn: data.expires_in
743
- };
817
+ return parseOAuthTokenResponse(data);
818
+ }
819
+ function getLeaseExpiry(expiresAt) {
820
+ return expiresAt ? Math.min(expiresAt, Date.now() + MAX_LEASE_MS2) : Date.now() + MAX_LEASE_MS2;
744
821
  }
745
822
  function createOAuthBearerBroker(manifest, credentials, deps) {
746
823
  const provider = manifest.name;
747
824
  const supportedCapabilities = new Set(manifest.capabilities);
748
- const { apiDomains, authTokenEnv } = credentials;
825
+ const { apiDomains, apiHeaders, authTokenEnv } = credentials;
749
826
  const authTokenPlaceholder = resolveAuthTokenPlaceholder(credentials);
750
827
  function buildLease(token, capability, expiresAtMs, reason) {
751
828
  return {
@@ -755,7 +832,7 @@ function createOAuthBearerBroker(manifest, credentials, deps) {
755
832
  env: { [authTokenEnv]: authTokenPlaceholder },
756
833
  headerTransforms: apiDomains.map((domain) => ({
757
834
  domain,
758
- headers: { Authorization: `Bearer ${token}` }
835
+ headers: { ...apiHeaders ?? {}, Authorization: `Bearer ${token}` }
759
836
  })),
760
837
  expiresAt: new Date(expiresAtMs).toISOString(),
761
838
  metadata: { reason }
@@ -764,27 +841,58 @@ function createOAuthBearerBroker(manifest, credentials, deps) {
764
841
  return {
765
842
  async issue(input) {
766
843
  if (!supportedCapabilities.has(input.capability)) {
767
- throw new Error(`Unsupported ${provider} capability: ${input.capability}`);
844
+ throw new Error(
845
+ `Unsupported ${provider} capability: ${input.capability}`
846
+ );
847
+ }
848
+ const envToken = process.env[authTokenEnv]?.trim();
849
+ const oauth = manifest.oauth;
850
+ if (!oauth) {
851
+ if (envToken) {
852
+ return buildLease(
853
+ envToken,
854
+ input.capability,
855
+ Date.now() + MAX_LEASE_MS2,
856
+ input.reason
857
+ );
858
+ }
859
+ throw new CredentialUnavailableError(
860
+ provider,
861
+ `No ${provider} credentials available.`
862
+ );
768
863
  }
769
- if (input.requesterId && deps.userTokenStore) {
770
- const stored = await deps.userTokenStore.get(input.requesterId, provider);
864
+ if (input.requesterId) {
865
+ const stored = await deps.userTokenStore.get(
866
+ input.requesterId,
867
+ provider
868
+ );
771
869
  if (stored) {
772
870
  const now = Date.now();
773
- if (stored.expiresAt - now < REFRESH_BUFFER_MS && stored.refreshToken && manifest.oauth) {
871
+ if (stored.expiresAt !== void 0 && stored.expiresAt - now < REFRESH_BUFFER_MS) {
774
872
  try {
775
- const refreshed = await refreshAccessToken(stored.refreshToken, manifest.oauth);
776
- const expiresAt = Date.now() + refreshed.expiresIn * 1e3;
777
- await deps.userTokenStore.set(input.requesterId, provider, {
778
- accessToken: refreshed.accessToken,
779
- refreshToken: refreshed.refreshToken,
780
- expiresAt
781
- });
782
- const leaseExpiry = Math.min(expiresAt, Date.now() + MAX_LEASE_MS2);
783
- return buildLease(refreshed.accessToken, input.capability, leaseExpiry, input.reason);
873
+ const refreshed = await refreshAccessToken(
874
+ stored.refreshToken,
875
+ oauth
876
+ );
877
+ await deps.userTokenStore.set(
878
+ input.requesterId,
879
+ provider,
880
+ refreshed
881
+ );
882
+ return buildLease(
883
+ refreshed.accessToken,
884
+ input.capability,
885
+ getLeaseExpiry(refreshed.expiresAt),
886
+ input.reason
887
+ );
784
888
  } catch {
785
889
  if (stored.expiresAt > Date.now()) {
786
- const leaseExpiry = Math.min(stored.expiresAt, Date.now() + MAX_LEASE_MS2);
787
- return buildLease(stored.accessToken, input.capability, leaseExpiry, input.reason);
890
+ return buildLease(
891
+ stored.accessToken,
892
+ input.capability,
893
+ getLeaseExpiry(stored.expiresAt),
894
+ input.reason
895
+ );
788
896
  }
789
897
  throw new CredentialUnavailableError(
790
898
  provider,
@@ -792,9 +900,13 @@ function createOAuthBearerBroker(manifest, credentials, deps) {
792
900
  );
793
901
  }
794
902
  }
795
- if (stored.expiresAt > Date.now()) {
796
- const leaseExpiry = Math.min(stored.expiresAt, Date.now() + MAX_LEASE_MS2);
797
- return buildLease(stored.accessToken, input.capability, leaseExpiry, input.reason);
903
+ if (stored.expiresAt === void 0 || stored.expiresAt > Date.now()) {
904
+ return buildLease(
905
+ stored.accessToken,
906
+ input.capability,
907
+ getLeaseExpiry(stored.expiresAt),
908
+ input.reason
909
+ );
798
910
  }
799
911
  throw new CredentialUnavailableError(
800
912
  provider,
@@ -806,10 +918,13 @@ function createOAuthBearerBroker(manifest, credentials, deps) {
806
918
  `No ${provider} credentials available.`
807
919
  );
808
920
  }
809
- const envToken = process.env[authTokenEnv]?.trim();
810
921
  if (envToken) {
811
- const expiresAtMs = Date.now() + MAX_LEASE_MS2;
812
- return buildLease(envToken, input.capability, expiresAtMs, input.reason);
922
+ return buildLease(
923
+ envToken,
924
+ input.capability,
925
+ getLeaseExpiry(),
926
+ input.reason
927
+ );
813
928
  }
814
929
  throw new CredentialUnavailableError(
815
930
  provider,
@@ -826,6 +941,15 @@ var SHORT_CONFIG_KEY_RE = /^[a-z0-9]+(\.[a-z0-9-]+)*$/;
826
941
  var AUTH_TOKEN_ENV_RE = /^[A-Z][A-Z0-9_]*$/;
827
942
  var API_DOMAIN_RE = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
828
943
  var RUNTIME_POSTINSTALL_CMD_RE = /^[A-Za-z0-9._/-]+$/;
944
+ var RESERVED_AUTHORIZE_PARAM_KEYS = /* @__PURE__ */ new Set([
945
+ "client_id",
946
+ "scope",
947
+ "state",
948
+ "redirect_uri",
949
+ "response_type"
950
+ ]);
951
+ var FORBIDDEN_API_HEADER_NAMES = /* @__PURE__ */ new Set(["authorization"]);
952
+ var FORBIDDEN_TOKEN_HEADER_NAMES = /* @__PURE__ */ new Set(["authorization"]);
829
953
  function toRecord(value, errorMessage) {
830
954
  if (!value || typeof value !== "object" || Array.isArray(value)) {
831
955
  throw new Error(errorMessage);
@@ -840,14 +964,24 @@ function requireStringField(record, field, errorMessage) {
840
964
  return value.trim();
841
965
  }
842
966
  function requireEnvVarField(record, field, pluginName) {
843
- const value = requireStringField(record, field, `Plugin ${pluginName} ${field} must be a non-empty string`);
967
+ const value = requireStringField(
968
+ record,
969
+ field,
970
+ `Plugin ${pluginName} ${field} must be a non-empty string`
971
+ );
844
972
  if (!AUTH_TOKEN_ENV_RE.test(value)) {
845
- throw new Error(`Plugin ${pluginName} ${field} must be an uppercase env var name`);
973
+ throw new Error(
974
+ `Plugin ${pluginName} ${field} must be an uppercase env var name`
975
+ );
846
976
  }
847
977
  return value;
848
978
  }
849
979
  function requireHttpsUrlField(record, field, pluginName) {
850
- const value = requireStringField(record, field, `Plugin ${pluginName} oauth.${field} must be a non-empty string`);
980
+ const value = requireStringField(
981
+ record,
982
+ field,
983
+ `Plugin ${pluginName} oauth.${field} must be a non-empty string`
984
+ );
851
985
  let parsed;
852
986
  try {
853
987
  parsed = new URL(value);
@@ -862,26 +996,79 @@ function requireHttpsUrlField(record, field, pluginName) {
862
996
  function normalizeApiDomain(rawDomain, name) {
863
997
  const domain = typeof rawDomain === "string" ? rawDomain.trim().toLowerCase() : "";
864
998
  if (!domain) {
865
- throw new Error(`Plugin ${name} credentials.api-domains entries must be non-empty strings`);
999
+ throw new Error(
1000
+ `Plugin ${name} credentials.api-domains entries must be non-empty strings`
1001
+ );
866
1002
  }
867
1003
  if (!API_DOMAIN_RE.test(domain)) {
868
- throw new Error(`Plugin ${name} credentials.api-domains entries must be valid domain names`);
1004
+ throw new Error(
1005
+ `Plugin ${name} credentials.api-domains entries must be valid domain names`
1006
+ );
869
1007
  }
870
1008
  return domain;
871
1009
  }
1010
+ function parseStringMap(data, errorLabel, options = {}) {
1011
+ if (data === void 0) {
1012
+ return void 0;
1013
+ }
1014
+ const record = toRecord(
1015
+ data,
1016
+ `${errorLabel} must be an object when provided`
1017
+ );
1018
+ const entries = Object.entries(record);
1019
+ if (entries.length === 0) {
1020
+ return void 0;
1021
+ }
1022
+ const result = {};
1023
+ const seen = /* @__PURE__ */ new Set();
1024
+ for (const [rawKey, rawValue] of entries) {
1025
+ const key = rawKey.trim();
1026
+ if (!key) {
1027
+ throw new Error(`${errorLabel} keys must be non-empty strings`);
1028
+ }
1029
+ if (typeof rawValue !== "string" || !rawValue.trim()) {
1030
+ throw new Error(`${errorLabel}.${key} must be a non-empty string`);
1031
+ }
1032
+ const normalizedKey = key.toLowerCase();
1033
+ if (options.reservedKeys?.has(normalizedKey)) {
1034
+ throw new Error(`${errorLabel}.${key} is reserved by the runtime`);
1035
+ }
1036
+ if (options.forbiddenKeys?.has(normalizedKey)) {
1037
+ throw new Error(`${errorLabel}.${key} is not allowed`);
1038
+ }
1039
+ if (seen.has(normalizedKey)) {
1040
+ throw new Error(`${errorLabel}.${key} is duplicated`);
1041
+ }
1042
+ seen.add(normalizedKey);
1043
+ result[key] = rawValue.trim();
1044
+ }
1045
+ return Object.keys(result).length > 0 ? result : void 0;
1046
+ }
872
1047
  function parseBaseCredentialFields(data, name) {
873
1048
  const rawDomains = data["api-domains"];
874
1049
  if (!Array.isArray(rawDomains) || rawDomains.length === 0) {
875
- throw new Error(`Plugin ${name} credentials.api-domains must be a non-empty array of strings`);
1050
+ throw new Error(
1051
+ `Plugin ${name} credentials.api-domains must be a non-empty array of strings`
1052
+ );
876
1053
  }
877
- const apiDomains = rawDomains.map((rawDomain) => normalizeApiDomain(rawDomain, name));
1054
+ const apiDomains = rawDomains.map(
1055
+ (rawDomain) => normalizeApiDomain(rawDomain, name)
1056
+ );
1057
+ const apiHeaders = parseStringMap(
1058
+ data["api-headers"],
1059
+ `Plugin ${name} credentials.api-headers`,
1060
+ { forbiddenKeys: FORBIDDEN_API_HEADER_NAMES }
1061
+ );
878
1062
  const authTokenEnv = requireEnvVarField(data, "auth-token-env", name);
879
1063
  const authTokenPlaceholderRaw = data["auth-token-placeholder"];
880
1064
  if (authTokenPlaceholderRaw !== void 0 && (typeof authTokenPlaceholderRaw !== "string" || !authTokenPlaceholderRaw.trim())) {
881
- throw new Error(`Plugin ${name} credentials.auth-token-placeholder must be a non-empty string when provided`);
1065
+ throw new Error(
1066
+ `Plugin ${name} credentials.auth-token-placeholder must be a non-empty string when provided`
1067
+ );
882
1068
  }
883
1069
  return {
884
1070
  apiDomains,
1071
+ ...apiHeaders ? { apiHeaders } : {},
885
1072
  authTokenEnv,
886
1073
  ...typeof authTokenPlaceholderRaw === "string" ? { authTokenPlaceholder: authTokenPlaceholderRaw.trim() } : {}
887
1074
  };
@@ -896,8 +1083,18 @@ function parseCredentials(data, name) {
896
1083
  const base = parseBaseCredentialFields(data, name);
897
1084
  const appIdEnv = requireEnvVarField(data, "app-id-env", name);
898
1085
  const privateKeyEnv = requireEnvVarField(data, "private-key-env", name);
899
- const installationIdEnv = requireEnvVarField(data, "installation-id-env", name);
900
- return { type: "github-app", ...base, appIdEnv, privateKeyEnv, installationIdEnv };
1086
+ const installationIdEnv = requireEnvVarField(
1087
+ data,
1088
+ "installation-id-env",
1089
+ name
1090
+ );
1091
+ return {
1092
+ type: "github-app",
1093
+ ...base,
1094
+ appIdEnv,
1095
+ privateKeyEnv,
1096
+ installationIdEnv
1097
+ };
901
1098
  }
902
1099
  throw new Error(`Plugin ${name} has unsupported credentials.type: "${type}"`);
903
1100
  }
@@ -912,7 +1109,9 @@ function parseRuntimeDependencies(data, name) {
912
1109
  const seen = /* @__PURE__ */ new Set();
913
1110
  for (const entry of data) {
914
1111
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
915
- throw new Error(`Plugin ${name} runtime-dependencies entries must be objects`);
1112
+ throw new Error(
1113
+ `Plugin ${name} runtime-dependencies entries must be objects`
1114
+ );
916
1115
  }
917
1116
  const record = entry;
918
1117
  const type = record.type;
@@ -921,20 +1120,28 @@ function parseRuntimeDependencies(data, name) {
921
1120
  const version = record.version;
922
1121
  const sha256 = record.sha256;
923
1122
  if (typeof type !== "string" || type !== "npm" && type !== "system") {
924
- throw new Error(`Plugin ${name} runtime dependency type must be "npm" or "system"`);
1123
+ throw new Error(
1124
+ `Plugin ${name} runtime dependency type must be "npm" or "system"`
1125
+ );
925
1126
  }
926
1127
  const normalizedPackage = typeof packageName === "string" ? packageName.trim() : "";
927
1128
  const normalizedUrl = typeof packageUrl === "string" ? packageUrl.trim() : "";
928
1129
  if (type === "npm") {
929
1130
  if (!normalizedPackage) {
930
- throw new Error(`Plugin ${name} runtime dependency package must be a non-empty string`);
1131
+ throw new Error(
1132
+ `Plugin ${name} runtime dependency package must be a non-empty string`
1133
+ );
931
1134
  }
932
1135
  if (packageUrl !== void 0 || sha256 !== void 0) {
933
- throw new Error(`Plugin ${name} npm runtime dependencies must only include package/version fields`);
1136
+ throw new Error(
1137
+ `Plugin ${name} npm runtime dependencies must only include package/version fields`
1138
+ );
934
1139
  }
935
1140
  const normalizedVersion = typeof version === "string" ? version.trim() : "latest";
936
1141
  if (!normalizedVersion) {
937
- throw new Error(`Plugin ${name} runtime dependency version must be a non-empty string when provided`);
1142
+ throw new Error(
1143
+ `Plugin ${name} runtime dependency version must be a non-empty string when provided`
1144
+ );
938
1145
  }
939
1146
  const dedupeKey2 = `${type}:${normalizedPackage}:${normalizedVersion}`;
940
1147
  if (seen.has(dedupeKey2)) {
@@ -949,17 +1156,25 @@ function parseRuntimeDependencies(data, name) {
949
1156
  continue;
950
1157
  }
951
1158
  if (version !== void 0) {
952
- throw new Error(`Plugin ${name} system runtime dependencies must not include a version`);
1159
+ throw new Error(
1160
+ `Plugin ${name} system runtime dependencies must not include a version`
1161
+ );
953
1162
  }
954
1163
  if (normalizedPackage && normalizedUrl) {
955
- throw new Error(`Plugin ${name} system runtime dependencies must specify either package or url, not both`);
1164
+ throw new Error(
1165
+ `Plugin ${name} system runtime dependencies must specify either package or url, not both`
1166
+ );
956
1167
  }
957
1168
  if (!normalizedPackage && !normalizedUrl) {
958
- throw new Error(`Plugin ${name} system runtime dependencies must specify package or url`);
1169
+ throw new Error(
1170
+ `Plugin ${name} system runtime dependencies must specify package or url`
1171
+ );
959
1172
  }
960
1173
  if (normalizedPackage) {
961
1174
  if (sha256 !== void 0) {
962
- throw new Error(`Plugin ${name} system runtime dependency package entries must not include sha256`);
1175
+ throw new Error(
1176
+ `Plugin ${name} system runtime dependency package entries must not include sha256`
1177
+ );
963
1178
  }
964
1179
  const dedupeKey2 = `${type}:package:${normalizedPackage}`;
965
1180
  if (seen.has(dedupeKey2)) {
@@ -973,11 +1188,15 @@ function parseRuntimeDependencies(data, name) {
973
1188
  continue;
974
1189
  }
975
1190
  if (!/^https:\/\//i.test(normalizedUrl)) {
976
- throw new Error(`Plugin ${name} system runtime dependency url must be an https URL`);
1191
+ throw new Error(
1192
+ `Plugin ${name} system runtime dependency url must be an https URL`
1193
+ );
977
1194
  }
978
1195
  const normalizedSha256 = typeof sha256 === "string" ? sha256.trim().toLowerCase() : "";
979
1196
  if (!/^[a-f0-9]{64}$/.test(normalizedSha256)) {
980
- throw new Error(`Plugin ${name} system runtime dependency url entries must include a valid sha256`);
1197
+ throw new Error(
1198
+ `Plugin ${name} system runtime dependency url entries must include a valid sha256`
1199
+ );
981
1200
  }
982
1201
  const dedupeKey = `${type}:url:${normalizedUrl}:${normalizedSha256}`;
983
1202
  if (seen.has(dedupeKey)) {
@@ -1002,12 +1221,16 @@ function parseRuntimePostinstall(data, name) {
1002
1221
  const parsed = [];
1003
1222
  for (const entry of data) {
1004
1223
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
1005
- throw new Error(`Plugin ${name} runtime-postinstall entries must be objects`);
1224
+ throw new Error(
1225
+ `Plugin ${name} runtime-postinstall entries must be objects`
1226
+ );
1006
1227
  }
1007
1228
  const record = entry;
1008
1229
  const cmd = typeof record.cmd === "string" ? record.cmd.trim() : "";
1009
1230
  if (!cmd) {
1010
- throw new Error(`Plugin ${name} runtime-postinstall cmd must be a non-empty string`);
1231
+ throw new Error(
1232
+ `Plugin ${name} runtime-postinstall cmd must be a non-empty string`
1233
+ );
1011
1234
  }
1012
1235
  if (!RUNTIME_POSTINSTALL_CMD_RE.test(cmd)) {
1013
1236
  throw new Error(
@@ -1016,11 +1239,15 @@ function parseRuntimePostinstall(data, name) {
1016
1239
  }
1017
1240
  const argsRaw = record.args;
1018
1241
  if (argsRaw !== void 0 && (!Array.isArray(argsRaw) || !argsRaw.every((arg) => typeof arg === "string"))) {
1019
- throw new Error(`Plugin ${name} runtime-postinstall args must be an array of strings when provided`);
1242
+ throw new Error(
1243
+ `Plugin ${name} runtime-postinstall args must be an array of strings when provided`
1244
+ );
1020
1245
  }
1021
1246
  const sudoRaw = record.sudo;
1022
1247
  if (sudoRaw !== void 0 && typeof sudoRaw !== "boolean") {
1023
- throw new Error(`Plugin ${name} runtime-postinstall sudo must be a boolean when provided`);
1248
+ throw new Error(
1249
+ `Plugin ${name} runtime-postinstall sudo must be a boolean when provided`
1250
+ );
1024
1251
  }
1025
1252
  const normalizedArgs = Array.isArray(argsRaw) ? argsRaw.map((arg) => arg.trim()).filter((arg) => arg.length > 0) : void 0;
1026
1253
  parsed.push({
@@ -1032,7 +1259,10 @@ function parseRuntimePostinstall(data, name) {
1032
1259
  return parsed.length > 0 ? parsed : void 0;
1033
1260
  }
1034
1261
  function parseManifest(raw, dir) {
1035
- const data = toRecord(parseYaml(raw), `Invalid plugin manifest in ${dir}: expected an object`);
1262
+ const data = toRecord(
1263
+ parseYaml(raw),
1264
+ `Invalid plugin manifest in ${dir}: expected an object`
1265
+ );
1036
1266
  const rawName = data.name;
1037
1267
  if (typeof rawName !== "string" || !PLUGIN_NAME_RE.test(rawName)) {
1038
1268
  throw new Error(`Invalid plugin name in ${dir}: "${rawName}"`);
@@ -1045,7 +1275,9 @@ function parseManifest(raw, dir) {
1045
1275
  const description = rawDescription;
1046
1276
  const rawCapabilities = data.capabilities;
1047
1277
  if (rawCapabilities !== void 0 && !Array.isArray(rawCapabilities)) {
1048
- throw new Error(`Plugin ${name} capabilities must be an array when provided`);
1278
+ throw new Error(
1279
+ `Plugin ${name} capabilities must be an array when provided`
1280
+ );
1049
1281
  }
1050
1282
  const capabilities = [];
1051
1283
  for (const cap of rawCapabilities ?? []) {
@@ -1056,7 +1288,9 @@ function parseManifest(raw, dir) {
1056
1288
  }
1057
1289
  const rawConfigKeys = data["config-keys"];
1058
1290
  if (rawConfigKeys !== void 0 && !Array.isArray(rawConfigKeys)) {
1059
- throw new Error(`Plugin ${name} config-keys must be an array when provided`);
1291
+ throw new Error(
1292
+ `Plugin ${name} config-keys must be an array when provided`
1293
+ );
1060
1294
  }
1061
1295
  const configKeys = [];
1062
1296
  for (const key of rawConfigKeys ?? []) {
@@ -1067,11 +1301,20 @@ function parseManifest(raw, dir) {
1067
1301
  }
1068
1302
  const credentialsRaw = data.credentials;
1069
1303
  if (credentialsRaw !== void 0) {
1070
- toRecord(credentialsRaw, `Plugin ${name} credentials must be an object when provided`);
1304
+ toRecord(
1305
+ credentialsRaw,
1306
+ `Plugin ${name} credentials must be an object when provided`
1307
+ );
1071
1308
  }
1072
1309
  const credentials = credentialsRaw ? parseCredentials(credentialsRaw, name) : void 0;
1073
- const runtimeDependencies = parseRuntimeDependencies(data["runtime-dependencies"], name);
1074
- const runtimePostinstall = parseRuntimePostinstall(data["runtime-postinstall"], name);
1310
+ const runtimeDependencies = parseRuntimeDependencies(
1311
+ data["runtime-dependencies"],
1312
+ name
1313
+ );
1314
+ const runtimePostinstall = parseRuntimePostinstall(
1315
+ data["runtime-postinstall"],
1316
+ name
1317
+ );
1075
1318
  const manifest = {
1076
1319
  name,
1077
1320
  description,
@@ -1087,14 +1330,54 @@ function parseManifest(raw, dir) {
1087
1330
  throw new Error(`Plugin ${name} oauth requires credentials`);
1088
1331
  }
1089
1332
  if (credentials.type !== "oauth-bearer") {
1090
- throw new Error(`Plugin ${name} oauth requires credentials.type "oauth-bearer"`);
1333
+ throw new Error(
1334
+ `Plugin ${name} oauth requires credentials.type "oauth-bearer"`
1335
+ );
1336
+ }
1337
+ const authorizeParams = parseStringMap(
1338
+ oauthRaw["authorize-params"],
1339
+ `Plugin ${name} oauth.authorize-params`,
1340
+ { reservedKeys: RESERVED_AUTHORIZE_PARAM_KEYS }
1341
+ );
1342
+ const tokenExtraHeaders = parseStringMap(
1343
+ oauthRaw["token-extra-headers"],
1344
+ `Plugin ${name} oauth.token-extra-headers`,
1345
+ { forbiddenKeys: FORBIDDEN_TOKEN_HEADER_NAMES }
1346
+ );
1347
+ const tokenAuthMethodRaw = oauthRaw["token-auth-method"];
1348
+ let tokenAuthMethod;
1349
+ if (tokenAuthMethodRaw !== void 0) {
1350
+ const parsedTokenAuthMethod = requireStringField(
1351
+ oauthRaw,
1352
+ "token-auth-method",
1353
+ `Plugin ${name} oauth.token-auth-method must be a non-empty string`
1354
+ );
1355
+ if (parsedTokenAuthMethod !== "body" && parsedTokenAuthMethod !== "basic") {
1356
+ throw new Error(
1357
+ `Plugin ${name} oauth.token-auth-method must be "body" or "basic"`
1358
+ );
1359
+ }
1360
+ tokenAuthMethod = parsedTokenAuthMethod;
1091
1361
  }
1092
1362
  manifest.oauth = {
1093
1363
  clientIdEnv: requireEnvVarField(oauthRaw, "client-id-env", name),
1094
1364
  clientSecretEnv: requireEnvVarField(oauthRaw, "client-secret-env", name),
1095
- authorizeEndpoint: requireHttpsUrlField(oauthRaw, "authorize-endpoint", name),
1365
+ authorizeEndpoint: requireHttpsUrlField(
1366
+ oauthRaw,
1367
+ "authorize-endpoint",
1368
+ name
1369
+ ),
1096
1370
  tokenEndpoint: requireHttpsUrlField(oauthRaw, "token-endpoint", name),
1097
- scope: requireStringField(oauthRaw, "scope", `Plugin ${name} oauth.scope must be a non-empty string`)
1371
+ ...oauthRaw.scope !== void 0 ? {
1372
+ scope: requireStringField(
1373
+ oauthRaw,
1374
+ "scope",
1375
+ `Plugin ${name} oauth.scope must be a non-empty string`
1376
+ )
1377
+ } : {},
1378
+ ...authorizeParams ? { authorizeParams } : {},
1379
+ ...tokenAuthMethod ? { tokenAuthMethod } : {},
1380
+ ...tokenExtraHeaders ? { tokenExtraHeaders } : {}
1098
1381
  };
1099
1382
  }
1100
1383
  const targetRaw = data.target ? toRecord(data.target, `Plugin ${name} target must be an object`) : void 0;
@@ -1104,14 +1387,20 @@ function parseManifest(raw, dir) {
1104
1387
  }
1105
1388
  const rawConfigKey = targetRaw["config-key"];
1106
1389
  if (typeof rawConfigKey !== "string" || !rawConfigKey.trim()) {
1107
- throw new Error(`Plugin ${name} target.config-key must be a non-empty string`);
1390
+ throw new Error(
1391
+ `Plugin ${name} target.config-key must be a non-empty string`
1392
+ );
1108
1393
  }
1109
1394
  if (!SHORT_CONFIG_KEY_RE.test(rawConfigKey)) {
1110
- throw new Error(`Plugin ${name} target.config-key "${rawConfigKey}" is invalid`);
1395
+ throw new Error(
1396
+ `Plugin ${name} target.config-key "${rawConfigKey}" is invalid`
1397
+ );
1111
1398
  }
1112
1399
  const qualifiedKey = `${name}.${rawConfigKey}`;
1113
1400
  if (!configKeys.includes(qualifiedKey)) {
1114
- throw new Error(`Plugin ${name} target.config-key "${rawConfigKey}" must be listed in config-keys`);
1401
+ throw new Error(
1402
+ `Plugin ${name} target.config-key "${rawConfigKey}" must be listed in config-keys`
1403
+ );
1115
1404
  }
1116
1405
  manifest.target = { type: "repo", configKey: qualifiedKey };
1117
1406
  }
@@ -1130,7 +1419,9 @@ function registerPluginManifest(raw, pluginDir) {
1130
1419
  }
1131
1420
  for (const cap of manifest.capabilities) {
1132
1421
  if (capabilityToPlugin.has(cap)) {
1133
- throw new Error(`Duplicate capability "${cap}" in plugin "${manifest.name}"`);
1422
+ throw new Error(
1423
+ `Duplicate capability "${cap}" in plugin "${manifest.name}"`
1424
+ );
1134
1425
  }
1135
1426
  }
1136
1427
  const definition = {
@@ -1159,10 +1450,15 @@ function loadPlugins() {
1159
1450
  try {
1160
1451
  rootStat = statSync(pluginsRoot);
1161
1452
  } catch (error) {
1162
- logWarn("plugin_root_read_failed", {}, {
1163
- "file.directory": pluginsRoot,
1164
- "error.message": error instanceof Error ? error.message : String(error)
1165
- }, "Failed to read plugin root");
1453
+ logWarn(
1454
+ "plugin_root_read_failed",
1455
+ {},
1456
+ {
1457
+ "file.directory": pluginsRoot,
1458
+ "error.message": error instanceof Error ? error.message : String(error)
1459
+ },
1460
+ "Failed to read plugin root"
1461
+ );
1166
1462
  continue;
1167
1463
  }
1168
1464
  if (rootStat.isDirectory()) {
@@ -1182,10 +1478,15 @@ function loadPlugins() {
1182
1478
  try {
1183
1479
  entries = readdirSync(pluginsRoot);
1184
1480
  } catch (error) {
1185
- logWarn("plugin_root_read_failed", {}, {
1186
- "file.directory": pluginsRoot,
1187
- "error.message": error instanceof Error ? error.message : String(error)
1188
- }, "Failed to read plugin root");
1481
+ logWarn(
1482
+ "plugin_root_read_failed",
1483
+ {},
1484
+ {
1485
+ "file.directory": pluginsRoot,
1486
+ "error.message": error instanceof Error ? error.message : String(error)
1487
+ },
1488
+ "Failed to read plugin root"
1489
+ );
1189
1490
  continue;
1190
1491
  }
1191
1492
  for (const entry of entries.sort()) {
@@ -1221,8 +1522,12 @@ function loadPlugins() {
1221
1522
  "Loaded plugins"
1222
1523
  );
1223
1524
  }
1525
+ function ensurePluginsLoaded() {
1526
+ loadPlugins();
1527
+ }
1224
1528
  loadPlugins();
1225
1529
  function getPluginCapabilityProviders() {
1530
+ ensurePluginsLoaded();
1226
1531
  return pluginDefinitions.map((plugin) => ({
1227
1532
  provider: plugin.manifest.name,
1228
1533
  capabilities: [...plugin.manifest.capabilities],
@@ -1231,9 +1536,11 @@ function getPluginCapabilityProviders() {
1231
1536
  }));
1232
1537
  }
1233
1538
  function getPluginProviders() {
1539
+ ensurePluginsLoaded();
1234
1540
  return [...pluginDefinitions];
1235
1541
  }
1236
1542
  function getPluginRuntimeDependencies() {
1543
+ ensurePluginsLoaded();
1237
1544
  const seen = /* @__PURE__ */ new Set();
1238
1545
  const deps = [];
1239
1546
  for (const plugin of pluginDefinitions) {
@@ -1262,6 +1569,7 @@ function getPluginRuntimeDependencies() {
1262
1569
  });
1263
1570
  }
1264
1571
  function getPluginRuntimePostinstall() {
1572
+ ensurePluginsLoaded();
1265
1573
  const commands = [];
1266
1574
  for (const plugin of pluginDefinitions) {
1267
1575
  for (const command of plugin.manifest.runtimePostinstall ?? []) {
@@ -1275,6 +1583,7 @@ function getPluginRuntimePostinstall() {
1275
1583
  return commands;
1276
1584
  }
1277
1585
  function getPluginOAuthConfig(provider) {
1586
+ ensurePluginsLoaded();
1278
1587
  const plugin = pluginsByName.get(provider);
1279
1588
  if (!plugin?.manifest.oauth) return void 0;
1280
1589
  const oauth = plugin.manifest.oauth;
@@ -1283,17 +1592,28 @@ function getPluginOAuthConfig(provider) {
1283
1592
  clientSecretEnv: oauth.clientSecretEnv,
1284
1593
  authorizeEndpoint: oauth.authorizeEndpoint,
1285
1594
  tokenEndpoint: oauth.tokenEndpoint,
1286
- scope: oauth.scope,
1595
+ ...oauth.scope ? { scope: oauth.scope } : {},
1596
+ ...oauth.authorizeParams ? { authorizeParams: { ...oauth.authorizeParams } } : {},
1597
+ ...oauth.tokenAuthMethod ? { tokenAuthMethod: oauth.tokenAuthMethod } : {},
1598
+ ...oauth.tokenExtraHeaders ? { tokenExtraHeaders: { ...oauth.tokenExtraHeaders } } : {},
1287
1599
  callbackPath: `/api/oauth/callback/${plugin.manifest.name}`
1288
1600
  };
1289
1601
  }
1290
1602
  function getPluginSkillRoots() {
1291
- return [.../* @__PURE__ */ new Set([...pluginDefinitions.map((plugin) => plugin.skillsDir), ...packageSkillRoots])];
1603
+ ensurePluginsLoaded();
1604
+ return [
1605
+ .../* @__PURE__ */ new Set([
1606
+ ...pluginDefinitions.map((plugin) => plugin.skillsDir),
1607
+ ...packageSkillRoots
1608
+ ])
1609
+ ];
1292
1610
  }
1293
1611
  function isPluginProvider(provider) {
1612
+ ensurePluginsLoaded();
1294
1613
  return pluginsByName.has(provider);
1295
1614
  }
1296
1615
  function createPluginBroker(provider, deps) {
1616
+ ensurePluginsLoaded();
1297
1617
  const plugin = pluginsByName.get(provider);
1298
1618
  if (!plugin) {
1299
1619
  throw new Error(`Unknown plugin provider: "${provider}"`);
@@ -1393,6 +1713,9 @@ function buildDependencyProfile(runtime) {
1393
1713
  postinstall
1394
1714
  };
1395
1715
  }
1716
+ function getRuntimeDependencyProfileHash(runtime) {
1717
+ return buildDependencyProfile(runtime)?.profileHash;
1718
+ }
1396
1719
  function shouldRebuildCachedSnapshot(profile, cached) {
1397
1720
  if (!profile.hasFloatingVersions) {
1398
1721
  return false;
@@ -1423,7 +1746,11 @@ async function getCachedSnapshot(profileHash) {
1423
1746
  async function setCachedSnapshot(entry) {
1424
1747
  const state = getStateAdapter();
1425
1748
  await state.connect();
1426
- await state.set(profileCacheKey(entry.profileHash), JSON.stringify(entry), SNAPSHOT_CACHE_TTL_MS);
1749
+ await state.set(
1750
+ profileCacheKey(entry.profileHash),
1751
+ JSON.stringify(entry),
1752
+ SNAPSHOT_CACHE_TTL_MS
1753
+ );
1427
1754
  }
1428
1755
  async function withSnapshotSpan(name, op, attributes, callback) {
1429
1756
  return await withSpan(name, op, {}, callback, attributes);
@@ -1458,7 +1785,11 @@ async function installGhCliViaDnf(sandbox) {
1458
1785
  }
1459
1786
  const dnf5Repo = await tryRun(sandbox, {
1460
1787
  cmd: "dnf",
1461
- args: ["config-manager", "addrepo", "--from-repofile=https://cli.github.com/packages/rpm/gh-cli.repo"],
1788
+ args: [
1789
+ "config-manager",
1790
+ "addrepo",
1791
+ "--from-repofile=https://cli.github.com/packages/rpm/gh-cli.repo"
1792
+ ],
1462
1793
  sudo: true
1463
1794
  });
1464
1795
  if (!dnf5Repo.ok) {
@@ -1475,7 +1806,11 @@ async function installGhCliViaDnf(sandbox) {
1475
1806
  sandbox,
1476
1807
  {
1477
1808
  cmd: "dnf",
1478
- args: ["config-manager", "--add-repo", "https://cli.github.com/packages/rpm/gh-cli.repo"],
1809
+ args: [
1810
+ "config-manager",
1811
+ "--add-repo",
1812
+ "https://cli.github.com/packages/rpm/gh-cli.repo"
1813
+ ],
1479
1814
  sudo: true
1480
1815
  },
1481
1816
  "dnf config-manager --add-repo gh-cli.repo"
@@ -1506,8 +1841,12 @@ function runtimeDependencyFilePath(url, sha256) {
1506
1841
  return `/tmp/junior-runtime-${sha256.slice(0, 12)}-${sanitizedBasename}`;
1507
1842
  }
1508
1843
  async function installRuntimeDependencies(sandbox, deps) {
1509
- const systemDeps = deps.filter((dep) => dep.type === "system");
1510
- const npmPackages = deps.filter((dep) => dep.type === "npm").map((dep) => `${dep.package}@${dep.version}`);
1844
+ const systemDeps = deps.filter(
1845
+ (dep) => dep.type === "system"
1846
+ );
1847
+ const npmPackages = deps.filter(
1848
+ (dep) => dep.type === "npm"
1849
+ ).map((dep) => `${dep.package}@${dep.version}`);
1511
1850
  if (systemDeps.length > 0) {
1512
1851
  await withSnapshotSpan(
1513
1852
  "sandbox.snapshot.install_system",
@@ -1534,7 +1873,9 @@ async function installRuntimeDependencies(sandbox, deps) {
1534
1873
  const checksumStdout = (await checksumResult.stdout()).trim();
1535
1874
  const checksumStderr = (await checksumResult.stderr()).trim();
1536
1875
  if (checksumResult.exitCode !== 0) {
1537
- throw new Error(`sha256sum failed: ${checksumStderr || checksumStdout || "command failed"}`);
1876
+ throw new Error(
1877
+ `sha256sum failed: ${checksumStderr || checksumStdout || "command failed"}`
1878
+ );
1538
1879
  }
1539
1880
  const actualChecksum = checksumStdout.split(/\s+/)[0]?.toLowerCase();
1540
1881
  if (!actualChecksum) {
@@ -1611,7 +1952,10 @@ async function runRuntimePostinstall(sandbox, commands) {
1611
1952
  },
1612
1953
  async () => {
1613
1954
  for (const command of commands) {
1614
- const invocation = [JSON.stringify(command.cmd), ...(command.args ?? []).map((arg) => JSON.stringify(arg))].join(" ");
1955
+ const invocation = [
1956
+ JSON.stringify(command.cmd),
1957
+ ...(command.args ?? []).map((arg) => JSON.stringify(arg))
1958
+ ].join(" ");
1615
1959
  const pathPrefix = `${SANDBOX_WORKSPACE_ROOT}/.junior/bin:$PATH`;
1616
1960
  await runOrThrow(
1617
1961
  sandbox,
@@ -1766,7 +2110,9 @@ async function resolveRuntimeDependencySnapshot(params) {
1766
2110
  };
1767
2111
  }
1768
2112
  const cached = await getCachedSnapshot(profile.profileHash);
1769
- const cachedNeedsRebuild = Boolean(cached?.snapshotId && shouldRebuildCachedSnapshot(profile, cached));
2113
+ const cachedNeedsRebuild = Boolean(
2114
+ cached?.snapshotId && shouldRebuildCachedSnapshot(profile, cached)
2115
+ );
1770
2116
  if (!params.forceRebuild && cached?.snapshotId && !cachedNeedsRebuild) {
1771
2117
  await params.onProgress?.("cache_hit");
1772
2118
  return {
@@ -1792,34 +2138,50 @@ async function resolveRuntimeDependencySnapshot(params) {
1792
2138
  }
1793
2139
  return !shouldRebuildCachedSnapshot(profile, candidate);
1794
2140
  };
1795
- const lockResult = await withBuildLock(profile.profileHash, async () => {
1796
- const latest = await getCachedSnapshot(profile.profileHash);
1797
- if (latest?.snapshotId && canUseCachedSnapshot(latest)) {
1798
- await params.onProgress?.("cache_hit");
1799
- return { snapshotId: latest.snapshotId, source: "callback_cache" };
1800
- }
1801
- await params.onProgress?.("building_snapshot");
1802
- const nextSnapshotId = await createDependencySnapshot(profile, params.runtime, params.timeoutMs);
1803
- await setCachedSnapshot({
1804
- profileHash: profile.profileHash,
1805
- snapshotId: nextSnapshotId,
1806
- runtime: params.runtime,
1807
- createdAtMs: Date.now(),
1808
- dependencyCount: profile.dependencyCount
1809
- });
1810
- await params.onProgress?.("build_complete");
1811
- return { snapshotId: nextSnapshotId, source: "built" };
1812
- }, canUseCachedSnapshot, {
1813
- onWaitingForLock: async () => {
1814
- await params.onProgress?.("waiting_for_lock");
2141
+ const lockResult = await withBuildLock(
2142
+ profile.profileHash,
2143
+ async () => {
2144
+ const latest = await getCachedSnapshot(profile.profileHash);
2145
+ if (latest?.snapshotId && canUseCachedSnapshot(latest)) {
2146
+ await params.onProgress?.("cache_hit");
2147
+ return {
2148
+ snapshotId: latest.snapshotId,
2149
+ source: "callback_cache"
2150
+ };
2151
+ }
2152
+ await params.onProgress?.("building_snapshot");
2153
+ const nextSnapshotId = await createDependencySnapshot(
2154
+ profile,
2155
+ params.runtime,
2156
+ params.timeoutMs
2157
+ );
2158
+ await setCachedSnapshot({
2159
+ profileHash: profile.profileHash,
2160
+ snapshotId: nextSnapshotId,
2161
+ runtime: params.runtime,
2162
+ createdAtMs: Date.now(),
2163
+ dependencyCount: profile.dependencyCount
2164
+ });
2165
+ await params.onProgress?.("build_complete");
2166
+ return { snapshotId: nextSnapshotId, source: "built" };
2167
+ },
2168
+ canUseCachedSnapshot,
2169
+ {
2170
+ onWaitingForLock: async () => {
2171
+ await params.onProgress?.("waiting_for_lock");
2172
+ }
1815
2173
  }
1816
- });
2174
+ );
1817
2175
  return {
1818
2176
  snapshotId: lockResult.snapshotId,
1819
2177
  profileHash: profile.profileHash,
1820
2178
  dependencyCount: profile.dependencyCount,
1821
2179
  cacheHit: lockResult.source !== "built",
1822
- resolveOutcome: toResolveOutcome(Boolean(params.forceRebuild), lockResult.source, lockResult.waitedForLock),
2180
+ resolveOutcome: toResolveOutcome(
2181
+ Boolean(params.forceRebuild),
2182
+ lockResult.source,
2183
+ lockResult.waitedForLock
2184
+ ),
1823
2185
  ...rebuildReason ? { rebuildReason } : {}
1824
2186
  };
1825
2187
  }
@@ -1836,6 +2198,8 @@ export {
1836
2198
  soulPathCandidates,
1837
2199
  resolveAuthTokenPlaceholder,
1838
2200
  CredentialUnavailableError,
2201
+ buildOAuthTokenRequest,
2202
+ parseOAuthTokenResponse,
1839
2203
  getPluginCapabilityProviders,
1840
2204
  getPluginProviders,
1841
2205
  getPluginOAuthConfig,
@@ -1861,6 +2225,7 @@ export {
1861
2225
  SANDBOX_WORKSPACE_ROOT,
1862
2226
  SANDBOX_SKILLS_ROOT,
1863
2227
  sandboxSkillDir,
2228
+ getRuntimeDependencyProfileHash,
1864
2229
  resolveRuntimeDependencySnapshot,
1865
2230
  isSnapshotMissingError
1866
2231
  };