@agentconnect.md/setup 1.44.0-rc.61 → 1.44.0-rc.62

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/index.js CHANGED
@@ -56542,6 +56542,30 @@ const SECRET_ENVELOPE_PREFIX = "acv1:";
56542
56542
  function effectiveOrgKeyPrefix(transitKey, configured) {
56543
56543
  return configured ?? `${transitKey}-org-`;
56544
56544
  }
56545
+ //#endregion
56546
+ //#region ../control-plane/dist/gitlab/config.js
56547
+ /** The default value of the host axis: an unset base URL means GitLab.com (§24.1). */
56548
+ const GITLAB_DEFAULT_BASE_URL = "https://gitlab.com";
56549
+ /** The public callback path, in its gateway form (deploy-public-url-prefix decision). */
56550
+ const GITLAB_OAUTH_CALLBACK_PATH = "/v1/gitlab/oauth/callback";
56551
+ /** The one normalization of the host axis (§24.1); downstream sees only its
56552
+ * result. HTTPS, no userinfo/query/fragment, lower-cased host, explicit
56553
+ * non-default port kept, no trailing slash, and a path prefix preserved —
56554
+ * a relative URL root is a first-class install shape. */
56555
+ function normalizeGitlabBaseUrl(raw) {
56556
+ const trimmed = raw.trim();
56557
+ let url;
56558
+ try {
56559
+ url = new URL(trimmed);
56560
+ } catch {
56561
+ throw new Error("gitlab base url must be an absolute URL");
56562
+ }
56563
+ if (url.protocol !== "https:") throw new Error("gitlab base url must use https");
56564
+ if (url.username !== "" || url.password !== "") throw new Error("gitlab base url must not carry userinfo");
56565
+ if (url.search !== "") throw new Error("gitlab base url must not carry a query");
56566
+ if (url.hash !== "") throw new Error("gitlab base url must not carry a fragment");
56567
+ return `https://${url.host}${url.pathname.replace(/\/+$/, "")}`;
56568
+ }
56545
56569
  const HttpUrlSchema = string().url().superRefine((value, ctx) => {
56546
56570
  const url = new URL(value);
56547
56571
  if (url.protocol !== "http:" && url.protocol !== "https:") ctx.addIssue({
@@ -56605,7 +56629,20 @@ const GithubAppSchema = preprocess(withoutProviderUrlSnapshot, strictObject({
56605
56629
  /** Whether Relay should accept GitHub webhook delivery for this App. Omitted means enabled. */
56606
56630
  webhookEnabled: boolean().optional()
56607
56631
  }));
56608
- const GitlabAppSchema = preprocess(withoutProviderUrlSnapshot, strictObject({ clientId: string().trim().min(1) }));
56632
+ const GitlabBaseUrlSchema = string().trim().min(1).superRefine((value, ctx) => {
56633
+ try {
56634
+ normalizeGitlabBaseUrl(value);
56635
+ } catch (error) {
56636
+ ctx.addIssue({
56637
+ code: "custom",
56638
+ message: error.message
56639
+ });
56640
+ }
56641
+ });
56642
+ const GitlabAppSchema = preprocess(withoutProviderUrlSnapshot, strictObject({
56643
+ clientId: string().trim().min(1),
56644
+ baseUrl: GitlabBaseUrlSchema.nullable().optional()
56645
+ }));
56609
56646
  const SlackAppSchema = preprocess(withoutProviderUrlSnapshot, strictObject({
56610
56647
  appId: string().trim().min(1),
56611
56648
  clientId: string().trim().min(1)
@@ -56713,6 +56750,27 @@ var DeploymentConfigSecretRefreshRequiredError = class extends Error {
56713
56750
  this.name = "DeploymentConfigSecretRefreshRequiredError";
56714
56751
  }
56715
56752
  };
56753
+ /** The named reason a GitLab base-URL change is refused (gitlab-com-integration.md §24.1). */
56754
+ const GITLAB_BASE_URL_LOCKED_REASON = "gitlab_base_url_locked";
56755
+ /** Retargeting would send the old host's credentials and host-relative numeric
56756
+ * ids to a new one, because no GitLab row carries instance provenance (§24.1):
56757
+ * the axis is immutable while any GitLab state exists. */
56758
+ var DeploymentConfigGitlabBaseUrlLockedError = class extends Error {
56759
+ currentBaseUrl;
56760
+ requestedBaseUrl;
56761
+ code = GITLAB_BASE_URL_LOCKED_REASON;
56762
+ constructor(currentBaseUrl, requestedBaseUrl) {
56763
+ super(`the GitLab instance base URL is locked while GitLab state exists (${currentBaseUrl} → ${requestedBaseUrl}); disconnect every GitLab project first`);
56764
+ this.currentBaseUrl = currentBaseUrl;
56765
+ this.requestedBaseUrl = requestedBaseUrl;
56766
+ this.name = "DeploymentConfigGitlabBaseUrlLockedError";
56767
+ }
56768
+ };
56769
+ /** The instance a document's GitLab entry selects; absent means GitLab.com (§24.1). */
56770
+ function effectiveGitlabBaseUrl(values) {
56771
+ const configured = values.gitlab?.baseUrl;
56772
+ return configured ? normalizeGitlabBaseUrl(configured) : GITLAB_DEFAULT_BASE_URL;
56773
+ }
56716
56774
  function parseDeploymentConfigValues(schemaVersion, values) {
56717
56775
  if (schemaVersion !== 1) throw new Error(`unsupported deployment configuration schema version: ${schemaVersion}`);
56718
56776
  return DeploymentConfigValuesV1Schema.parse(values);
@@ -56721,7 +56779,7 @@ function deploymentSecretsRequiringRefresh(previous, next) {
56721
56779
  const githubAppChanged = next.github && previous?.github?.appId !== next.github.appId;
56722
56780
  const githubClientChanged = next.github !== null && next.github.clientId !== null && previous?.github?.clientId !== next.github.clientId;
56723
56781
  const githubWebhookEnabled = next.github !== null && next.github.webhookEnabled !== false;
56724
- const gitlabClientChanged = next.gitlab != null && previous?.gitlab?.clientId !== next.gitlab.clientId;
56782
+ const gitlabClientChanged = next.gitlab != null && (previous?.gitlab?.clientId !== next.gitlab.clientId || previous != null && effectiveGitlabBaseUrl(previous) !== effectiveGitlabBaseUrl(next));
56725
56783
  const slackIdentityChanged = next.slack && (previous?.slack?.appId !== next.slack.appId || previous?.slack?.clientId !== next.slack.clientId);
56726
56784
  const feishuIdentityChanged = next.feishu && previous?.feishu?.loginAppId !== next.feishu.loginAppId;
56727
56785
  const larkIdentityChanged = next.lark && previous?.lark?.loginAppId !== next.lark.loginAppId;
@@ -56845,17 +56903,26 @@ var DeploymentConfigService = class {
56845
56903
  }
56846
56904
  };
56847
56905
  //#endregion
56848
- //#region ../control-plane/dist/persistence/repositories/deployment-config.repo.js
56906
+ //#region ../control-plane/dist/persistence/repositories/gitlab-axis.js
56849
56907
  /**
56850
- * PostgreSQL persistence for the deployment-wide singleton configuration.
56908
+ * The deployment-wide GitLab host-axis fence (gitlab-com-integration.md §24.1).
56851
56909
  *
56852
- * Admin reads deliberately omit `deployment_secret.value`; the separate
56853
- * runtime read is the only query in this repository that selects ciphertext.
56854
- * Replacement locks one deployment-global advisory key and commits the typed
56855
- * JSON document, monotonic revision, and secret patch in one transaction.
56910
+ * No GitLab row carries instance provenance, so the axis may only change while
56911
+ * no GitLab state exists. That is a two-sided invariant, and one advisory key
56912
+ * carries both sides: the deployment-config writer takes it EXCLUSIVELY around
56913
+ * its state count, and every transaction that creates first-of-its-kind GitLab
56914
+ * state takes it SHARED and then proves the persisted axis still matches the
56915
+ * base its in-flight operation was composed against. Without the shared side a
56916
+ * count of zero and a concurrent connect could both commit, leaving one host's
56917
+ * credentials to be presented to another after the next restart.
56856
56918
  */
56857
- const DEPLOYMENT_CONFIG_ID = 1;
56858
56919
  const DEPLOYMENT_CONFIG_LOCK_KEY = "agentconnect:deployment-config";
56920
+ /** The config writer's side: nothing else may commit GitLab state alongside it. */
56921
+ async function lockAxisExclusive(tx) {
56922
+ await tx.$queryRaw(sql`SELECT pg_advisory_xact_lock(hashtextextended(${DEPLOYMENT_CONFIG_LOCK_KEY}, 0)) IS NULL AS "locked"`);
56923
+ }
56924
+ //#endregion
56925
+ //#region ../control-plane/dist/persistence/repositories/deployment-config.repo.js
56859
56926
  const adminSelect = {
56860
56927
  schemaVersion: true,
56861
56928
  revision: true,
@@ -56908,30 +56975,50 @@ function toRuntime(row) {
56908
56975
  updatedAt: row.updatedAt
56909
56976
  };
56910
56977
  }
56978
+ /** Does any GitLab state still bind this deployment to its instance (§24.1)? A
56979
+ * `disconnected` connection is credential-free history and does not; a binding
56980
+ * (`cleanup_pending` included), an account, a hook, or a claim carrying a
56981
+ * tombstone or an unfinished cleanup obligation does. */
56982
+ async function gitlabStateExists(tx) {
56983
+ const [connections, bindings, accounts, hooks, claims] = await Promise.all([
56984
+ tx.gitlabConnection.count({ where: { NOT: { state: "disconnected" } } }),
56985
+ tx.gitlabProjectBinding.count(),
56986
+ tx.gitlabAgentAccount.count(),
56987
+ tx.hookDef.count({ where: { kind: "gitlab" } }),
56988
+ tx.codeHostRepositoryClaim.count({ where: { provider: "gitlab" } })
56989
+ ]);
56990
+ return connections + bindings + accounts + hooks + claims > 0;
56991
+ }
56911
56992
  var PgDeploymentConfigRepository = class {
56912
56993
  prisma;
56913
- constructor(prisma) {
56994
+ /** The axis the process-level fallback selects, for the FIRST persisted
56995
+ * document: with no row, `GITLAB_BASE_URL` is what the running deployment
56996
+ * already serves, so it — not GitLab.com — is what a first write must match. */
56997
+ envGitlabBaseUrl;
56998
+ constructor(prisma, envGitlabBaseUrl) {
56914
56999
  this.prisma = prisma;
57000
+ const raw = envGitlabBaseUrl?.trim();
57001
+ this.envGitlabBaseUrl = raw ? normalizeGitlabBaseUrl(raw) : GITLAB_DEFAULT_BASE_URL;
56915
57002
  }
56916
57003
  async readAdmin() {
56917
57004
  const row = await this.prisma.deploymentConfig.findUnique({
56918
- where: { id: DEPLOYMENT_CONFIG_ID },
57005
+ where: { id: 1 },
56919
57006
  select: adminSelect
56920
57007
  });
56921
57008
  return row ? toAdmin(row) : null;
56922
57009
  }
56923
57010
  async readRuntime() {
56924
57011
  const row = await this.prisma.deploymentConfig.findUnique({
56925
- where: { id: DEPLOYMENT_CONFIG_ID },
57012
+ where: { id: 1 },
56926
57013
  select: runtimeSelect
56927
57014
  });
56928
57015
  return row ? toRuntime(row) : null;
56929
57016
  }
56930
57017
  async replace(input) {
56931
57018
  return withTx(this.prisma, async (tx) => {
56932
- await tx.$queryRaw(sql`SELECT pg_advisory_xact_lock(hashtextextended(${DEPLOYMENT_CONFIG_LOCK_KEY}, 0)) IS NULL AS "locked"`);
57019
+ await lockAxisExclusive(tx);
56933
57020
  const current = await tx.deploymentConfig.findUnique({
56934
- where: { id: DEPLOYMENT_CONFIG_ID },
57021
+ where: { id: 1 },
56935
57022
  select: {
56936
57023
  revision: true,
56937
57024
  schemaVersion: true,
@@ -56941,10 +57028,13 @@ var PgDeploymentConfigRepository = class {
56941
57028
  const actualRevision = current?.revision ?? 0;
56942
57029
  if (actualRevision !== input.expectedRevision) throw new DeploymentConfigConflictError(input.expectedRevision, actualRevision);
56943
57030
  const previousValues = current ? parseDeploymentConfigValues(current.schemaVersion, current.values) : null;
57031
+ const previousBaseUrl = previousValues ? effectiveGitlabBaseUrl(previousValues) : this.envGitlabBaseUrl;
57032
+ const nextBaseUrl = effectiveGitlabBaseUrl(input.values);
57033
+ if (previousBaseUrl !== nextBaseUrl && await gitlabStateExists(tx)) throw new DeploymentConfigGitlabBaseUrlLockedError(previousBaseUrl, nextBaseUrl);
56944
57034
  const missingRefresh = (previousValues ? deploymentSecretsRequiringRefresh(previousValues, input.values) : []).filter((key) => !input.secrets[key]);
56945
57035
  if (missingRefresh.length > 0) throw new DeploymentConfigSecretRefreshRequiredError(missingRefresh);
56946
57036
  const currentSecrets = await tx.deploymentSecret.findMany({
56947
- where: { deploymentConfigId: DEPLOYMENT_CONFIG_ID },
57037
+ where: { deploymentConfigId: 1 },
56948
57038
  select: { key: true }
56949
57039
  });
56950
57040
  const effective = new Set(currentSecrets.map(({ key }) => key));
@@ -56953,9 +57043,9 @@ var PgDeploymentConfigRepository = class {
56953
57043
  const missing = input.requiredSecretKeys.filter((key) => !effective.has(key));
56954
57044
  if (missing.length > 0) throw new DeploymentConfigMissingSecretsError(missing);
56955
57045
  await tx.deploymentConfig.upsert({
56956
- where: { id: DEPLOYMENT_CONFIG_ID },
57046
+ where: { id: 1 },
56957
57047
  create: {
56958
- id: DEPLOYMENT_CONFIG_ID,
57048
+ id: 1,
56959
57049
  schemaVersion: input.schemaVersion,
56960
57050
  values: input.values,
56961
57051
  revision: 1
@@ -56970,18 +57060,18 @@ var PgDeploymentConfigRepository = class {
56970
57060
  const key = rawKey;
56971
57061
  if (prepared === null) {
56972
57062
  await tx.deploymentSecret.deleteMany({ where: {
56973
- deploymentConfigId: DEPLOYMENT_CONFIG_ID,
57063
+ deploymentConfigId: 1,
56974
57064
  key
56975
57065
  } });
56976
57066
  continue;
56977
57067
  }
56978
57068
  await tx.deploymentSecret.upsert({
56979
57069
  where: { deploymentConfigId_key: {
56980
- deploymentConfigId: DEPLOYMENT_CONFIG_ID,
57070
+ deploymentConfigId: 1,
56981
57071
  key
56982
57072
  } },
56983
57073
  create: {
56984
- deploymentConfigId: DEPLOYMENT_CONFIG_ID,
57074
+ deploymentConfigId: 1,
56985
57075
  key,
56986
57076
  value: prepared.sealedValue,
56987
57077
  fingerprint: prepared.fingerprint
@@ -56993,7 +57083,7 @@ var PgDeploymentConfigRepository = class {
56993
57083
  });
56994
57084
  }
56995
57085
  return toAdmin(await tx.deploymentConfig.findUniqueOrThrow({
56996
- where: { id: DEPLOYMENT_CONFIG_ID },
57086
+ where: { id: 1 },
56997
57087
  select: adminSelect
56998
57088
  }));
56999
57089
  });
@@ -57001,21 +57091,21 @@ var PgDeploymentConfigRepository = class {
57001
57091
  async markAdminClaimed(expectedRevision, claimedFor) {
57002
57092
  if ((await this.prisma.deploymentConfig.updateMany({
57003
57093
  where: {
57004
- id: DEPLOYMENT_CONFIG_ID,
57094
+ id: 1,
57005
57095
  revision: expectedRevision
57006
57096
  },
57007
57097
  data: { adminClaimedFor: claimedFor }
57008
57098
  })).count === 1) return;
57009
57099
  throw new DeploymentConfigConflictError(expectedRevision, (await this.prisma.deploymentConfig.findUnique({
57010
- where: { id: DEPLOYMENT_CONFIG_ID },
57100
+ where: { id: 1 },
57011
57101
  select: { revision: true }
57012
57102
  }))?.revision ?? 0);
57013
57103
  }
57014
57104
  };
57015
57105
  /** Composition convenience used by the CP container and tests. */
57016
57106
  var PgDeploymentConfigStore = class extends DeploymentConfigService {
57017
- constructor(prisma, cipher) {
57018
- super(new PgDeploymentConfigRepository(prisma), cipher);
57107
+ constructor(prisma, cipher, envGitlabBaseUrl) {
57108
+ super(new PgDeploymentConfigRepository(prisma, envGitlabBaseUrl), cipher);
57019
57109
  }
57020
57110
  };
57021
57111
  //#endregion
@@ -57294,7 +57384,7 @@ function makeSecretCipher(config) {
57294
57384
  /** Open exactly the DB + SecretCipher slice shared by CP and setup tooling. */
57295
57385
  function openDeploymentConfigStore(options) {
57296
57386
  return {
57297
- store: new PgDeploymentConfigStore(createPrisma(options.databaseUrl), makeSecretCipher(options)),
57387
+ store: new PgDeploymentConfigStore(createPrisma(options.databaseUrl), makeSecretCipher(options), options.gitlabBaseUrl),
57298
57388
  close: disconnectPrisma
57299
57389
  };
57300
57390
  }
@@ -59750,10 +59840,6 @@ async function convertGithubManifest(code, options = {}) {
59750
59840
  };
59751
59841
  }
59752
59842
  //#endregion
59753
- //#region ../control-plane/dist/gitlab/config.js
59754
- /** The public callback path, in its gateway form (deploy-public-url-prefix decision). */
59755
- const GITLAB_OAUTH_CALLBACK_PATH = "/v1/gitlab/oauth/callback";
59756
- //#endregion
59757
59843
  //#region src/gitlab-app.ts
59758
59844
  /** Exactly the scope set the Control Plane asks GitLab for (gitlab-com-integration.md §9.1). */
59759
59845
  const GITLAB_OAUTH_SCOPES = ["api"];
@@ -62183,6 +62269,7 @@ function buildSetupServer(deps, options = {}) {
62183
62269
  if (error instanceof DeploymentConfigMissingSecretsError) return problem(reply, 400, error.message, error.code);
62184
62270
  if (error instanceof DeploymentConfigSecretRefreshRequiredError) return problem(reply, 400, error.message, error.code);
62185
62271
  if (error instanceof DeploymentConfigConflictError) return problem(reply, 409, error.message, error.code);
62272
+ if (error instanceof DeploymentConfigGitlabBaseUrlLockedError) return problem(reply, 409, error.message, error.code);
62186
62273
  if (error instanceof LogtoManagementError) return problem(reply, error.code === "LOGTO_UNAVAILABLE" ? 502 : error.code === "SOCIAL_CONNECTOR_UNSUPPORTED" ? 400 : 409, error.message, error.code);
62187
62274
  request.log.error({ err: error }, "setup-server request failed");
62188
62275
  return problem(reply, 500, "internal server error");
@@ -63056,6 +63143,7 @@ async function serveSetupServer(env = process.env) {
63056
63143
  if (!isLoopbackHostname(host) && !config.SETUP_SERVER_ALLOW_CONTAINER_PROXY) throw new Error("Setup Server may bind outside loopback only behind an isolated local port forward");
63057
63144
  const handle = openDeploymentConfigStore({
63058
63145
  databaseUrl: config.DATABASE_URL,
63146
+ ...env.GITLAB_BASE_URL ? { gitlabBaseUrl: env.GITLAB_BASE_URL } : {},
63059
63147
  SECRET_CIPHER: config.SECRET_CIPHER,
63060
63148
  VAULT_TRANSIT_KEY: config.VAULT_TRANSIT_KEY,
63061
63149
  VAULT_TRANSIT_MOUNT: config.VAULT_TRANSIT_MOUNT,