diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e36b28b67..054e948491 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: push: branches: [main] pull_request: - branches: [main] + branches: [main, mobile-provider-parity-1d93-s*] # Cancel in-progress runs when a new commit is pushed to the same PR concurrency: @@ -51,6 +51,11 @@ jobs: with: filters: | kilocode_backend: + - '.github/workflows/ci.yml' + - '.github/workflows/kilo-app-ci.yml' + - 'apps/mobile/src/components/pr-review/**' + - 'apps/mobile/src/lib/pr-review/**' + - 'apps/mobile/src/app/\(app\)/provider-review/**' - 'apps/web/src/**' - 'apps/web/.env' - 'apps/web/.env.test' diff --git a/.github/workflows/kilo-app-ci.yml b/.github/workflows/kilo-app-ci.yml index 7bbb23c1c2..e0278fec9b 100644 --- a/.github/workflows/kilo-app-ci.yml +++ b/.github/workflows/kilo-app-ci.yml @@ -16,7 +16,7 @@ on: - 'pnpm-lock.yaml' - '.github/workflows/kilo-app-ci.yml' pull_request: - branches: [main] + branches: [main, mobile-provider-parity-1d93-s*] paths: - 'apps/mobile/**' - 'packages/trpc/**' diff --git a/apps/web/package.json b/apps/web/package.json index 39e6e51e69..4dd195bb0e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -44,6 +44,8 @@ "@chat-adapter/state-redis": "4.36.0", "@emoji-mart/data": "1.2.1", "@emoji-mart/react": "1.1.1", + "@gitbeaker/requester-utils": "43.8.0", + "@gitbeaker/rest": "43.8.0", "@kilocode/app-shared": "workspace:*", "@kilocode/auto-routing-contracts": "workspace:*", "@kilocode/cloud-agent-profile": "workspace:*", @@ -190,6 +192,7 @@ "@types/linkify-it": "5.0.0", "@types/node": "catalog:", "@types/pg": "8.18.0", + "@types/picomatch": "2.3.4", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "@typescript/native-preview": "catalog:", @@ -201,6 +204,7 @@ "jest": "30.3.0", "knip": "5.86.0", "madge": "8.0.0", + "picomatch": "2.3.2", "postcss": "8.5.23", "tailwindcss": "4.2.4", "ts-jest": "29.4.9", diff --git a/apps/web/src/lib/integrations/core/types.ts b/apps/web/src/lib/integrations/core/types.ts index bab97daf8e..fd8bb565a8 100644 --- a/apps/web/src/lib/integrations/core/types.ts +++ b/apps/web/src/lib/integrations/core/types.ts @@ -17,11 +17,9 @@ export function requireNumericPlatformRepositories( return repositories; } -/** - * Represents ownership of an integration - * Can be either a user or an organization - */ -export type Owner = { type: 'user'; id: string } | { type: 'org'; id: string }; +// Compatibility: retain the old web import path until all Owner imports migrate. +// Database helpers and exports stay here; the shared Owner has no database dependency. +export type { Owner } from '@kilocode/app-shared/code-review/repository-identity'; export type WebhookEvent = { platform: string; diff --git a/apps/web/src/lib/stack-ci-triggers.test.ts b/apps/web/src/lib/stack-ci-triggers.test.ts new file mode 100644 index 0000000000..460c0a73ee --- /dev/null +++ b/apps/web/src/lib/stack-ci-triggers.test.ts @@ -0,0 +1,131 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { load } from 'js-yaml'; +import picomatch from 'picomatch'; + +// Match the pinned dorny/paths-filter action's picomatch 2.x behavior, including dotfiles. +type Step = { name?: string; id?: string; run?: string; with?: { filters?: string } }; +type Workflow = { + on: { + push: { branches: string[] }; + pull_request: { branches: string[]; paths?: string[] }; + pull_request_target?: unknown; + }; + jobs: Record< + string, + { if?: string; needs?: string[]; steps: Step[]; outputs?: Record } + >; +}; +const workflowFiles = ['ci.yml', 'kilo-app-ci.yml']; +const stackPattern = 'mobile-provider-parity-1d93-s*'; +const stackBranch = 'mobile-provider-parity-1d93-s2'; +const routePattern = String.raw`apps/mobile/src/app/\(app\)/provider-review/**`; +const routes = [ + 'apps/mobile/src/app/(app)/provider-review/_layout.tsx', + 'apps/mobile/src/app/(app)/provider-review/[identity]/(surface)/index.tsx', +]; +const source = (file: string) => + readFileSync(resolve(__dirname, '../../../../.github/workflows', file), 'utf8'); +const parse = (text: string) => load(text) as Workflow; +const ciSource = source('ci.yml'); +const ci = parse(ciSource); +const matches = (patterns: string[], value: string) => picomatch(patterns, { dot: true })(value); + +function matchesPullRequest(workflow: Workflow, branch: string, file: string) { + const trigger = workflow.on.pull_request; + return matches(trigger.branches, branch) && (!trigger.paths || matches(trigger.paths, file)); +} + +function backendPatterns(workflow: Workflow) { + const filters = workflow.jobs.changes.steps.find(step => step.id === 'filter')?.with?.filters; + if (typeof filters !== 'string') throw new Error('Missing paths-filter configuration'); + return (load(filters) as { kilocode_backend: string[] }).kilocode_backend; +} + +function selectsGuard(workflow: Workflow, branch: string, file: string) { + return matchesPullRequest(workflow, branch, file) && matches(backendPatterns(workflow), file); +} + +function assertTargets(workflow: Workflow) { + for (const branch of [ + 'main', + ...Array.from({ length: 21 }, (_, index) => `mobile-provider-parity-1d93-s${index + 1}`), + ]) { + expect(matchesPullRequest(workflow, branch, routes[0])).toBe(true); + } +} + +// Each row is a separate changed-path fixture. Server roots share the existing web pattern. +const monitoredPaths = [ + ['.github/workflows/ci.yml', '.github/workflows/ci.yml'], + ['.github/workflows/kilo-app-ci.yml', '.github/workflows/kilo-app-ci.yml'], + [ + 'apps/mobile/src/components/pr-review/new-consumer.tsx', + 'apps/mobile/src/components/pr-review/**', + ], + ['apps/mobile/src/lib/pr-review/nested/new-consumer.ts', 'apps/mobile/src/lib/pr-review/**'], + ...routes.map(path => [path, routePattern]), + ['apps/web/src/lib/provider-review/new-consumer.ts', 'apps/web/src/**'], + ['apps/web/src/routers/provider-review-router.ts', 'apps/web/src/**'], + ['apps/web/src/lib/provider-review-boundary.test.ts', 'apps/web/src/**'], + ['apps/web/src/lib/stack-ci-triggers.test.ts', 'apps/web/src/**'], +]; + +describe('standard stack CI guard execution', () => { + it.each(workflowFiles)( + '%s retains main and all stack targets without privileged triggers', + file => { + const workflow = parse(source(file)); + assertTargets(workflow); + expect(workflow.on.push.branches).toEqual(['main']); + expect(workflow.on).not.toHaveProperty('pull_request_target'); + } + ); + + it.each(monitoredPaths)('%s selects the guard and requires %s', (file, pattern) => { + for (const branch of ['main', stackBranch]) { + expect(selectsGuard(ci, branch, file)).toBe(true); + const removed = ciSource.replace(`- '${pattern}'`, ''); + expect(removed).not.toBe(ciSource); + expect(selectsGuard(parse(removed), branch, file)).toBe(false); + } + }); + + it.each(workflowFiles)('a workflow-only removal of the %s stack target fails the guard', file => { + expect(selectsGuard(ci, stackBranch, `.github/workflows/${file}`)).toBe(true); + const removed = parse(source(file).replace(stackPattern, 'main')); + expect(() => assertTargets(removed)).toThrow(); + }); + + it('preserves literal Expo parentheses through both YAML parses', () => { + expect( + selectsGuard(ci, stackBranch, 'apps/mobile/src/app/app/provider-review/_layout.tsx') + ).toBe(false); + const unescaped = parse( + ciSource.replace(routePattern, 'apps/mobile/src/app/(app)/provider-review/**') + ); + for (const file of routes) expect(selectsGuard(unescaped, stackBranch, file)).toBe(false); + }); + + it('keeps the existing prerequisites and root test command on the backend filter', () => { + expect(ci.jobs.changes.outputs?.kilocode_backend).toBe( + '${{ steps.filter.outputs.kilocode_backend }}' + ); + expect(ci.jobs.test.if).toBe("needs.changes.outputs.kilocode_backend == 'true'"); + expect(ci.jobs.test.needs).toEqual([ + 'changes', + 'typecheck', + 'lint', + 'format-check', + 'drizzle-check', + ]); + for (const prerequisite of ['typecheck', 'lint', 'format-check', 'drizzle-check']) { + expect(ci.jobs[prerequisite]).toBeDefined(); + expect(ci.jobs[prerequisite].if).toBeUndefined(); + } + expect(ci.jobs.test.steps.find(step => step.name === 'Run tests')).toEqual({ + name: 'Run tests', + run: 'pnpm run test', + }); + }); +}); diff --git a/packages/app-shared/package.json b/packages/app-shared/package.json index d6d2f8f4bc..4ff9501a90 100644 --- a/packages/app-shared/package.json +++ b/packages/app-shared/package.json @@ -8,6 +8,9 @@ "./utils": "./src/utils.ts", "./security-agent": "./src/security-agent/index.ts", "./code-review": "./src/code-review/index.ts", + "./code-review/repository-identity": "./src/code-review/repository-identity.ts", + "./provider-review": "./src/provider-review/index.ts", + "./provider-review/*": "./src/provider-review/*.ts", "./code-reviews": "./src/code-reviews/index.ts", "./organizations": "./src/organizations/index.ts", "./platforms": "./src/platforms.ts", diff --git a/packages/app-shared/src/code-review/repository-identity.test.ts b/packages/app-shared/src/code-review/repository-identity.test.ts new file mode 100644 index 0000000000..5349f3c97e --- /dev/null +++ b/packages/app-shared/src/code-review/repository-identity.test.ts @@ -0,0 +1,209 @@ +import { describe, expect, it } from 'vitest'; +import { + normalizeLegacyGitHubReviewRepository, + repositoryResourceKey, + requireLaunchRepository, + type GitHubUserAuthorization, + type LaunchRepositoryReference, + type RepositoryReference, +} from './repository-identity'; + +const accountId = 'oauth/account'; +const reference = { + repository: { + provider: 'github', + instanceUrl: 'https://github.com', + repositoryId: 'R_123', + fullName: 'Team/Repo', + defaultBranch: 'trunk', + }, + authorization: { + kind: 'ownerIntegration', + owner: { type: 'org', id: 'owner' }, + integrationId: 'integration', + }, +} satisfies LaunchRepositoryReference; +const { repository, authorization } = reference; +const userAuthorization: GitHubUserAuthorization = { + kind: 'githubUser', + accountId, + authorizationId: 'authorization', +}; +const review = { repository, authorization: userAuthorization }; +const key = (value: RepositoryReference, account = accountId) => + repositoryResourceKey(account, value); + +const variants: Record = { + 'authorization kind': review, + 'owner kind': { + ...reference, + authorization: { ...authorization, owner: { type: 'user', id: 'owner' } }, + }, + 'owner ID': { + ...reference, + authorization: { ...authorization, owner: { type: 'org', id: 'OWNER' } }, + }, + integration: { ...reference, authorization: { ...authorization, integrationId: 'other' } }, + provider: { ...reference, repository: { ...repository, provider: 'gitlab' } }, + instance: { ...reference, repository: { ...repository, instanceUrl: 'https://other.example' } }, + 'instance subpath': { + ...reference, + repository: { ...repository, instanceUrl: 'https://github.com/GitLab' }, + }, + 'repository ID': { ...reference, repository: { ...repository, repositoryId: 'r_123' } }, + 'repository path': { ...reference, repository: { ...repository, fullName: 'Team/Sub/Repo' } }, +}; + +describe('repository identity isolation', () => { + it.each(Object.entries(variants))( + 'does not restore a draft from another %s', + (_name, changed) => { + expect(new Map([[key(reference), 'saved draft']]).get(key(changed))).toBeUndefined(); + } + ); + + it('isolates the calling account, including case', () => { + expect(key(reference, 'oauth/ACCOUNT')).not.toBe(key(reference)); + }); + + it('canonicalizes the host, default port, trailing slash, and GitHub name case', () => { + const equivalent = { + ...reference, + repository: { ...repository, instanceUrl: 'https://GITHUB.com:443/', fullName: 'team/repo' }, + }; + expect(new Map([[key(reference), 'draft']]).get(key(equivalent))).toBe('draft'); + }); + + it('preserves nested GitLab paths and instance subpath case', () => { + const gitlab = { + ...reference, + repository: { + ...repository, + provider: 'gitlab', + instanceUrl: 'https://git.example/GitLab', + fullName: 'Group/Sub/Repo', + }, + } satisfies LaunchRepositoryReference; + for (const changed of [ + { fullName: 'group/sub/repo' }, + { fullName: 'Group/Repo' }, + { instanceUrl: 'https://git.example/gitlab' }, + ]) { + expect(key({ ...gitlab, repository: { ...gitlab.repository, ...changed } })).not.toBe( + key(gitlab) + ); + } + }); + + it('isolates Bitbucket workspace UUIDs, repository UUIDs, and paths', () => { + const bitbucket = { + ...reference, + repository: { + provider: 'bitbucket', + instanceUrl: 'https://bitbucket.org', + workspaceUuid: '{11111111-1111-4111-8111-111111111111}', + repositoryId: '{22222222-2222-4222-8222-222222222222}', + fullName: 'Workspace/Repo', + defaultBranch: null, + }, + } satisfies LaunchRepositoryReference; + for (const changed of [ + { workspaceUuid: '{33333333-3333-4333-8333-333333333333}' }, + { repositoryId: '{33333333-3333-4333-8333-333333333333}' }, + { fullName: 'workspace/repo' }, + ]) { + expect(key({ ...bitbucket, repository: { ...bitbucket.repository, ...changed } })).not.toBe( + key(bitbucket) + ); + } + }); + + it('does not collide when delimiters move between components', () => { + const first = { + ...reference, + authorization: { + ...authorization, + owner: { ...authorization.owner, id: 'owner:part' }, + integrationId: 'rest/#%"', + }, + }; + const second = { + ...reference, + authorization: { ...authorization, integrationId: 'part:rest/#%"' }, + }; + expect( + new Map([ + [key(first), 'first'], + [key(second), 'second'], + ]).size + ).toBe(2); + }); + + it('keeps drafts when only default-branch metadata changes', () => { + expect(key({ ...reference, repository: { ...repository, defaultBranch: null } })).toBe( + key(reference) + ); + }); + + it.each([ + 'invalid', + 'http://git.example', + 'https://user:secret@git.example', + 'https://git.example?query=1', + 'https://git.example#fragment', + ])('rejects an invalid instance: %s', instanceUrl => { + expect(() => key({ ...reference, repository: { ...repository, instanceUrl } })).toThrow(); + }); +}); + +describe('legacy GitHub review authorization', () => { + it.each([ + { installations: [] }, + { installations: ['unrelated-installation', 'another-installation'] }, + ])('resolves user access independently of installations: $installations', context => { + const input = { + ...context, + accountId, + repository: { repositoryId: 'R_123', fullName: 'Team/Repo' }, + authorization: userAuthorization, + }; + expect(normalizeLegacyGitHubReviewRepository(input)).toEqual({ + kind: 'resolved', + reference: { + repository: { ...repository, defaultBranch: null }, + authorization: userAuthorization, + }, + }); + }); + + it.each([ + { authorization: null, repository }, + { authorization: { ...userAuthorization, accountId: 'another-account' }, repository }, + { authorization: userAuthorization, repository: { fullName: 'Team/Repo' } }, + ])('quarantines unresolved identity without installation fallback: %j', changed => { + const input = { accountId, ...changed }; + expect(normalizeLegacyGitHubReviewRepository(input)).toEqual({ + kind: 'legacy-unresolved', + accountId, + repository: input.repository, + }); + }); + + it('isolates replacement user authorization', () => { + expect( + key({ ...review, authorization: { ...userAuthorization, authorizationId: 'replacement' } }) + ).not.toBe(key(review)); + }); + + it('rejects a user authorization from another account', () => { + expect(() => key(review, 'another-account')).toThrow('another account'); + }); + + it('rejects user authorization for launch', () => { + expect(() => requireLaunchRepository(review)).toThrow('ownerIntegration'); + }); + + it('preserves the exact owner integration for launch', () => { + expect(requireLaunchRepository(reference)).toEqual(reference); + }); +}); diff --git a/packages/app-shared/src/code-review/repository-identity.ts b/packages/app-shared/src/code-review/repository-identity.ts new file mode 100644 index 0000000000..5e74ffef68 --- /dev/null +++ b/packages/app-shared/src/code-review/repository-identity.ts @@ -0,0 +1,118 @@ +import { z } from 'zod'; +import type { CodeReviewPlatform } from './enums'; + +// Kilo ownership, not the provider's repository namespace. +export type Owner = { type: 'user'; id: string } | { type: 'org'; id: string }; + +// These values come from an authorized server lookup, never from a pasted URL alone. +export type RepositoryIdentity = { + instanceUrl: string; + repositoryId: string; + fullName: string; + defaultBranch: string | null; +} & ( + | { provider: Exclude; workspaceUuid?: never } + | { provider: 'bitbucket'; workspaceUuid: string } +); + +export type OwnerIntegrationAuthorization = { + kind: 'ownerIntegration'; + owner: Owner; + integrationId: string; +}; + +export type GitHubUserAuthorization = { + kind: 'githubUser'; + accountId: string; + authorizationId: string; +}; + +export type RepositoryAuthorization = OwnerIntegrationAuthorization | GitHubUserAuthorization; +export type LaunchRepositoryReference = { + repository: RepositoryIdentity; + authorization: OwnerIntegrationAuthorization; +}; +export type GitHubReviewRepositoryReference = { + repository: RepositoryIdentity & { provider: 'github' }; + authorization: GitHubUserAuthorization; +}; +export type RepositoryReference = LaunchRepositoryReference | GitHubReviewRepositoryReference; + +export function requireLaunchRepository(reference: RepositoryReference): LaunchRepositoryReference { + if (reference.authorization.kind !== 'ownerIntegration') { + throw new Error('Launch requires ownerIntegration authorization'); + } + return { repository: reference.repository, authorization: reference.authorization }; +} + +// Normalize URL syntax, excluding credentials, queries, and fragments. This does not authorize a host. +const instanceUrlSchema = z + .url({ protocol: /^https$/, normalize: true }) + .regex(/^https:\/\/[^/?#@]+(?:\/[^?#]*)?$/); + +// JSON tuples encode components without delimiter collisions. This namespace does +// not replace the legacy GitHub ledger keys or intent-fingerprint bytes. +export function repositoryResourceKey(accountId: string, reference: RepositoryReference): string { + const { repository, authorization } = reference; + if (authorization.kind === 'githubUser' && authorization.accountId !== accountId) { + throw new Error('GitHub authorization belongs to another account'); + } + return JSON.stringify([ + 'provider-repository:v1', + accountId, + authorization.kind === 'ownerIntegration' + ? [ + authorization.kind, + authorization.owner.type, + authorization.owner.id, + authorization.integrationId, + ] + : [authorization.kind, authorization.accountId, authorization.authorizationId], + repository.provider, + instanceUrlSchema.parse(repository.instanceUrl).replace(/\/+$/, ''), + repository.provider === 'bitbucket' ? repository.workspaceUuid : null, + repository.repositoryId, + repository.provider === 'github' ? repository.fullName.toLowerCase() : repository.fullName, + ]); +} + +type LegacyGitHubRepository = { + provider?: 'github'; + instanceUrl?: string; + repositoryId?: string; + fullName: string; + defaultBranch?: string | null; +}; + +// Old GitHub records omit provider/instance/defaultBranch and sometimes repository ID. +// Remove this fallback only after old clients/records disappear and the 30-day ledger window expires. +// The caller supplies user authorization and the resolved repository ID; installations play no part. +export function normalizeLegacyGitHubReviewRepository(input: { + accountId: string; + repository: LegacyGitHubRepository; + authorization: GitHubUserAuthorization | null; +}): + | { kind: 'resolved'; reference: GitHubReviewRepositoryReference } + | { kind: 'legacy-unresolved'; accountId: string; repository: LegacyGitHubRepository } { + const { accountId, repository, authorization } = input; + if ( + !authorization?.authorizationId || + authorization.accountId !== accountId || + !repository.repositoryId + ) { + return { kind: 'legacy-unresolved', accountId, repository }; + } + return { + kind: 'resolved', + reference: { + repository: { + provider: repository.provider ?? 'github', + instanceUrl: repository.instanceUrl ?? 'https://github.com', + repositoryId: repository.repositoryId, + fullName: repository.fullName, + defaultBranch: repository.defaultBranch ?? null, + }, + authorization, + }, + }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be89a656d4..3cdd25f10e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -786,6 +786,12 @@ importers: '@emoji-mart/react': specifier: 1.1.1 version: 1.1.1(emoji-mart@5.6.0)(react@19.2.6) + '@gitbeaker/requester-utils': + specifier: 43.8.0 + version: 43.8.0 + '@gitbeaker/rest': + specifier: 43.8.0 + version: 43.8.0 '@kilocode/app-shared': specifier: workspace:* version: link:../../packages/app-shared @@ -1219,6 +1225,9 @@ importers: '@types/pg': specifier: 8.18.0 version: 8.18.0 + '@types/picomatch': + specifier: 2.3.4 + version: 2.3.4 '@types/react': specifier: 19.2.14 version: 19.2.14 @@ -1252,6 +1261,9 @@ importers: madge: specifier: 8.0.0 version: 8.0.0(typescript@5.9.3) + picomatch: + specifier: 2.3.2 + version: 2.3.2 postcss: specifier: '>=8.5.23' version: 8.5.25 @@ -2482,6 +2494,9 @@ importers: drizzle-orm: specifier: 0.45.2 version: 0.45.2(@cloudflare/workers-types@4.20260605.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(expo-sqlite@57.0.1(expo@57.0.10)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(kysely@0.29.2)(pg@8.20.0) + openapi-fetch: + specifier: 0.17.0 + version: 0.17.0 zod: specifier: 'catalog:' version: 4.4.3 @@ -2492,6 +2507,9 @@ importers: '@typescript/native-preview': specifier: 'catalog:' version: 7.0.0-dev.20260514.1 + openapi-typescript: + specifier: 7.13.0 + version: 7.13.0(typescript@5.9.3) typescript: specifier: 'catalog:' version: 5.9.3 @@ -5266,6 +5284,18 @@ packages: resolution: {integrity: sha512-PyUXQWB42s4jBli435TDiYuVsadwRHnMc27YaLouINktvTWsL3FcKrRMGawTayFk46X+n5bE23RjUTWQwrukWw==} engines: {node: '>= 0.10.0'} + '@gitbeaker/core@43.8.0': + resolution: {integrity: sha512-H+LfKuf4dExBinb79c+CXViRBvTVQNf5BYLNSizm2SiqdED5JruhKX88payefleY0szp7G/mySlFSXPyGRH1dQ==} + engines: {node: '>=18.20.0'} + + '@gitbeaker/requester-utils@43.8.0': + resolution: {integrity: sha512-d/SiJdxijc+aH5ZBQOw83XLxNSXqsBZNm5k3nPu1EHxGxK0fajXmxdMl0/vNXbKRggnIquFCxURkrQSEzfjqxQ==} + engines: {node: '>=18.20.0'} + + '@gitbeaker/rest@43.8.0': + resolution: {integrity: sha512-xxqsNsUXaFang9b2e/NTIgqUeuUlifA2Opy1mOVqTDuJZZNIOTgUNyziwBJoleBhMC0XuvY3JNVMWthufcVjRw==} + engines: {node: '>=18.20.0'} + '@google/genai@2.10.0': resolution: {integrity: sha512-e4cFxj3tiuMtsgOT4G9c1hXyGJhg7/Buj7VVeBacRY3fRtkRZZ59Q3nuVp2xbq8BGQXLXCDB253qMhklMOeUDg==} engines: {node: '>=20.0.0'} @@ -7930,6 +7960,16 @@ packages: peerDependencies: '@redis/client': ^5.11.0 + '@redocly/ajv@8.11.2': + resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} + + '@redocly/config@0.22.0': + resolution: {integrity: sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==} + + '@redocly/openapi-core@1.34.19': + resolution: {integrity: sha512-o/0VgsBXgwcY1lyeqcVtSGdTQAPnVggo0fbFVPlxl5XVDKUcVH0OLRqt3CbkwByT5FU305E0iE0O7MzThjDblw==} + engines: {node: '>=18.17.0', npm: '>=9.5.0'} + '@reduxjs/toolkit@2.11.2': resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} peerDependencies: @@ -9881,6 +9921,9 @@ packages: '@types/pg@8.18.0': resolution: {integrity: sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==} + '@types/picomatch@2.3.4': + resolution: {integrity: sha512-0so8lU8O5zatZS/2Fi4zrwks+vZv7e0dygrgEZXljODXBig97l4cPQD+9LabXfGJOWwoRkTVz6Q4edZvD12UOA==} + '@types/proper-lockfile@4.1.4': resolution: {integrity: sha512-uo2ABllncSqg9F1D4nugVl9v93RmjxF6LJzQLMLDdPaXCUIDPeOJ21Gbqi43xNKzBi/WQ0Q0dICqufzQbMjipQ==} @@ -10529,6 +10572,10 @@ packages: ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -11120,6 +11167,9 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -11379,6 +11429,9 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -14369,6 +14422,10 @@ packages: js-cookie@3.0.8: resolution: {integrity: sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==} + js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + js-tokens@10.0.0: resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} @@ -15613,6 +15670,18 @@ packages: zod: optional: true + openapi-fetch@0.17.0: + resolution: {integrity: sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig==} + + openapi-typescript-helpers@0.1.0: + resolution: {integrity: sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw==} + + openapi-typescript@7.13.0: + resolution: {integrity: sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==} + hasBin: true + peerDependencies: + typescript: ^5.x + openclaw@2026.7.1: resolution: {integrity: sha512-ge/Xss99CHAjPL/ikmH/UFoiOrjcxDB4sW3y9mhyCD+dYW3wzV7TKbAVdkrXFgAG2d2BjpJofP97zUZ+umxo8g==} engines: {node: '>=22.22.3 <23 || >=24.15.0 <25 || >=25.9.0'} @@ -15909,6 +15978,10 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch-browser@2.2.6: + resolution: {integrity: sha512-0ypsOQt9D4e3hziV8O4elD9uN0z/jtUEfxVRtNaAAtXIyUx9m/SzlO020i8YNL2aL/E6blOvvHQcin6HZlFy/w==} + engines: {node: '>=8.6'} + picomatch@2.3.2: resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} @@ -16340,6 +16413,9 @@ packages: resolution: {integrity: sha512-CX4nij6+ZLHYIaojJNfLTr7W+AiH/IPJi6E9Aw1br2///1KZL2KBOHd68rkcLedc47MPvb4hhH+fzYeGFa4A/Q==} engines: {node: '>=22'} + rate-limiter-flexible@8.3.0: + resolution: {integrity: sha512-mzwlfipDLlRinPgELqVDJetke6Snq26nL565m8nLWXIcWgosYSeNRgqwh7ZrZ4MfYs8CNfmLvR5SBVz3rISQsQ==} + raw-body@3.0.2: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} @@ -18099,6 +18175,9 @@ packages: resolution: {integrity: sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA==} engines: {node: '>=18'} + uri-js-replace@1.0.1: + resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -18598,6 +18677,9 @@ packages: x-path@0.0.2: resolution: {integrity: sha512-zQ4WFI0XfJN1uEkkrB19Y4TuXOlHqKSxUJo0Yt+axPjRm8tCG6SJ6+Wo3/+Kjg4c2c8IvBXuJ0uYoshxNn4qMw==} + xcase@2.0.1: + resolution: {integrity: sha512-UmFXIPU+9Eg3E9m/728Bii0lAIuoc+6nbrNUKaRPJOFp91ih44qqGlWtxMB6kXFrRD6po+86ksHM5XHCfk6iPw==} + xcode@3.0.1: resolution: {integrity: sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==} engines: {node: '>=10.0.0'} @@ -18653,6 +18735,9 @@ packages: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} + yaml-ast-parser@0.0.43: + resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} + yaml-loader@0.8.1: resolution: {integrity: sha512-BCEndnUoi3BaZmePkwGGe93txRxLgMhBa/gE725v1/GHnura8QvNs7c4+4C1yyhhKoj3Dg63M7IqhA++15j6ww==} engines: {node: '>= 14'} @@ -21937,6 +22022,24 @@ snapshots: '@fregante/relaxed-json@2.0.0': {} + '@gitbeaker/core@43.8.0': + dependencies: + '@gitbeaker/requester-utils': 43.8.0 + qs: 6.15.2 + xcase: 2.0.1 + + '@gitbeaker/requester-utils@43.8.0': + dependencies: + picomatch-browser: 2.2.6 + qs: 6.15.2 + rate-limiter-flexible: 8.3.0 + xcase: 2.0.1 + + '@gitbeaker/rest@43.8.0': + dependencies: + '@gitbeaker/core': 43.8.0 + '@gitbeaker/requester-utils': 43.8.0 + '@google/genai@2.10.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@6.0.6)': dependencies: google-auth-library: 10.6.2 @@ -24844,6 +24947,29 @@ snapshots: dependencies: '@redis/client': 5.11.0 + '@redocly/ajv@8.11.2': + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js-replace: 1.0.1 + + '@redocly/config@0.22.0': {} + + '@redocly/openapi-core@1.34.19': + dependencies: + '@redocly/ajv': 8.11.2 + '@redocly/config': 0.22.0 + colorette: 1.4.0 + https-proxy-agent: 7.0.6 + js-levenshtein: 1.1.6 + js-yaml: 4.3.1 + minimatch: 5.1.9 + pluralize: 8.0.0 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - supports-color + '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1))(react@19.2.6)': dependencies: '@standard-schema/spec': 1.1.0 @@ -27039,6 +27165,8 @@ snapshots: pg-protocol: 1.13.0 pg-types: 2.2.0 + '@types/picomatch@2.3.4': {} + '@types/proper-lockfile@4.1.4': dependencies: '@types/retry': 0.12.0 @@ -27738,6 +27866,8 @@ snapshots: dependencies: string-width: 4.2.3 + ansi-colors@4.1.3: {} + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -28443,6 +28573,8 @@ snapshots: chalk@5.6.2: {} + change-case@5.4.4: {} + char-regex@1.0.2: {} character-entities-html4@2.1.0: {} @@ -28701,6 +28833,8 @@ snapshots: color-convert: 2.0.1 color-string: 1.9.1 + colorette@1.4.0: {} + colorette@2.0.20: {} colorjs.io@0.6.0-alpha.1: {} @@ -32509,6 +32643,8 @@ snapshots: js-cookie@3.0.8: {} + js-levenshtein@1.1.6: {} + js-tokens@10.0.0: {} js-tokens@4.0.0: {} @@ -34273,6 +34409,22 @@ snapshots: ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) zod: 4.4.3 + openapi-fetch@0.17.0: + dependencies: + openapi-typescript-helpers: 0.1.0 + + openapi-typescript-helpers@0.1.0: {} + + openapi-typescript@7.13.0(typescript@5.9.3): + dependencies: + '@redocly/openapi-core': 1.34.19 + ansi-colors: 4.1.3 + change-case: 5.4.4 + parse-json: 8.3.0 + supports-color: 10.2.2 + typescript: 5.9.3 + yargs-parser: 21.1.1 + openclaw@2026.7.1(@aws-sdk/credential-provider-node@3.972.21)(@opentelemetry/api@1.9.1)(bufferutil@4.1.0)(utf-8-validate@6.0.6): dependencies: '@agentclientprotocol/sdk': 1.1.0(zod@4.4.3) @@ -34778,6 +34930,8 @@ snapshots: picocolors@1.1.1: {} + picomatch-browser@2.2.6: {} + picomatch@2.3.2: {} picomatch@4.0.4: {} @@ -35213,6 +35367,8 @@ snapshots: dependencies: '@silvia-odwyer/photon-node': 0.3.4 + rate-limiter-flexible@8.3.0: {} + raw-body@3.0.2: dependencies: bytes: 3.1.2 @@ -37467,6 +37623,8 @@ snapshots: semver: 7.8.5 xdg-basedir: 5.1.0 + uri-js-replace@1.0.1: {} + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -38427,6 +38585,8 @@ snapshots: dependencies: path-extra: 1.0.3 + xcase@2.0.1: {} + xcode@3.0.1: dependencies: simple-plist: 1.3.1 @@ -38466,6 +38626,8 @@ snapshots: yallist@5.0.0: {} + yaml-ast-parser@0.0.43: {} + yaml-loader@0.8.1: dependencies: javascript-stringify: 2.1.0 diff --git a/services/git-token-service/package.json b/services/git-token-service/package.json index 2a95234c66..cbf26b76aa 100644 --- a/services/git-token-service/package.json +++ b/services/git-token-service/package.json @@ -19,11 +19,13 @@ "@octokit/auth-app": "catalog:", "@octokit/rest": "22.0.1", "drizzle-orm": "catalog:", + "openapi-fetch": "0.17.0", "zod": "catalog:" }, "devDependencies": { "@types/node": "catalog:", "@typescript/native-preview": "catalog:", + "openapi-typescript": "7.13.0", "typescript": "catalog:", "vitest": "catalog:", "wrangler": "catalog:"