paperclip-github-plugin 0.9.0 → 0.9.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.
package/dist/worker.js CHANGED
@@ -1,9 +1,9 @@
1
1
  // src/worker.ts
2
2
  import { Buffer } from "node:buffer";
3
3
  import { realpathSync } from "node:fs";
4
- import { readFile } from "node:fs/promises";
4
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
5
5
  import { homedir } from "node:os";
6
- import { join, resolve } from "node:path";
6
+ import { dirname, join, resolve } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { Octokit } from "@octokit/rest";
9
9
  import {
@@ -1604,6 +1604,10 @@ function getErrorMessage(error) {
1604
1604
  }
1605
1605
  return String(error);
1606
1606
  }
1607
+ function isPluginSecretReferenceDisabledError(error) {
1608
+ const message = getErrorMessage(error).toLowerCase();
1609
+ return message.includes("plugin secret reference") && message.includes("disabled") || message.includes("company-scoped plugin config lands");
1610
+ }
1607
1611
  function getErrorCause(error) {
1608
1612
  if (!error || typeof error !== "object" || !("cause" in error)) {
1609
1613
  return void 0;
@@ -1974,6 +1978,20 @@ function normalizeGitHubTokenRefs(value) {
1974
1978
  }
1975
1979
  return Object.fromEntries(entries);
1976
1980
  }
1981
+ function normalizeGitHubTokensByCompanyId(value) {
1982
+ if (!value || typeof value !== "object") {
1983
+ return void 0;
1984
+ }
1985
+ const entries = Object.entries(value).map(([companyId, token]) => {
1986
+ const normalizedCompanyId = normalizeCompanyId(companyId);
1987
+ const normalizedToken = normalizeGitHubToken(token);
1988
+ return normalizedCompanyId && normalizedToken ? [normalizedCompanyId, normalizedToken] : null;
1989
+ }).filter((entry) => entry !== null);
1990
+ if (entries.length === 0) {
1991
+ return void 0;
1992
+ }
1993
+ return Object.fromEntries(entries);
1994
+ }
1977
1995
  function formatUtcTimestamp(value) {
1978
1996
  const parsed = new Date(value);
1979
1997
  if (Number.isNaN(parsed.getTime())) {
@@ -3710,12 +3728,14 @@ function normalizeConfig(value) {
3710
3728
  const githubTokenRefs = normalizeGitHubTokenRefs(record.githubTokenRefs);
3711
3729
  const githubTokenRef = normalizeGitHubTokenRef(record.githubTokenRef);
3712
3730
  const githubToken = normalizeGitHubToken(record.githubToken);
3731
+ const githubTokensByCompanyId = normalizeGitHubTokensByCompanyId(record.githubTokensByCompanyId);
3713
3732
  const paperclipBoardApiTokenRefs = normalizePaperclipBoardApiTokenRefs(record.paperclipBoardApiTokenRefs);
3714
3733
  const paperclipApiBaseUrl = normalizePaperclipApiBaseUrl(record.paperclipApiBaseUrl);
3715
3734
  return {
3716
3735
  ...githubTokenRefs ? { githubTokenRefs } : {},
3717
3736
  ...githubTokenRef ? { githubTokenRef } : {},
3718
3737
  ...githubToken ? { githubToken } : {},
3738
+ ...githubTokensByCompanyId ? { githubTokensByCompanyId } : {},
3719
3739
  ...paperclipBoardApiTokenRefs ? { paperclipBoardApiTokenRefs } : {},
3720
3740
  ...paperclipApiBaseUrl ? { paperclipApiBaseUrl } : {}
3721
3741
  };
@@ -3791,6 +3811,55 @@ async function readExternalConfig(ctx) {
3791
3811
  return {};
3792
3812
  }
3793
3813
  }
3814
+ async function readExternalConfigRecordForWrite(ctx, filePath) {
3815
+ try {
3816
+ const rawConfig = await readFile(filePath, "utf8");
3817
+ const parsedConfig = JSON.parse(rawConfig);
3818
+ return parsedConfig && typeof parsedConfig === "object" && !Array.isArray(parsedConfig) ? { ...parsedConfig } : {};
3819
+ } catch (error) {
3820
+ const errorCode = error && typeof error === "object" && "code" in error ? error.code : void 0;
3821
+ if (errorCode === "ENOENT") {
3822
+ return {};
3823
+ }
3824
+ if (error instanceof SyntaxError) {
3825
+ ctx.logger.warn("Ignoring the GitHub Sync worker-local token fallback config file because it is not valid JSON.", {
3826
+ filePath,
3827
+ error: error.message
3828
+ });
3829
+ return {};
3830
+ }
3831
+ throw error;
3832
+ }
3833
+ }
3834
+ async function writeExternalCompanyGitHubTokenFallback(ctx, companyId, token) {
3835
+ const externalConfigFilePath = getExternalConfigFilePath();
3836
+ if (!externalConfigFilePath) {
3837
+ throw new Error("Could not resolve a Paperclip home directory for the GitHub Sync fallback token config.");
3838
+ }
3839
+ const currentRecord = await readExternalConfigRecordForWrite(ctx, externalConfigFilePath);
3840
+ const currentCompanyTokens = normalizeGitHubTokensByCompanyId(currentRecord.githubTokensByCompanyId) ?? {};
3841
+ const nextRecord = {
3842
+ ...currentRecord,
3843
+ githubTokensByCompanyId: {
3844
+ ...currentCompanyTokens,
3845
+ [companyId]: token
3846
+ }
3847
+ };
3848
+ await mkdir(dirname(externalConfigFilePath), { recursive: true });
3849
+ await writeFile(externalConfigFilePath, `${JSON.stringify(nextRecord, null, 2)}
3850
+ `, {
3851
+ encoding: "utf8",
3852
+ mode: 384
3853
+ });
3854
+ try {
3855
+ await chmod(externalConfigFilePath, 384);
3856
+ } catch (error) {
3857
+ ctx.logger.warn("GitHub Sync could not tighten permissions on the worker-local token fallback file.", {
3858
+ filePath: externalConfigFilePath,
3859
+ error: getErrorMessage(error)
3860
+ });
3861
+ }
3862
+ }
3794
3863
  function normalizePaperclipBoardApiTokenRefs(value) {
3795
3864
  if (!value || typeof value !== "object") {
3796
3865
  return void 0;
@@ -9985,6 +10054,7 @@ async function getResolvedConfig(ctx) {
9985
10054
  }
9986
10055
  function getConfiguredGithubTokenSource(settings, config, companyId) {
9987
10056
  const normalizedCompanyId = normalizeCompanyId(companyId);
10057
+ const companyFallbackToken = normalizedCompanyId ? normalizeGitHubToken(config.githubTokensByCompanyId?.[normalizedCompanyId]) : void 0;
9988
10058
  const hasScopedGitHubTokenRefs = hasAnyScopedValue(settings?.githubTokenRefs) || hasAnyScopedValue(config.githubTokenRefs);
9989
10059
  const secretRef = normalizedCompanyId ? normalizeSecretRef(config.githubTokenRefs?.[normalizedCompanyId]) ?? normalizeSecretRef(settings?.githubTokenRefs?.[normalizedCompanyId]) ?? (!hasScopedGitHubTokenRefs ? normalizeGitHubTokenRef(config.githubTokenRef) ?? normalizeGitHubTokenRef(settings?.githubTokenRef) : void 0) : normalizeGitHubTokenRef(config.githubTokenRef) ?? normalizeGitHubTokenRef(settings?.githubTokenRef) ?? (() => {
9990
10060
  const configuredRefs = [
@@ -9995,14 +10065,17 @@ function getConfiguredGithubTokenSource(settings, config, companyId) {
9995
10065
  return uniqueRefs.length === 1 ? uniqueRefs[0] : void 0;
9996
10066
  })();
9997
10067
  if (secretRef) {
9998
- return { secretRef };
10068
+ return {
10069
+ secretRef,
10070
+ ...companyFallbackToken ? { fallbackToken: companyFallbackToken } : {}
10071
+ };
9999
10072
  }
10000
- const token = !normalizedCompanyId || !hasScopedGitHubTokenRefs ? normalizeGitHubToken(config.githubToken) : void 0;
10073
+ const token = companyFallbackToken ?? (!normalizedCompanyId || !hasScopedGitHubTokenRefs ? normalizeGitHubToken(config.githubToken) : void 0);
10001
10074
  return token ? { token } : {};
10002
10075
  }
10003
10076
  function hasConfiguredGithubToken(settings, config, companyId) {
10004
10077
  const configuredTokenSource = getConfiguredGithubTokenSource(settings, config, companyId);
10005
- if (configuredTokenSource.secretRef ?? configuredTokenSource.token) {
10078
+ if (configuredTokenSource.secretRef ?? configuredTokenSource.token ?? configuredTokenSource.fallbackToken) {
10006
10079
  return true;
10007
10080
  }
10008
10081
  if (normalizeCompanyId(companyId)) {
@@ -10012,6 +10085,18 @@ function hasConfiguredGithubToken(settings, config, companyId) {
10012
10085
  settings?.githubTokenRefs && Object.keys(settings.githubTokenRefs).length > 0 || config.githubTokenRefs && Object.keys(config.githubTokenRefs).length > 0
10013
10086
  );
10014
10087
  }
10088
+ function getSavedGitHubTokenRef(settings, companyId) {
10089
+ if (!companyId) {
10090
+ return void 0;
10091
+ }
10092
+ return normalizeSecretRef(settings?.githubTokenRefs?.[companyId]);
10093
+ }
10094
+ function getConfiguredGitHubTokenRef(config, companyId) {
10095
+ if (!companyId) {
10096
+ return void 0;
10097
+ }
10098
+ return normalizeSecretRef(config?.githubTokenRefs?.[companyId]);
10099
+ }
10015
10100
  function getSavedPaperclipBoardApiTokenRef(settings, companyId) {
10016
10101
  if (!companyId) {
10017
10102
  return void 0;
@@ -10095,7 +10180,23 @@ async function resolveGithubToken(ctx, options = {}) {
10095
10180
  const config = options.config ?? await getResolvedConfig(ctx);
10096
10181
  const configuredTokenSource = getConfiguredGithubTokenSource(settings, config, options.companyId);
10097
10182
  if (configuredTokenSource.secretRef) {
10098
- return ctx.secrets.resolve(configuredTokenSource.secretRef);
10183
+ try {
10184
+ const token = (await ctx.secrets.resolve(configuredTokenSource.secretRef)).trim();
10185
+ if (token) {
10186
+ return token;
10187
+ }
10188
+ return configuredTokenSource.fallbackToken ?? "";
10189
+ } catch (error) {
10190
+ if (configuredTokenSource.fallbackToken && isPluginSecretReferenceDisabledError(error)) {
10191
+ ctx.logger.warn("GitHub Sync is using a worker-local company token fallback because plugin secret refs are unavailable in this host.", {
10192
+ companyId: normalizeCompanyId(options.companyId),
10193
+ secretRef: configuredTokenSource.secretRef,
10194
+ error: getErrorMessage(error)
10195
+ });
10196
+ return configuredTokenSource.fallbackToken;
10197
+ }
10198
+ throw error;
10199
+ }
10099
10200
  }
10100
10201
  return configuredTokenSource.token ?? "";
10101
10202
  }
@@ -15553,6 +15654,7 @@ var __testing = {
15553
15654
  formatPaperclipApiFetchErrorMessage,
15554
15655
  hasUnresolvedPaperclipIssueBlocker,
15555
15656
  isHealthyMaintainerWaitTransition,
15657
+ resolveGithubToken,
15556
15658
  resolvePaperclipPullRequestIssueStatus,
15557
15659
  resolveSyncTransitionAssignee
15558
15660
  };
@@ -15569,6 +15671,8 @@ var plugin = definePlugin({
15569
15671
  const normalizedSettings = normalizeSettings(saved);
15570
15672
  const config = await getResolvedConfig(ctx);
15571
15673
  const githubTokenConfigured = hasConfiguredGithubToken(normalizedSettings, config, requestedCompanyId);
15674
+ const configuredGitHubTokenRef = getConfiguredGitHubTokenRef(config, requestedCompanyId);
15675
+ const savedGitHubTokenRef = getSavedGitHubTokenRef(normalizedSettings, requestedCompanyId);
15572
15676
  const configuredBoardTokenRef = getConfiguredPaperclipBoardApiTokenRef(config, requestedCompanyId);
15573
15677
  const savedBoardTokenRef = getSavedPaperclipBoardApiTokenRef(normalizedSettings, requestedCompanyId);
15574
15678
  const settingsForResponse = sanitizeSettingsForCurrentSetup(
@@ -15590,6 +15694,8 @@ var plugin = definePlugin({
15590
15694
  paperclipApiBaseUrlConfigured: Boolean(normalizePaperclipApiBaseUrl(config.paperclipApiBaseUrl)),
15591
15695
  githubTokenConfigured,
15592
15696
  paperclipBoardAccessConfigured: requestedCompanyId ? hasConfiguredPaperclipBoardAccess(settingsForResponse, config, requestedCompanyId) : hasConfiguredPaperclipBoardAccessForMappings(settingsForResponse, config, scopedMappings),
15697
+ ...savedGitHubTokenRef ? { githubTokenConfigSyncRef: savedGitHubTokenRef } : {},
15698
+ githubTokenNeedsConfigSync: Boolean(savedGitHubTokenRef && configuredGitHubTokenRef !== savedGitHubTokenRef),
15593
15699
  ...savedBoardTokenRef ? { paperclipBoardAccessConfigSyncRef: savedBoardTokenRef } : {},
15594
15700
  paperclipBoardAccessNeedsConfigSync: Boolean(savedBoardTokenRef && !configuredBoardTokenRef)
15595
15701
  };
@@ -15856,6 +15962,44 @@ var plugin = definePlugin({
15856
15962
  }
15857
15963
  return validateGithubToken(ctx, trimmedToken);
15858
15964
  });
15965
+ ctx.actions.register("settings.ensureGitHubTokenAvailable", async (input) => {
15966
+ const record = input && typeof input === "object" ? input : {};
15967
+ const companyId = normalizeCompanyId(record.companyId);
15968
+ const githubTokenRef = normalizeSecretRef(record.githubTokenRef);
15969
+ const token = normalizeGitHubToken(record.token);
15970
+ if (!companyId) {
15971
+ throw new Error("Company context is required to verify worker access to the GitHub token.");
15972
+ }
15973
+ if (!githubTokenRef) {
15974
+ throw new Error("A GitHub token secret ref is required to verify worker token access.");
15975
+ }
15976
+ if (!token) {
15977
+ throw new Error("A validated GitHub token is required to prepare the worker token fallback.");
15978
+ }
15979
+ try {
15980
+ const resolvedToken = (await ctx.secrets.resolve(githubTokenRef)).trim();
15981
+ if (resolvedToken) {
15982
+ return {
15983
+ secretResolvable: true,
15984
+ fallbackStored: false
15985
+ };
15986
+ }
15987
+ } catch (error) {
15988
+ if (!isPluginSecretReferenceDisabledError(error)) {
15989
+ throw error;
15990
+ }
15991
+ await writeExternalCompanyGitHubTokenFallback(ctx, companyId, token);
15992
+ return {
15993
+ secretResolvable: false,
15994
+ fallbackStored: true
15995
+ };
15996
+ }
15997
+ await writeExternalCompanyGitHubTokenFallback(ctx, companyId, token);
15998
+ return {
15999
+ secretResolvable: false,
16000
+ fallbackStored: true
16001
+ };
16002
+ });
15859
16003
  ctx.actions.register("project.pullRequests.createIssue", async (input) => {
15860
16004
  const record = input && typeof input === "object" ? input : {};
15861
16005
  return createProjectPullRequestPaperclipIssue(ctx, record);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperclip-github-plugin",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Paperclip plugin for synchronizing GitHub issues into Paperclip projects.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",