feat(app): allow non-admin developers to claim and list marketplace apps (#22621)
## Context Follow-up to #22609. Lets a non-admin developer claim ownership of a public Twenty app they published to npm, then request a marketplace listing that a server admin reviews. Marketplace state is per-instance for now. ## Claiming - Developer tab gets a **Claim an application** section: look up an unclaimed npm app by package name or universal identifier. - Ownership is proven with GitHub OAuth against the package's npm provenance (trusted publishing): the connected account must own the GitHub account or organization the package was published from. - Errors from the GitHub callback come back as a code and are shown inline with a link to the relevant documentation. - The old one-click claim stays admin-only. - A **Sync catalog** button triggers a catalog refresh instead of waiting for the hourly cron. - Gated behind the `IS_APP_CLAIMING_ENABLED` feature flag. ## Listing requests - Catalog-synced apps are created **unlisted**; a data migration unlists previously auto-listed unclaimed npm apps (owned or vetted rows are left untouched). - Owners request a listing from the Distribution tab (logo + description required); a server admin approves or rejects it from a **Listing requests** section in the Admin Panel. ## Screenshots <img width="1512" height="829" alt="image" src="https://github.com/user-attachments/assets/788d4362-97c4-4e42-810c-ef1f11517bec"/> <img width="1512" height="829" alt="image" src="https://github.com/user-attachments/assets/d6246190-c82a-4f64-87be-3bb668527645"/> <img width="1512" height="828" alt="image" src="https://github.com/user-attachments/assets/21a8dad4-610b-4d1f-8948-b9acab40d373"/> <img width="1512" height="829" alt="image" src="https://github.com/user-attachments/assets/58246130-41f7-451e-ae7f-57bd21d04bb6"/> --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -14,6 +14,15 @@ List the top things your app does, for example:
|
||||
|
||||
Setup instructions live in [SETUP.md](SETUP.md).
|
||||
|
||||
## Publishing
|
||||
|
||||
The `Publish` workflow (`.github/workflows/publish.yml`) publishes the app to npm with provenance using [npm trusted publishing](https://docs.npmjs.com/trusted-publishers). To publish:
|
||||
|
||||
1. On npmjs.com register this repository as a trusted publisher of your package, pointing at the `publish.yml` workflow.
|
||||
2. Bump the version in `package.json`, then push a version tag (e.g. `git tag v1.0.0 && git push --tags`) or run the workflow manually from the Actions tab.
|
||||
|
||||
Publishing with provenance is also how you prove ownership when claiming your app in a Twenty marketplace.
|
||||
|
||||
## Changelog
|
||||
|
||||
Notable changes are documented in [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Publishes the app to npm with provenance (npm trusted publishing).
|
||||
#
|
||||
# One-time setup:
|
||||
# 1. On npmjs.com open your package > Settings > Trusted Publisher and register
|
||||
# this repository and this workflow file (publish.yml).
|
||||
# See https://docs.npmjs.com/trusted-publishers
|
||||
# 2. Push a version tag (e.g. `git tag v1.0.0 && git push --tags`) or run the
|
||||
# workflow manually from the Actions tab.
|
||||
#
|
||||
# Publishing with provenance certifies which GitHub repository the package is
|
||||
# built from. This is also how you claim ownership of your app in a Twenty
|
||||
# marketplace: the claim flow verifies you own the GitHub account or
|
||||
# organization the provenance points to.
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# Required for npm trusted publishing (OIDC provenance).
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Update npm
|
||||
# Trusted publishing requires npm 11.5.1 or later.
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Publish to npm
|
||||
run: yarn twenty app:publish
|
||||
@@ -1727,6 +1727,7 @@ type FeatureFlag {
|
||||
}
|
||||
|
||||
enum FeatureFlagKey {
|
||||
IS_APP_CLAIMING_ENABLED
|
||||
IS_UNIQUE_INDEXES_ENABLED
|
||||
IS_JSON_FILTER_ENABLED
|
||||
IS_CALENDAR_WEEK_VIEW_ENABLED
|
||||
@@ -1947,6 +1948,17 @@ type UsageBreakdownItem {
|
||||
creditsUsed: Float!
|
||||
}
|
||||
|
||||
type ClaimableApplicationRegistration {
|
||||
id: String!
|
||||
universalIdentifier: String!
|
||||
name: String!
|
||||
sourcePackage: String
|
||||
logoUrl: String
|
||||
description: String
|
||||
author: String
|
||||
isOwned: Boolean!
|
||||
}
|
||||
|
||||
type CreateApplicationRegistration {
|
||||
applicationRegistration: ApplicationRegistration!
|
||||
clientSecret: String!
|
||||
@@ -3116,6 +3128,8 @@ type Query {
|
||||
findApplicationRegistrationStats(id: String!): ApplicationRegistrationStats!
|
||||
findApplicationRegistrationVariables(applicationRegistrationId: String!): [ApplicationRegistrationVariableDTO!]!
|
||||
applicationRegistrationTarballUrl(id: String!): String
|
||||
findClaimableApplicationRegistration(sourcePackage: String, universalIdentifier: String): ClaimableApplicationRegistration
|
||||
githubClaimAuthorizationUrl(applicationRegistrationId: String!): String!
|
||||
getRoles: [Role!]!
|
||||
previewMessageCampaignAudience(input: PreviewMessageCampaignAudienceInput!): CampaignAudiencePreviewDTO!
|
||||
unsubscribeTopics: [UnsubscribeTopic!]!
|
||||
|
||||
@@ -1390,7 +1390,7 @@ export interface FeatureFlag {
|
||||
__typename: 'FeatureFlag'
|
||||
}
|
||||
|
||||
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_CALENDAR_WEEK_VIEW_ENABLED' | 'IS_EMAIL_GROUP_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' | 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED' | 'IS_SETTINGS_DISCOVERY_HERO_ENABLED' | 'IS_WORKFLOW_VERSION_IN_CORE_ENABLED'
|
||||
export type FeatureFlagKey = 'IS_APP_CLAIMING_ENABLED' | 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_CALENDAR_WEEK_VIEW_ENABLED' | 'IS_EMAIL_GROUP_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' | 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED' | 'IS_SETTINGS_DISCOVERY_HERO_ENABLED' | 'IS_WORKFLOW_VERSION_IN_CORE_ENABLED'
|
||||
|
||||
export interface WorkspaceUrls {
|
||||
customUrl?: Scalars['String']
|
||||
@@ -1604,6 +1604,18 @@ export interface UsageBreakdownItem {
|
||||
__typename: 'UsageBreakdownItem'
|
||||
}
|
||||
|
||||
export interface ClaimableApplicationRegistration {
|
||||
id: Scalars['String']
|
||||
universalIdentifier: Scalars['String']
|
||||
name: Scalars['String']
|
||||
sourcePackage?: Scalars['String']
|
||||
logoUrl?: Scalars['String']
|
||||
description?: Scalars['String']
|
||||
author?: Scalars['String']
|
||||
isOwned: Scalars['Boolean']
|
||||
__typename: 'ClaimableApplicationRegistration'
|
||||
}
|
||||
|
||||
export interface CreateApplicationRegistration {
|
||||
applicationRegistration: ApplicationRegistration
|
||||
clientSecret: Scalars['String']
|
||||
@@ -2756,6 +2768,8 @@ export interface Query {
|
||||
findApplicationRegistrationStats: ApplicationRegistrationStats
|
||||
findApplicationRegistrationVariables: ApplicationRegistrationVariableDTO[]
|
||||
applicationRegistrationTarballUrl?: Scalars['String']
|
||||
findClaimableApplicationRegistration?: ClaimableApplicationRegistration
|
||||
githubClaimAuthorizationUrl: Scalars['String']
|
||||
getRoles: Role[]
|
||||
previewMessageCampaignAudience: CampaignAudiencePreviewDTO
|
||||
unsubscribeTopics: UnsubscribeTopic[]
|
||||
@@ -4699,6 +4713,19 @@ export interface UsageBreakdownItemGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ClaimableApplicationRegistrationGenqlSelection{
|
||||
id?: boolean | number
|
||||
universalIdentifier?: boolean | number
|
||||
name?: boolean | number
|
||||
sourcePackage?: boolean | number
|
||||
logoUrl?: boolean | number
|
||||
description?: boolean | number
|
||||
author?: boolean | number
|
||||
isOwned?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface CreateApplicationRegistrationGenqlSelection{
|
||||
applicationRegistration?: ApplicationRegistrationGenqlSelection
|
||||
clientSecret?: boolean | number
|
||||
@@ -5946,6 +5973,8 @@ export interface QueryGenqlSelection{
|
||||
findApplicationRegistrationStats?: (ApplicationRegistrationStatsGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
findApplicationRegistrationVariables?: (ApplicationRegistrationVariableDTOGenqlSelection & { __args: {applicationRegistrationId: Scalars['String']} })
|
||||
applicationRegistrationTarballUrl?: { __args: {id: Scalars['String']} }
|
||||
findClaimableApplicationRegistration?: (ClaimableApplicationRegistrationGenqlSelection & { __args?: {sourcePackage?: (Scalars['String'] | null), universalIdentifier?: (Scalars['String'] | null)} })
|
||||
githubClaimAuthorizationUrl?: { __args: {applicationRegistrationId: Scalars['String']} }
|
||||
getRoles?: RoleGenqlSelection
|
||||
previewMessageCampaignAudience?: (CampaignAudiencePreviewDTOGenqlSelection & { __args: {input: PreviewMessageCampaignAudienceInput} })
|
||||
unsubscribeTopics?: UnsubscribeTopicGenqlSelection
|
||||
@@ -7702,6 +7731,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const ClaimableApplicationRegistration_possibleTypes: string[] = ['ClaimableApplicationRegistration']
|
||||
export const isClaimableApplicationRegistration = (obj?: { __typename?: any } | null): obj is ClaimableApplicationRegistration => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isClaimableApplicationRegistration"')
|
||||
return ClaimableApplicationRegistration_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const CreateApplicationRegistration_possibleTypes: string[] = ['CreateApplicationRegistration']
|
||||
export const isCreateApplicationRegistration = (obj?: { __typename?: any } | null): obj is CreateApplicationRegistration => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isCreateApplicationRegistration"')
|
||||
@@ -9162,6 +9199,7 @@ export const enumLogicFunctionExecutionStatus = {
|
||||
}
|
||||
|
||||
export const enumFeatureFlagKey = {
|
||||
IS_APP_CLAIMING_ENABLED: 'IS_APP_CLAIMING_ENABLED' as const,
|
||||
IS_UNIQUE_INDEXES_ENABLED: 'IS_UNIQUE_INDEXES_ENABLED' as const,
|
||||
IS_JSON_FILTER_ENABLED: 'IS_JSON_FILTER_ENABLED' as const,
|
||||
IS_CALENDAR_WEEK_VIEW_ENABLED: 'IS_CALENDAR_WEEK_VIEW_ENABLED' as const,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,7 @@ my-twenty-app/
|
||||
.github/workflows/
|
||||
ci.yml # Lint, typecheck, unit + integration tests
|
||||
cd.yml # Deploy + install on push to main
|
||||
publish.yml # Publish to npm on version tags (with provenance)
|
||||
public/
|
||||
logo.svg # Static assets
|
||||
vitest.config.ts # Integration test runner config
|
||||
|
||||
@@ -112,7 +112,7 @@ The server is the authoritative check — it validates `engines.twenty` on both
|
||||
|
||||
## Automated CI/CD (scaffolded workflows)
|
||||
|
||||
Apps generated with `create-twenty-app` ship with two GitHub Actions workflows out of the box, under `.github/workflows/`. They are ready to run as soon as you push the repo to GitHub — no extra setup is needed for CI, and CD only requires a single secret.
|
||||
Apps generated with `create-twenty-app` ship with three GitHub Actions workflows out of the box, under `.github/workflows/`. CI runs with no setup, CD requires a single secret, and publishing to npm requires a one-time npm trusted-publisher setup.
|
||||
|
||||
### CI — `ci.yml`
|
||||
|
||||
@@ -157,9 +157,22 @@ The default `TWENTY_DEPLOY_URL` of `http://localhost:3000` is a placeholder —
|
||||
|
||||
Add the `deploy` label to a pull request. The `if:` guard in `cd.yml` will run the job for that PR using the PR's head commit, letting you validate a change on the target server before merging.
|
||||
|
||||
### Publish — `publish.yml`
|
||||
|
||||
Publishes your app to npm with provenance when you push a version tag (e.g. `v1.0.0`), or when you run the workflow manually from the Actions tab.
|
||||
|
||||
**What it does:**
|
||||
|
||||
1. Checks out your app, sets up Node.js, and updates npm (trusted publishing requires npm 11.5.1 or later).
|
||||
2. Runs `yarn twenty app:publish`, which builds the app and publishes `.twenty/output` to npm. In CI it automatically adds `--provenance` and `--access public`, so no flags are needed in the workflow.
|
||||
|
||||
**One-time setup:**
|
||||
|
||||
On npmjs.com open your package > **Settings → Trusted Publisher** and register this repository with the `publish.yml` workflow (see the [npm trusted publishing docs](https://docs.npmjs.com/trusted-publishers)). Publishing with provenance certifies which GitHub repository built the package, which is also how you claim ownership of your app in a Twenty marketplace.
|
||||
|
||||
### Pinning the reusable actions
|
||||
|
||||
Both workflows reference reusable actions at `@main`, so action updates in the `twentyhq/twenty` repo are picked up automatically. If you want deterministic builds, replace `@main` with a commit SHA or release tag on each `uses:` line.
|
||||
The `ci.yml` and `cd.yml` workflows reference reusable actions at `@main`, so action updates in the `twentyhq/twenty` repo are picked up automatically. If you want deterministic builds, replace `@main` with a commit SHA or release tag on each `uses:` line.
|
||||
|
||||
## Publishing to npm
|
||||
|
||||
@@ -241,37 +254,12 @@ If your app does not define an `aboutDescription` in `defineApplication()`, the
|
||||
|
||||
### CI publishing
|
||||
|
||||
Use this GitHub Actions workflow to publish automatically on every release (uses [OIDC](https://docs.npmjs.com/trusted-publishers)):
|
||||
The scaffolded `publish.yml` workflow described above publishes to npm automatically on version tags, with provenance. Because `yarn twenty app:publish` adds `--provenance` and `--access public` for you when it runs in CI, the workflow needs no npm flags — only the one-time trusted-publisher setup.
|
||||
|
||||
```yaml filename=".github/workflows/publish.yml"
|
||||
name: Publish
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
registry-url: https://registry.npmjs.org
|
||||
- run: yarn install --immutable
|
||||
- run: npx twenty dev:build
|
||||
- run: npm publish --provenance --access public
|
||||
working-directory: .twenty/output
|
||||
```
|
||||
|
||||
For other CI systems (GitLab CI, CircleCI, etc.), the same three commands apply: `yarn install`, `yarn twenty dev:build`, then `npm publish` from `.twenty/output`.
|
||||
For other CI systems (GitLab CI, CircleCI, etc.), run `yarn install` then `yarn twenty app:publish`. Provenance is emitted when the environment can mint an OIDC token and skipped automatically otherwise.
|
||||
|
||||
<Note>
|
||||
**npm provenance** is optional but recommended. Publishing with `--provenance` adds a trust badge to your npm listing, letting users verify the package was built from a specific commit in a public CI pipeline. See the [npm provenance docs](https://docs.npmjs.com/generating-provenance-statements) for setup instructions.
|
||||
**npm provenance** adds a trust badge to your npm listing, letting users verify the package was built from a specific commit in a public CI pipeline. It is also what lets you claim ownership of your app in a Twenty marketplace. See the [npm provenance docs](https://docs.npmjs.com/generating-provenance-statements) for details.
|
||||
</Note>
|
||||
|
||||
## Installing apps
|
||||
|
||||
@@ -259,4 +259,4 @@ This runs `tsc --noEmit` against your app's `tsconfig.json` and reports any type
|
||||
|
||||
The scaffolder generates a ready-to-use workflow at `.github/workflows/ci.yml`. On every push to `main` and every pull request, it spawns an ephemeral Twenty server in the runner (via the `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test` action), then runs `yarn lint`, `yarn typecheck`, `yarn test:unit`, and `yarn test` with `TWENTY_API_URL` / `TWENTY_API_KEY` pointing at that server. No secrets are required, and you can pin the server version via the `TWENTY_VERSION` env at the top of the workflow.
|
||||
|
||||
See [Publishing → Automated CI/CD](/developers/extend/apps/operations/publishing#automated-cicd-scaffolded-workflows) for a full walkthrough of both scaffolded workflows (`ci.yml` and the `cd.yml` deploy pipeline).
|
||||
See [Publishing → Automated CI/CD](/developers/extend/apps/operations/publishing#automated-cicd-scaffolded-workflows) for a full walkthrough of the three scaffolded workflows (`ci.yml`, the `cd.yml` deploy pipeline, and `publish.yml` for npm publishing).
|
||||
|
||||
@@ -46,6 +46,12 @@ export type AdminAiModels = {
|
||||
models: Array<AdminAiModelConfig>;
|
||||
};
|
||||
|
||||
export type AdminApplicationRegistrationClaim = {
|
||||
__typename?: 'AdminApplicationRegistrationClaim';
|
||||
workspaceDisplayName?: Maybe<Scalars['String']['output']>;
|
||||
workspaceId: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type AdminChatMessage = {
|
||||
__typename?: 'AdminChatMessage';
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
@@ -314,6 +320,7 @@ export type FeatureFlag = {
|
||||
};
|
||||
|
||||
export enum FeatureFlagKey {
|
||||
IS_APP_CLAIMING_ENABLED = 'IS_APP_CLAIMING_ENABLED',
|
||||
IS_CALENDAR_WEEK_VIEW_ENABLED = 'IS_CALENDAR_WEEK_VIEW_ENABLED',
|
||||
IS_EMAIL_GROUP_ENABLED = 'IS_EMAIL_GROUP_ENABLED',
|
||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||
@@ -596,6 +603,7 @@ export type Query = {
|
||||
__typename?: 'Query';
|
||||
adminPanelRecentUsers: Array<AdminPanelRecentUser>;
|
||||
adminPanelTopWorkspaces: Array<AdminPanelTopWorkspace>;
|
||||
findAdminApplicationRegistrationClaims: Array<AdminApplicationRegistrationClaim>;
|
||||
findAdminApplicationRegistrationInstalledWorkspaces: ApplicationRegistrationInstalledWorkspaces;
|
||||
findAdminApplicationRegistrationStats: ApplicationRegistrationStats;
|
||||
findAdminApplicationRegistrationVariables: Array<ApplicationRegistrationVariableDto>;
|
||||
@@ -636,6 +644,11 @@ export type QueryAdminPanelTopWorkspacesArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryFindAdminApplicationRegistrationClaimsArgs = {
|
||||
applicationRegistrationId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryFindAdminApplicationRegistrationInstalledWorkspacesArgs = {
|
||||
input: FindApplicationRegistrationInstalledWorkspacesInput;
|
||||
};
|
||||
@@ -1107,6 +1120,13 @@ export type UpgradeRegistrationApplicationsMutationVariables = Exact<{
|
||||
|
||||
export type UpgradeRegistrationApplicationsMutation = { __typename?: 'Mutation', upgradeRegistrationApplications: boolean };
|
||||
|
||||
export type FindAdminApplicationRegistrationClaimsQueryVariables = Exact<{
|
||||
applicationRegistrationId: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type FindAdminApplicationRegistrationClaimsQuery = { __typename?: 'Query', findAdminApplicationRegistrationClaims: Array<{ __typename?: 'AdminApplicationRegistrationClaim', workspaceId: string, workspaceDisplayName?: string | null }> };
|
||||
|
||||
export type FindAdminApplicationRegistrationInstalledWorkspacesQueryVariables = Exact<{
|
||||
input: FindApplicationRegistrationInstalledWorkspacesInput;
|
||||
}>;
|
||||
@@ -1377,6 +1397,7 @@ export const SyncMarketplaceCatalogDocument = {"kind":"Document","definitions":[
|
||||
export const UpdateAdminApplicationRegistrationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateAdminApplicationRegistration"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateApplicationRegistrationInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateAdminApplicationRegistration"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ApplicationRegistrationFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ApplicationRegistrationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ApplicationRegistration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"galleryImagesUrls"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthClientId"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthRedirectUris"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthScopes"}},{"kind":"Field","name":{"kind":"Name","value":"sourceType"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"latestAvailableVersion"}},{"kind":"Field","name":{"kind":"Name","value":"isListed"}},{"kind":"Field","name":{"kind":"Name","value":"isVetted"}},{"kind":"Field","name":{"kind":"Name","value":"isPreInstalled"}},{"kind":"Field","name":{"kind":"Name","value":"isConfigured"}},{"kind":"Field","name":{"kind":"Name","value":"ownerWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<UpdateAdminApplicationRegistrationMutation, UpdateAdminApplicationRegistrationMutationVariables>;
|
||||
export const UpdateAdminApplicationRegistrationVariableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateAdminApplicationRegistrationVariable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateApplicationRegistrationVariableInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateAdminApplicationRegistrationVariable"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"isSecret"}},{"kind":"Field","name":{"kind":"Name","value":"isRequired"}},{"kind":"Field","name":{"kind":"Name","value":"isFilled"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<UpdateAdminApplicationRegistrationVariableMutation, UpdateAdminApplicationRegistrationVariableMutationVariables>;
|
||||
export const UpgradeRegistrationApplicationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpgradeRegistrationApplications"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"upgradeRegistrationApplications"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"applicationRegistrationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}}}]}]}}]} as unknown as DocumentNode<UpgradeRegistrationApplicationsMutation, UpgradeRegistrationApplicationsMutationVariables>;
|
||||
export const FindAdminApplicationRegistrationClaimsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindAdminApplicationRegistrationClaims"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findAdminApplicationRegistrationClaims"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"applicationRegistrationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceDisplayName"}}]}}]}}]} as unknown as DocumentNode<FindAdminApplicationRegistrationClaimsQuery, FindAdminApplicationRegistrationClaimsQueryVariables>;
|
||||
export const FindAdminApplicationRegistrationInstalledWorkspacesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindAdminApplicationRegistrationInstalledWorkspaces"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FindApplicationRegistrationInstalledWorkspacesInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findAdminApplicationRegistrationInstalledWorkspaces"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"hasMore"}},{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"version"}}]}}]}}]}}]} as unknown as DocumentNode<FindAdminApplicationRegistrationInstalledWorkspacesQuery, FindAdminApplicationRegistrationInstalledWorkspacesQueryVariables>;
|
||||
export const FindAdminApplicationRegistrationStatsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindAdminApplicationRegistrationStats"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findAdminApplicationRegistrationStats"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeInstalls"}},{"kind":"Field","name":{"kind":"Name","value":"mostInstalledVersion"}},{"kind":"Field","name":{"kind":"Name","value":"versionDistribution"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}}]}}]}}]} as unknown as DocumentNode<FindAdminApplicationRegistrationStatsQuery, FindAdminApplicationRegistrationStatsQueryVariables>;
|
||||
export const FindAdminApplicationRegistrationVariablesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindAdminApplicationRegistrationVariables"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findAdminApplicationRegistrationVariables"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"applicationRegistrationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"isSecret"}},{"kind":"Field","name":{"kind":"Name","value":"isRequired"}},{"kind":"Field","name":{"kind":"Name","value":"isFilled"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"options"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<FindAdminApplicationRegistrationVariablesQuery, FindAdminApplicationRegistrationVariablesQueryVariables>;
|
||||
|
||||
@@ -886,6 +886,18 @@ export type CheckUserExist = {
|
||||
isEmailVerified: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
export type ClaimableApplicationRegistration = {
|
||||
__typename?: 'ClaimableApplicationRegistration';
|
||||
author?: Maybe<Scalars['String']['output']>;
|
||||
description?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['String']['output'];
|
||||
isOwned: Scalars['Boolean']['output'];
|
||||
logoUrl?: Maybe<Scalars['String']['output']>;
|
||||
name: Scalars['String']['output'];
|
||||
sourcePackage?: Maybe<Scalars['String']['output']>;
|
||||
universalIdentifier: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type ClientAiModelConfig = {
|
||||
__typename?: 'ClientAiModelConfig';
|
||||
contextWindowTokens?: Maybe<Scalars['Float']['output']>;
|
||||
@@ -1739,6 +1751,7 @@ export type FeatureFlag = {
|
||||
};
|
||||
|
||||
export enum FeatureFlagKey {
|
||||
IS_APP_CLAIMING_ENABLED = 'IS_APP_CLAIMING_ENABLED',
|
||||
IS_CALENDAR_WEEK_VIEW_ENABLED = 'IS_CALENDAR_WEEK_VIEW_ENABLED',
|
||||
IS_EMAIL_GROUP_ENABLED = 'IS_EMAIL_GROUP_ENABLED',
|
||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||
@@ -4355,6 +4368,7 @@ export type Query = {
|
||||
findApplicationRegistrationByUniversalIdentifier?: Maybe<ApplicationRegistration>;
|
||||
findApplicationRegistrationStats: ApplicationRegistrationStats;
|
||||
findApplicationRegistrationVariables: Array<ApplicationRegistrationVariableDto>;
|
||||
findClaimableApplicationRegistration?: Maybe<ClaimableApplicationRegistration>;
|
||||
findManyAgents: Array<Agent>;
|
||||
findManyApplicationRegistrations: Array<ApplicationRegistration>;
|
||||
findManyApplications: Array<Application>;
|
||||
@@ -4409,6 +4423,7 @@ export type Query = {
|
||||
getViewSorts: Array<ViewSort>;
|
||||
getViews: Array<View>;
|
||||
getWorkspaceCreationDefaults: WorkspaceCreationDefaultsDto;
|
||||
githubClaimAuthorizationUrl: Scalars['String']['output'];
|
||||
lineChartData: LineChartData;
|
||||
listPlans: Array<BillingPlan>;
|
||||
minimalMetadata: MinimalMetadata;
|
||||
@@ -4559,6 +4574,12 @@ export type QueryFindApplicationRegistrationVariablesArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryFindClaimableApplicationRegistrationArgs = {
|
||||
sourcePackage?: InputMaybe<Scalars['String']['input']>;
|
||||
universalIdentifier?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryFindManyMarketplaceAppsArgs = {
|
||||
universalIdentifiers?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
};
|
||||
@@ -4751,6 +4772,11 @@ export type QueryGetViewsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryGithubClaimAuthorizationUrlArgs = {
|
||||
applicationRegistrationId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryLineChartDataArgs = {
|
||||
input: LineChartDataInput;
|
||||
};
|
||||
@@ -7224,6 +7250,11 @@ export type InstallApplicationMutationVariables = Exact<{
|
||||
|
||||
export type InstallApplicationMutation = { __typename?: 'Mutation', installApplication: { __typename?: 'Application', id: string } };
|
||||
|
||||
export type SyncMarketplaceCatalogMutationVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type SyncMarketplaceCatalogMutation = { __typename?: 'Mutation', syncMarketplaceCatalog: boolean };
|
||||
|
||||
export type UpgradeApplicationMutationVariables = Exact<{
|
||||
appRegistrationId: Scalars['String']['input'];
|
||||
targetVersion: Scalars['String']['input'];
|
||||
@@ -7813,6 +7844,14 @@ export type FindApplicationRegistrationVariablesQueryVariables = Exact<{
|
||||
|
||||
export type FindApplicationRegistrationVariablesQuery = { __typename?: 'Query', findApplicationRegistrationVariables: Array<{ __typename?: 'ApplicationRegistrationVariableDTO', id: string, key: string, value?: string | null, description: string, isSecret: boolean, isRequired: boolean, isFilled: boolean, type: string, options?: any | null, createdAt: string, updatedAt: string }> };
|
||||
|
||||
export type FindClaimableApplicationRegistrationQueryVariables = Exact<{
|
||||
sourcePackage?: InputMaybe<Scalars['String']['input']>;
|
||||
universalIdentifier?: InputMaybe<Scalars['String']['input']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type FindClaimableApplicationRegistrationQuery = { __typename?: 'Query', findClaimableApplicationRegistration?: { __typename?: 'ClaimableApplicationRegistration', id: string, universalIdentifier: string, name: string, sourcePackage?: string | null, logoUrl?: string | null, description?: string | null, author?: string | null, isOwned: boolean } | null };
|
||||
|
||||
export type ApplicationRegistrationListItemFragment = { __typename?: 'ApplicationRegistration', id: string, universalIdentifier: string, name: string, sourceType: ApplicationRegistrationSourceType, logoUrl?: string | null };
|
||||
|
||||
export type FindManyApplicationRegistrationsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
@@ -7827,6 +7866,13 @@ export type FindOneApplicationRegistrationQueryVariables = Exact<{
|
||||
|
||||
export type FindOneApplicationRegistrationQuery = { __typename?: 'Query', findOneApplicationRegistration: { __typename?: 'ApplicationRegistration', id: string, universalIdentifier: string, name: string, logoUrl?: string | null, galleryImagesUrls: Array<string>, oAuthClientId: string, oAuthRedirectUris: Array<string>, oAuthScopes: Array<string>, sourceType: ApplicationRegistrationSourceType, sourcePackage?: string | null, latestAvailableVersion?: string | null, isListed: boolean, isVetted: boolean, isPreInstalled: boolean, isConfigured: boolean, ownerWorkspaceId?: string | null, createdAt: string, updatedAt: string } };
|
||||
|
||||
export type GithubClaimAuthorizationUrlQueryVariables = Exact<{
|
||||
applicationRegistrationId: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GithubClaimAuthorizationUrlQuery = { __typename?: 'Query', githubClaimAuthorizationUrl: string };
|
||||
|
||||
export type UninstallApplicationMutationVariables = Exact<{
|
||||
universalIdentifier: Scalars['String']['input'];
|
||||
}>;
|
||||
@@ -8940,6 +8986,7 @@ export const FindManyLogicFunctionsDocument = {"kind":"Document","definitions":[
|
||||
export const FindOneLogicFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneLogicFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"LogicFunctionIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findOneLogicFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LogicFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LogicFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LogicFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"timeoutSeconds"}},{"kind":"Field","name":{"kind":"Name","value":"executionMode"}},{"kind":"Field","name":{"kind":"Name","value":"sourceHandlerPath"}},{"kind":"Field","name":{"kind":"Name","value":"handlerName"}},{"kind":"Field","name":{"kind":"Name","value":"cronTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"databaseEventTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"httpRouteTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"toolTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"workflowActionTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<FindOneLogicFunctionQuery, FindOneLogicFunctionQueryVariables>;
|
||||
export const GetLogicFunctionSourceCodeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetLogicFunctionSourceCode"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"LogicFunctionIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getLogicFunctionSourceCode"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<GetLogicFunctionSourceCodeQuery, GetLogicFunctionSourceCodeQueryVariables>;
|
||||
export const InstallApplicationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InstallApplication"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"installApplication"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"universalIdentifier"},"value":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<InstallApplicationMutation, InstallApplicationMutationVariables>;
|
||||
export const SyncMarketplaceCatalogDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncMarketplaceCatalog"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"syncMarketplaceCatalog"}}]}}]} as unknown as DocumentNode<SyncMarketplaceCatalogMutation, SyncMarketplaceCatalogMutationVariables>;
|
||||
export const UpgradeApplicationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpgradeApplication"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appRegistrationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"targetVersion"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"upgradeApplication"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appRegistrationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appRegistrationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"targetVersion"},"value":{"kind":"Variable","name":{"kind":"Name","value":"targetVersion"}}}]}]}}]} as unknown as DocumentNode<UpgradeApplicationMutation, UpgradeApplicationMutationVariables>;
|
||||
export const FindManyMarketplaceAppsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindManyMarketplaceApps"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifiers"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findManyMarketplaceApps"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"universalIdentifiers"},"value":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifiers"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"MarketplaceAppFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"MarketplaceAppFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"MarketplaceApp"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"author"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"isVetted"}}]}}]} as unknown as DocumentNode<FindManyMarketplaceAppsQuery, FindManyMarketplaceAppsQueryVariables>;
|
||||
export const FindMarketplaceAppDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindMarketplaceAppDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findMarketplaceAppDetail"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"universalIdentifier"},"value":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"MarketplaceAppDetailFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"MarketplaceAppDetailFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"MarketplaceAppDetail"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sourceType"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"latestAvailableVersion"}},{"kind":"Field","name":{"kind":"Name","value":"isListed"}},{"kind":"Field","name":{"kind":"Name","value":"isVetted"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"author"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"websiteUrl"}},{"kind":"Field","name":{"kind":"Name","value":"aboutDescription"}},{"kind":"Field","name":{"kind":"Name","value":"termsUrl"}},{"kind":"Field","name":{"kind":"Name","value":"emailSupport"}},{"kind":"Field","name":{"kind":"Name","value":"issueReportUrl"}},{"kind":"Field","name":{"kind":"Name","value":"galleryImages"}},{"kind":"Field","name":{"kind":"Name","value":"defaultRoleUniversalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"roles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"canUpdateAllSettings"}},{"kind":"Field","name":{"kind":"Name","value":"canAccessAllTools"}},{"kind":"Field","name":{"kind":"Name","value":"canReadAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canUpdateAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canSoftDeleteAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canDestroyAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"permissionFlagUniversalIdentifiers"}},{"kind":"Field","name":{"kind":"Name","value":"objectPermissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"objectUniversalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"canReadObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canUpdateObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canSoftDeleteObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canDestroyObjectRecords"}}]}},{"kind":"Field","name":{"kind":"Name","value":"fieldPermissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"objectUniversalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"fieldUniversalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"canReadFieldValue"}},{"kind":"Field","name":{"kind":"Name","value":"canUpdateFieldValue"}}]}}]}}]}}]} as unknown as DocumentNode<FindMarketplaceAppDetailQuery, FindMarketplaceAppDetailQueryVariables>;
|
||||
@@ -8997,8 +9044,10 @@ export const UpdateApplicationRegistrationVariableDocument = {"kind":"Document",
|
||||
export const ApplicationRegistrationTarballUrlDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ApplicationRegistrationTarballUrl"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"applicationRegistrationTarballUrl"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}]}}]} as unknown as DocumentNode<ApplicationRegistrationTarballUrlQuery, ApplicationRegistrationTarballUrlQueryVariables>;
|
||||
export const FindApplicationRegistrationStatsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindApplicationRegistrationStats"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findApplicationRegistrationStats"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeInstalls"}},{"kind":"Field","name":{"kind":"Name","value":"mostInstalledVersion"}},{"kind":"Field","name":{"kind":"Name","value":"versionDistribution"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}}]}}]}}]} as unknown as DocumentNode<FindApplicationRegistrationStatsQuery, FindApplicationRegistrationStatsQueryVariables>;
|
||||
export const FindApplicationRegistrationVariablesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindApplicationRegistrationVariables"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findApplicationRegistrationVariables"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"applicationRegistrationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"isSecret"}},{"kind":"Field","name":{"kind":"Name","value":"isRequired"}},{"kind":"Field","name":{"kind":"Name","value":"isFilled"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"options"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<FindApplicationRegistrationVariablesQuery, FindApplicationRegistrationVariablesQueryVariables>;
|
||||
export const FindClaimableApplicationRegistrationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindClaimableApplicationRegistration"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"sourcePackage"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findClaimableApplicationRegistration"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"sourcePackage"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sourcePackage"}}},{"kind":"Argument","name":{"kind":"Name","value":"universalIdentifier"},"value":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"author"}},{"kind":"Field","name":{"kind":"Name","value":"isOwned"}}]}}]}}]} as unknown as DocumentNode<FindClaimableApplicationRegistrationQuery, FindClaimableApplicationRegistrationQueryVariables>;
|
||||
export const FindManyApplicationRegistrationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindManyApplicationRegistrations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findManyApplicationRegistrations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ApplicationRegistrationListItem"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ApplicationRegistrationListItem"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ApplicationRegistration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sourceType"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}}]}}]} as unknown as DocumentNode<FindManyApplicationRegistrationsQuery, FindManyApplicationRegistrationsQueryVariables>;
|
||||
export const FindOneApplicationRegistrationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneApplicationRegistration"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findOneApplicationRegistration"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ApplicationRegistrationFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ApplicationRegistrationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ApplicationRegistration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"galleryImagesUrls"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthClientId"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthRedirectUris"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthScopes"}},{"kind":"Field","name":{"kind":"Name","value":"sourceType"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"latestAvailableVersion"}},{"kind":"Field","name":{"kind":"Name","value":"isListed"}},{"kind":"Field","name":{"kind":"Name","value":"isVetted"}},{"kind":"Field","name":{"kind":"Name","value":"isPreInstalled"}},{"kind":"Field","name":{"kind":"Name","value":"isConfigured"}},{"kind":"Field","name":{"kind":"Name","value":"ownerWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<FindOneApplicationRegistrationQuery, FindOneApplicationRegistrationQueryVariables>;
|
||||
export const GithubClaimAuthorizationUrlDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GithubClaimAuthorizationUrl"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"githubClaimAuthorizationUrl"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"applicationRegistrationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}}}]}]}}]} as unknown as DocumentNode<GithubClaimAuthorizationUrlQuery, GithubClaimAuthorizationUrlQueryVariables>;
|
||||
export const UninstallApplicationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UninstallApplication"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"uninstallApplication"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"universalIdentifier"},"value":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}}}]}]}}]} as unknown as DocumentNode<UninstallApplicationMutation, UninstallApplicationMutationVariables>;
|
||||
export const UpdateApplicationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateApplication"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateApplicationInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateApplication"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"autoUpgrade"}}]}}]}}]} as unknown as DocumentNode<UpdateApplicationMutation, UpdateApplicationMutationVariables>;
|
||||
export const UpdateOneApplicationVariableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateOneApplicationVariable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"value"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"applicationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateOneApplicationVariable"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"key"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}},{"kind":"Argument","name":{"kind":"Name","value":"value"},"value":{"kind":"Variable","name":{"kind":"Name","value":"value"}}},{"kind":"Argument","name":{"kind":"Name","value":"applicationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"applicationId"}}}]}]}}]} as unknown as DocumentNode<UpdateOneApplicationVariableMutation, UpdateOneApplicationVariableMutationVariables>;
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const SYNC_MARKETPLACE_CATALOG = gql`
|
||||
mutation SyncMarketplaceCatalog {
|
||||
syncMarketplaceCatalog
|
||||
}
|
||||
`;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const FIND_ADMIN_APPLICATION_REGISTRATION_CLAIMS = gql`
|
||||
query FindAdminApplicationRegistrationClaims(
|
||||
$applicationRegistrationId: String!
|
||||
) {
|
||||
findAdminApplicationRegistrationClaims(
|
||||
applicationRegistrationId: $applicationRegistrationId
|
||||
) {
|
||||
workspaceId
|
||||
workspaceDisplayName
|
||||
}
|
||||
}
|
||||
`;
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const FIND_CLAIMABLE_APPLICATION_REGISTRATION = gql`
|
||||
query FindClaimableApplicationRegistration(
|
||||
$sourcePackage: String
|
||||
$universalIdentifier: String
|
||||
) {
|
||||
findClaimableApplicationRegistration(
|
||||
sourcePackage: $sourcePackage
|
||||
universalIdentifier: $universalIdentifier
|
||||
) {
|
||||
id
|
||||
universalIdentifier
|
||||
name
|
||||
sourcePackage
|
||||
logoUrl
|
||||
description
|
||||
author
|
||||
isOwned
|
||||
}
|
||||
}
|
||||
`;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GITHUB_CLAIM_AUTHORIZATION_URL = gql`
|
||||
query GithubClaimAuthorizationUrl($applicationRegistrationId: String!) {
|
||||
githubClaimAuthorizationUrl(
|
||||
applicationRegistrationId: $applicationRegistrationId
|
||||
)
|
||||
}
|
||||
`;
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableBody } from '@/ui/layout/table/components/TableBody';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { Tag } from 'twenty-ui/data-display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { H2Title } from 'twenty-ui/typography';
|
||||
import { FindAdminApplicationRegistrationClaimsDocument } from '~/generated-admin/graphql';
|
||||
|
||||
const CLAIMS_TABLE_GRID = '1fr 140px';
|
||||
|
||||
export const SettingsAdminApplicationRegistrationClaims = ({
|
||||
applicationRegistrationId,
|
||||
}: {
|
||||
applicationRegistrationId: string;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
const apolloAdminClient = useApolloAdminClient();
|
||||
|
||||
const { data } = useQuery(FindAdminApplicationRegistrationClaimsDocument, {
|
||||
client: apolloAdminClient,
|
||||
variables: { applicationRegistrationId },
|
||||
});
|
||||
|
||||
const claims = data?.findAdminApplicationRegistrationClaims ?? [];
|
||||
|
||||
if (claims.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Ownership`}
|
||||
description={t`The workspace that claimed this app registration`}
|
||||
/>
|
||||
<Table>
|
||||
<TableRow gridAutoColumns={CLAIMS_TABLE_GRID}>
|
||||
<TableHeader>{t`Workspace`}</TableHeader>
|
||||
<TableHeader>{t`Status`}</TableHeader>
|
||||
</TableRow>
|
||||
<TableBody>
|
||||
{claims.map((claim) => (
|
||||
<TableRow
|
||||
key={claim.workspaceId}
|
||||
gridAutoColumns={CLAIMS_TABLE_GRID}
|
||||
>
|
||||
<TableCell overflow="hidden">
|
||||
{claim.workspaceDisplayName ?? claim.workspaceId}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Tag text={t`Owner`} color="green" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
+19
-17
@@ -215,23 +215,25 @@ export const SettingsAdminApplicationRegistrationDangerZone = ({
|
||||
delay={TooltipDelay.shortDelay}
|
||||
/>
|
||||
)}
|
||||
{isUnclaimed ? (
|
||||
<Button
|
||||
accent="default"
|
||||
variant="secondary"
|
||||
title={t`Claim ownership`}
|
||||
Icon={IconUserPlus}
|
||||
onClick={() => openModal(CLAIM_OWNERSHIP_MODAL_ID)}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
accent="default"
|
||||
variant="secondary"
|
||||
title={t`Transfer ownership`}
|
||||
Icon={IconShare}
|
||||
onClick={() => openModal(TRANSFER_OWNERSHIP_MODAL_ID)}
|
||||
/>
|
||||
)}
|
||||
{isUnclaimed
|
||||
? fromAdmin && (
|
||||
<Button
|
||||
accent="default"
|
||||
variant="secondary"
|
||||
title={t`Claim ownership`}
|
||||
Icon={IconUserPlus}
|
||||
onClick={() => openModal(CLAIM_OWNERSHIP_MODAL_ID)}
|
||||
/>
|
||||
)
|
||||
: !isUnclaimed && (
|
||||
<Button
|
||||
accent="default"
|
||||
variant="secondary"
|
||||
title={t`Transfer ownership`}
|
||||
Icon={IconShare}
|
||||
onClick={() => openModal(TRANSFER_OWNERSHIP_MODAL_ID)}
|
||||
/>
|
||||
)}
|
||||
</StyledDangerButtonGroup>
|
||||
</Section>
|
||||
|
||||
|
||||
+1
@@ -81,6 +81,7 @@ export const SettingsAdminApplicationRegistrationDetail = () => {
|
||||
return (
|
||||
<SettingsApplicationRegistrationDistributionTab
|
||||
registration={registration}
|
||||
fromAdmin
|
||||
/>
|
||||
);
|
||||
case 'general':
|
||||
|
||||
@@ -12,12 +12,14 @@ import { Section } from 'twenty-ui/layout';
|
||||
import coverDark from '~/pages/settings/applications/assets/cover-dark.png';
|
||||
import coverLight from '~/pages/settings/applications/assets/cover-light.png';
|
||||
import { PermissionFlagType } from '~/generated-metadata/graphql';
|
||||
import { SettingsApplicationsClaimErrorTabEffect } from '~/pages/settings/applications/components/SettingsApplicationsClaimErrorTabEffect';
|
||||
import { SettingsApplicationsAvailableTab } from '~/pages/settings/applications/tabs/SettingsApplicationsAvailableTab';
|
||||
import { SettingsApplicationsDeveloperTab } from '~/pages/settings/applications/tabs/SettingsApplicationsDeveloperTab';
|
||||
import { SettingsApplicationsInstalledTab } from '~/pages/settings/applications/tabs/SettingsApplicationsInstalledTab';
|
||||
|
||||
const APPLICATIONS_TAB_LIST_ID = 'applications-tab-list';
|
||||
const APPLICATIONS_HERO_INSTANCE_ID_PREFIX = 'settings-applications-hero';
|
||||
const DEVELOPER_TAB_ID = 'developer';
|
||||
|
||||
export const SettingsApplications = () => {
|
||||
const { t } = useLingui();
|
||||
@@ -30,7 +32,7 @@ export const SettingsApplications = () => {
|
||||
{ id: 'marketplace', title: t`Marketplace`, Icon: IconDownload },
|
||||
{ id: 'installed', title: t`Installed`, Icon: IconApps },
|
||||
...(hasDeveloperAccess
|
||||
? [{ id: 'developer', title: t`Developer`, Icon: IconCode }]
|
||||
? [{ id: DEVELOPER_TAB_ID, title: t`Developer`, Icon: IconCode }]
|
||||
: []),
|
||||
];
|
||||
|
||||
@@ -69,6 +71,11 @@ export const SettingsApplications = () => {
|
||||
{ children: t`Applications` },
|
||||
]}
|
||||
>
|
||||
<SettingsApplicationsClaimErrorTabEffect
|
||||
tabListId={APPLICATIONS_TAB_LIST_ID}
|
||||
developerTabId={DEVELOPER_TAB_ID}
|
||||
hasDeveloperAccess={hasDeveloperAccess}
|
||||
/>
|
||||
<SettingsPageContainer>
|
||||
<Section>
|
||||
<SettingsDiscoveryHeroCard
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { CLAIM_ERROR_CODE_SEARCH_PARAM } from '~/pages/settings/applications/components/SettingsClaimApplicationSection';
|
||||
|
||||
type SettingsApplicationsClaimErrorTabEffectProps = {
|
||||
tabListId: string;
|
||||
developerTabId: string;
|
||||
hasDeveloperAccess: boolean;
|
||||
};
|
||||
|
||||
// The GitHub claim callback returns here with a claim error code, but the URL
|
||||
// hash that selects the developer tab can be dropped on the way back, so
|
||||
// select it from the query param instead.
|
||||
export const SettingsApplicationsClaimErrorTabEffect = ({
|
||||
tabListId,
|
||||
developerTabId,
|
||||
hasDeveloperAccess,
|
||||
}: SettingsApplicationsClaimErrorTabEffectProps) => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const hasClaimError = searchParams.has(CLAIM_ERROR_CODE_SEARCH_PARAM);
|
||||
|
||||
const setActiveTabId = useSetAtomComponentState(
|
||||
activeTabIdComponentState,
|
||||
tabListId,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasClaimError && hasDeveloperAccess) {
|
||||
setActiveTabId(developerTabId);
|
||||
}
|
||||
}, [hasClaimError, hasDeveloperAccess, developerTabId, setActiveTabId]);
|
||||
|
||||
return null;
|
||||
};
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
import { ApplicationDisplay } from '@/applications/components/ApplicationDisplay';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { getDocumentationUrl } from '@/support/utils/getDocumentationUrl';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useLazyQuery, useMutation } from '@apollo/client/react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { i18n } from '@lingui/core';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Link,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
useSearchParams,
|
||||
} from 'react-router-dom';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import { Callout } from 'twenty-ui/feedback';
|
||||
import { IconBrandGithub, IconRefresh, IconSearch } from 'twenty-ui/icon';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { H2Title } from 'twenty-ui/typography';
|
||||
import {
|
||||
FindClaimableApplicationRegistrationDocument,
|
||||
GithubClaimAuthorizationUrlDocument,
|
||||
PermissionFlagType,
|
||||
SyncMarketplaceCatalogDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
|
||||
import { getClaimErrorContent } from '~/pages/settings/applications/utils/getClaimErrorContent';
|
||||
|
||||
export const CLAIM_ERROR_CODE_SEARCH_PARAM = 'claimErrorCode';
|
||||
|
||||
const UUID_REGEX =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
const StyledRow = styled.div`
|
||||
align-items: flex-end;
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const StyledResultCard = styled.div`
|
||||
border: 1px solid ${themeCssVariables.border.color.medium};
|
||||
border-radius: ${themeCssVariables.border.radius.md};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[3]};
|
||||
margin-top: ${themeCssVariables.spacing[3]};
|
||||
padding: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
const StyledResultTitleLink = styled(Link)`
|
||||
align-self: flex-start;
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
text-decoration: none;
|
||||
|
||||
:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledHint = styled.div`
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
`;
|
||||
|
||||
const StyledCalloutContainer = styled.div`
|
||||
margin-bottom: ${themeCssVariables.spacing[3]};
|
||||
|
||||
& > div {
|
||||
max-width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const SettingsClaimApplicationSection = () => {
|
||||
const { t } = useLingui();
|
||||
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
|
||||
|
||||
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
|
||||
const [searchParams] = useSearchParams();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const claimErrorCode = searchParams.get(CLAIM_ERROR_CODE_SEARCH_PARAM);
|
||||
|
||||
const [lookupValue, setLookupValue] = useState('');
|
||||
const [notFound, setNotFound] = useState(false);
|
||||
|
||||
const dismissClaimError = () => {
|
||||
const nextSearchParams = new URLSearchParams(searchParams);
|
||||
|
||||
nextSearchParams.delete(CLAIM_ERROR_CODE_SEARCH_PARAM);
|
||||
|
||||
navigate(
|
||||
{ search: nextSearchParams.toString(), hash: location.hash },
|
||||
{ replace: true },
|
||||
);
|
||||
};
|
||||
|
||||
const claimError = isDefined(claimErrorCode)
|
||||
? getClaimErrorContent(claimErrorCode)
|
||||
: null;
|
||||
|
||||
const canSyncCatalog = useHasPermissionFlag(
|
||||
PermissionFlagType.MARKETPLACE_APPS,
|
||||
);
|
||||
|
||||
const [runLookup, { data: lookupData, loading: isLookingUp }] = useLazyQuery(
|
||||
FindClaimableApplicationRegistrationDocument,
|
||||
{ fetchPolicy: 'network-only' },
|
||||
);
|
||||
|
||||
const [getGithubAuthorizationUrl, { loading: isRedirectingToGithub }] =
|
||||
useLazyQuery(GithubClaimAuthorizationUrlDocument, {
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
const [syncCatalog, { loading: isSyncing }] = useMutation(
|
||||
SyncMarketplaceCatalogDocument,
|
||||
);
|
||||
|
||||
const registration = lookupData?.findClaimableApplicationRegistration ?? null;
|
||||
|
||||
const handleLookup = async () => {
|
||||
const trimmed = lookupValue.trim();
|
||||
|
||||
if (trimmed.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setNotFound(false);
|
||||
|
||||
const variables = UUID_REGEX.test(trimmed)
|
||||
? { universalIdentifier: trimmed }
|
||||
: { sourcePackage: trimmed };
|
||||
|
||||
try {
|
||||
const result = await runLookup({ variables });
|
||||
|
||||
if (!isDefined(result.data?.findClaimableApplicationRegistration)) {
|
||||
setNotFound(true);
|
||||
}
|
||||
} catch (error) {
|
||||
enqueueErrorSnackBar({
|
||||
message:
|
||||
error instanceof Error ? error.message : t`Could not run the lookup`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleClaimWithGithub = async () => {
|
||||
if (!isDefined(registration)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await getGithubAuthorizationUrl({
|
||||
variables: { applicationRegistrationId: registration.id },
|
||||
});
|
||||
|
||||
const authorizationUrl = result.data?.githubClaimAuthorizationUrl;
|
||||
|
||||
if (!isDefined(authorizationUrl)) {
|
||||
throw new Error(result.error?.message ?? 'Missing authorization URL');
|
||||
}
|
||||
|
||||
window.location.href = authorizationUrl;
|
||||
} catch (error) {
|
||||
enqueueErrorSnackBar({
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t`Could not start the GitHub claim`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSync = async () => {
|
||||
try {
|
||||
await syncCatalog();
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Catalog sync started. Try your lookup again in a moment.`,
|
||||
});
|
||||
} catch (error) {
|
||||
enqueueErrorSnackBar({
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t`Could not sync the catalog`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renderCardTitle = (app: {
|
||||
universalIdentifier: string;
|
||||
name: string;
|
||||
logoUrl?: string | null;
|
||||
}) => (
|
||||
<StyledResultTitleLink
|
||||
to={getSettingsPath(SettingsPath.AvailableApplicationDetail, {
|
||||
availableApplicationId: app.universalIdentifier,
|
||||
})}
|
||||
>
|
||||
<ApplicationDisplay application={{ name: app.name, logo: app.logoUrl }} />
|
||||
</StyledResultTitleLink>
|
||||
);
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Claim an application`}
|
||||
description={t`Take ownership of an app you published to npm. Enter its exact package name (or universal identifier) to find it.`}
|
||||
/>
|
||||
{isDefined(claimError) && (
|
||||
<StyledCalloutContainer>
|
||||
<Callout
|
||||
variant="error"
|
||||
title={t`Could not claim this application`}
|
||||
description={i18n._(claimError.message)}
|
||||
action={{
|
||||
label: t`Read documentation`,
|
||||
onClick: () =>
|
||||
window.open(
|
||||
getDocumentationUrl({
|
||||
locale: currentWorkspaceMember?.locale,
|
||||
path: claimError.docPath,
|
||||
}),
|
||||
'_blank',
|
||||
),
|
||||
}}
|
||||
isClosable
|
||||
onClose={dismissClaimError}
|
||||
/>
|
||||
</StyledCalloutContainer>
|
||||
)}
|
||||
<StyledRow>
|
||||
<StyledInputContainer>
|
||||
<SettingsTextInput
|
||||
instanceId="claim-application-lookup"
|
||||
value={lookupValue}
|
||||
onChange={setLookupValue}
|
||||
placeholder={t`e.g. my-twenty-app`}
|
||||
fullWidth
|
||||
label={t`Package name or universal identifier`}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
<Button
|
||||
title={t`Look up`}
|
||||
Icon={IconSearch}
|
||||
onClick={handleLookup}
|
||||
disabled={isLookingUp || lookupValue.trim().length === 0}
|
||||
/>
|
||||
{canSyncCatalog && (
|
||||
<Button
|
||||
title={t`Sync catalog`}
|
||||
variant="secondary"
|
||||
Icon={IconRefresh}
|
||||
onClick={handleSync}
|
||||
disabled={isSyncing}
|
||||
/>
|
||||
)}
|
||||
</StyledRow>
|
||||
|
||||
{notFound && (
|
||||
<StyledResultCard>
|
||||
<StyledHint>
|
||||
{t`No application found. If you just published it to npm with the "twenty-app" keyword, sync the catalog and try again.`}
|
||||
</StyledHint>
|
||||
</StyledResultCard>
|
||||
)}
|
||||
|
||||
{isDefined(registration) && registration.isOwned && (
|
||||
<StyledResultCard>
|
||||
{renderCardTitle(registration)}
|
||||
<StyledHint>{t`This application has already been claimed.`}</StyledHint>
|
||||
</StyledResultCard>
|
||||
)}
|
||||
|
||||
{isDefined(registration) && !registration.isOwned && (
|
||||
<StyledResultCard>
|
||||
{renderCardTitle(registration)}
|
||||
{isDefined(registration.description) && (
|
||||
<StyledHint>{registration.description}</StyledHint>
|
||||
)}
|
||||
<StyledHint>
|
||||
{t`Ownership is verified through npm trusted publishing: the package must be published from GitHub Actions with provenance. Sign in with a GitHub account that owns the publishing account or organization — when the package is published by an organization, grant this app access to it on GitHub's authorization screen.`}
|
||||
</StyledHint>
|
||||
<StyledRow>
|
||||
<Button
|
||||
title={t`Claim with GitHub`}
|
||||
Icon={IconBrandGithub}
|
||||
accent="blue"
|
||||
onClick={handleClaimWithGithub}
|
||||
disabled={isRedirectingToGithub}
|
||||
/>
|
||||
</StyledRow>
|
||||
</StyledResultCard>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
+12
-1
@@ -1,5 +1,5 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CommandBlock } from 'twenty-ui/data-display';
|
||||
import { CommandBlock, Tag } from 'twenty-ui/data-display';
|
||||
import { IconCopy } from 'twenty-ui/icon';
|
||||
import { H2Title } from 'twenty-ui/typography';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
@@ -13,8 +13,10 @@ import { SettingsApplicationRegistrationShareLinkButtons } from '~/pages/setting
|
||||
|
||||
export const SettingsApplicationRegistrationDistributionTab = ({
|
||||
registration,
|
||||
fromAdmin,
|
||||
}: {
|
||||
registration: ApplicationRegistrationData;
|
||||
fromAdmin?: boolean;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
@@ -34,6 +36,15 @@ export const SettingsApplicationRegistrationDistributionTab = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{isNpmSource && fromAdmin !== true && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Ownership`}
|
||||
description={t`This application's registration is claimed by your workspace`}
|
||||
/>
|
||||
<Tag text={t`Claimed by this workspace`} color="green" />
|
||||
</Section>
|
||||
)}
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Public`}
|
||||
|
||||
+6
@@ -5,6 +5,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { InlineBanner } from 'twenty-ui/feedback';
|
||||
import { SettingsApplicationRegistrationGeneralInfo } from '~/pages/settings/applications/components/SettingsApplicationRegistrationGeneralInfo';
|
||||
|
||||
import { SettingsAdminApplicationRegistrationClaims } from '~/pages/settings/admin-panel/SettingsAdminApplicationRegistrationClaims';
|
||||
import { SettingsAdminApplicationRegistrationDangerZone } from '~/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone';
|
||||
import { SettingsApplicationRegistrationGeneralStats } from '~/pages/settings/applications/components/SettingsApplicationRegistrationGeneralStats';
|
||||
import { SettingsAdminApplicationRegistrationGeneralToggles } from '~/pages/settings/admin-panel/SettingsAdminApplicationRegistrationGeneralToggles';
|
||||
@@ -37,6 +38,11 @@ export const SettingsApplicationRegistrationGeneralTab = ({
|
||||
registration={registration}
|
||||
/>
|
||||
)}
|
||||
{fromAdmin && (
|
||||
<SettingsAdminApplicationRegistrationClaims
|
||||
applicationRegistrationId={registration.id}
|
||||
/>
|
||||
)}
|
||||
{fromAdmin && (
|
||||
<SettingsApplicationRegistrationGeneralStats
|
||||
registration={registration}
|
||||
|
||||
+17
@@ -18,13 +18,18 @@ import { Button, SearchInput } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import {
|
||||
type ApplicationRegistrationListItemFragment,
|
||||
FeatureFlagKey,
|
||||
FindManyApplicationRegistrationsDocument,
|
||||
PermissionFlagType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
import {
|
||||
APPLICATION_TABLE_ROW_GRID_TEMPLATE_COLUMNS,
|
||||
SettingsApplicationTableRow,
|
||||
} from '~/pages/settings/applications/components/SettingsApplicationTableRow';
|
||||
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
|
||||
import { SettingsClaimApplicationSection } from '~/pages/settings/applications/components/SettingsClaimApplicationSection';
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
display: flex;
|
||||
@@ -50,6 +55,14 @@ export const SettingsApplicationsDeveloperTab = () => {
|
||||
|
||||
const { data } = useQuery(FindManyApplicationRegistrationsDocument);
|
||||
|
||||
const canClaimApplications = useHasPermissionFlag(
|
||||
PermissionFlagType.APPLICATIONS,
|
||||
);
|
||||
|
||||
const isAppClaimingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_APP_CLAIMING_ENABLED,
|
||||
);
|
||||
|
||||
const [myAppsSearchTerm, setMyAppsSearchTerm] = useState('');
|
||||
|
||||
const registrations: ApplicationRegistrationListItemFragment[] =
|
||||
@@ -109,6 +122,10 @@ export const SettingsApplicationsDeveloperTab = () => {
|
||||
</StyledButtonContainer>
|
||||
</Section>
|
||||
|
||||
{canClaimApplications && isAppClaimingEnabled && (
|
||||
<SettingsClaimApplicationSection />
|
||||
)}
|
||||
|
||||
{registrations.length > 0 && (
|
||||
<Section>
|
||||
<H2Title
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { DOCUMENTATION_PATHS } from 'twenty-shared/constants';
|
||||
|
||||
const PUBLISHING_PATH =
|
||||
DOCUMENTATION_PATHS.DEVELOPERS_EXTEND_APPS_OPERATIONS_PUBLISHING;
|
||||
const CI_PUBLISHING_PATH = `${PUBLISHING_PATH}#ci-publishing`;
|
||||
|
||||
type ClaimErrorContent = {
|
||||
message: MessageDescriptor;
|
||||
docPath: string;
|
||||
};
|
||||
|
||||
export const getClaimErrorContent = (code: string): ClaimErrorContent => {
|
||||
switch (code) {
|
||||
case 'PROVENANCE_NOT_FOUND':
|
||||
return {
|
||||
message: msg`No provenance attestation was found for this package. Publish it from GitHub Actions with npm trusted publishing, then try claiming again.`,
|
||||
docPath: CI_PUBLISHING_PATH,
|
||||
};
|
||||
case 'PROVENANCE_CHECK_UNAVAILABLE':
|
||||
return {
|
||||
message: msg`We could not reach the package registry to verify the package provenance. Please try again in a few minutes.`,
|
||||
docPath: CI_PUBLISHING_PATH,
|
||||
};
|
||||
case 'GITHUB_ORG_OWNERSHIP_REQUIRED':
|
||||
return {
|
||||
message: msg`Your GitHub account does not own the organization that publishes this package. If you are an owner, grant this app access to the organization on GitHub's authorization screen (Organization access section).`,
|
||||
docPath: CI_PUBLISHING_PATH,
|
||||
};
|
||||
case 'GITHUB_AUTH_FAILED':
|
||||
return {
|
||||
message: msg`GitHub authorization failed or was denied. Please connect your GitHub account again.`,
|
||||
docPath: CI_PUBLISHING_PATH,
|
||||
};
|
||||
case 'CLAIM_NOT_CONFIGURED':
|
||||
return {
|
||||
message: msg`Claiming is not configured on this server. Ask an administrator to set up the GitHub OAuth app.`,
|
||||
docPath: PUBLISHING_PATH,
|
||||
};
|
||||
case 'CLAIM_NOT_SUPPORTED':
|
||||
return {
|
||||
message: msg`Only applications published to npm can be claimed this way.`,
|
||||
docPath: PUBLISHING_PATH,
|
||||
};
|
||||
case 'APPLICATION_REGISTRATION_ALREADY_OWNED':
|
||||
return {
|
||||
message: msg`This application has already been claimed by a workspace.`,
|
||||
docPath: PUBLISHING_PATH,
|
||||
};
|
||||
case 'APPLICATION_REGISTRATION_NOT_FOUND':
|
||||
return {
|
||||
message: msg`This application could not be found. It may have been removed from the marketplace catalog.`,
|
||||
docPath: PUBLISHING_PATH,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
message: msg`Could not complete the claim. Please try again.`,
|
||||
docPath: PUBLISHING_PATH,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -10,6 +10,7 @@ const STRUCTURAL_EXEMPTIONS = new Set<string>([
|
||||
'UserWorkspaceEntity',
|
||||
'AppTokenEntity',
|
||||
'ApplicationRegistrationEntity',
|
||||
'ApplicationRegistrationClaimEntity',
|
||||
'ApplicationRegistrationVariableEntity',
|
||||
// nullable workspaceId — both rows support instance-level and per-workspace use
|
||||
'KeyValuePairEntity',
|
||||
|
||||
@@ -40,8 +40,16 @@ const innerAppPublish = async (
|
||||
};
|
||||
}
|
||||
|
||||
// Provenance can only be generated from a CI with OIDC; forcing it locally
|
||||
// makes npm publish fail. ACTIONS_ID_TOKEN_REQUEST_URL is only set when the
|
||||
// GitHub Actions workflow grants id-token: write.
|
||||
const supportsProvenance = process.env.ACTIONS_ID_TOKEN_REQUEST_URL != null;
|
||||
|
||||
const publishArgs = [
|
||||
'publish',
|
||||
'--access',
|
||||
'public',
|
||||
...(supportsProvenance ? ['--provenance'] : []),
|
||||
...(options.npmTag ? ['--tag', options.npmTag] : []),
|
||||
];
|
||||
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { DataSource, QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/slow-instance-command.interface';
|
||||
|
||||
// Catalog-synced apps used to be listed automatically. Marketplace listing is
|
||||
// now curated by server admins, so unlist the auto-synced ones: npm-sourced,
|
||||
// still unclaimed, and not vetted. Owned or vetted rows are left untouched.
|
||||
@RegisteredInstanceCommand('2.23.0', 1784322591746, { type: 'slow' })
|
||||
export class UnlistUnclaimedNpmApplicationRegistrationsSlowInstanceCommand
|
||||
implements SlowInstanceCommand
|
||||
{
|
||||
async runDataMigration(dataSource: DataSource): Promise<void> {
|
||||
await dataSource.query(
|
||||
`UPDATE "core"."applicationRegistration"
|
||||
SET "isListed" = false
|
||||
WHERE "sourceType" = 'npm'
|
||||
AND "workspaceId" IS NULL
|
||||
AND "isVetted" = false`,
|
||||
);
|
||||
}
|
||||
|
||||
public async up(_queryRunner: QueryRunner): Promise<void> {}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
// Approximate rollback: rows that were manually unlisted before this
|
||||
// migration cannot be told apart and may be relisted.
|
||||
await queryRunner.query(
|
||||
`UPDATE "core"."applicationRegistration"
|
||||
SET "isListed" = true
|
||||
WHERE "sourceType" = 'npm'
|
||||
AND "workspaceId" IS NULL
|
||||
AND "isVetted" = false`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
@@ -112,6 +112,7 @@ import { BackfillWorkspaceDatabaseSchemaSlowInstanceCommand } from './2-21/2-21-
|
||||
import { AddLogoFileIdToApplicationRegistrationFastInstanceCommand } from './2-21/2-21-instance-command-fast-1783945979243-add-logo-file-id-to-application-registration';
|
||||
import { AddCalendarEndFieldMetadataIdToViewFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-22/2-22-instance-command-fast-1783956795000-add-calendar-end-field-metadata-id-to-view';
|
||||
import { AddCreatedWorkspaceActivationStatusSlowInstanceCommand } from './2-22/2-22-instance-command-slow-1784106205000-add-created-workspace-activation-status';
|
||||
import { UnlistUnclaimedNpmApplicationRegistrationsSlowInstanceCommand } from './2-23/2-23-instance-command-slow-1784322591746-unlist-unclaimed-npm-application-registrations';
|
||||
import { BackfillCreatedWorkspaceActivationStatusSlowInstanceCommand } from './2-23/2-23-instance-command-slow-1784286705000-backfill-created-workspace-activation-status';
|
||||
import { AddAutoUpgradeToApplicationFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-23/2-23-instance-command-fast-1784297307235-add-auto-upgrade-to-application';
|
||||
|
||||
@@ -228,6 +229,7 @@ export const INSTANCE_COMMANDS = [
|
||||
AddLogoFileIdToApplicationRegistrationFastInstanceCommand,
|
||||
AddCalendarEndFieldMetadataIdToViewFastInstanceCommand,
|
||||
AddCreatedWorkspaceActivationStatusSlowInstanceCommand,
|
||||
UnlistUnclaimedNpmApplicationRegistrationsSlowInstanceCommand,
|
||||
BackfillCreatedWorkspaceActivationStatusSlowInstanceCommand,
|
||||
AddAutoUpgradeToApplicationFastInstanceCommand,
|
||||
];
|
||||
|
||||
@@ -50,8 +50,10 @@ import { AdminPanelVersionService } from 'src/engine/core-modules/admin-panel/se
|
||||
import { ApplicationRegistrationVariableDTO } from 'src/engine/core-modules/application/application-registration-variable/dtos/application-registration-variable.dto';
|
||||
import { ApplicationRegistrationVariableService } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.service';
|
||||
import { UpdateApplicationRegistrationVariableInput } from 'src/engine/core-modules/application/application-registration-variable/dtos/update-application-registration-variable.input';
|
||||
import { ApplicationRegistrationClaimService } from 'src/engine/core-modules/application/application-registration/application-registration-claim.service';
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
|
||||
import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
|
||||
import { AdminApplicationRegistrationClaimDTO } from 'src/engine/core-modules/application/application-registration/dtos/admin-application-registration-claim.dto';
|
||||
import { ApplicationRegistrationInstalledWorkspacesDTO } from 'src/engine/core-modules/application/application-registration/dtos/application-registration-installed-workspaces.dto';
|
||||
import { ApplicationRegistrationStatsDTO } from 'src/engine/core-modules/application/application-registration/dtos/application-registration-stats.dto';
|
||||
import { FindApplicationRegistrationInstalledWorkspacesInput } from 'src/engine/core-modules/application/application-registration/dtos/find-application-registration-installed-workspaces.input';
|
||||
@@ -135,6 +137,7 @@ export class AdminPanelResolver {
|
||||
private readonly adminPanelHealthService: AdminPanelHealthService,
|
||||
private readonly adminPanelSigningKeyService: AdminPanelSigningKeyService,
|
||||
private readonly applicationRegistrationService: ApplicationRegistrationService,
|
||||
private readonly applicationRegistrationClaimService: ApplicationRegistrationClaimService,
|
||||
private readonly applicationRegistrationVariableService: ApplicationRegistrationVariableService,
|
||||
private adminPanelQueueService: AdminPanelQueueService,
|
||||
private featureFlagService: FeatureFlagService,
|
||||
@@ -514,6 +517,16 @@ export class AdminPanelResolver {
|
||||
return true;
|
||||
}
|
||||
|
||||
@UseGuards(AdminPanelGuard)
|
||||
@Query(() => [AdminApplicationRegistrationClaimDTO])
|
||||
async findAdminApplicationRegistrationClaims(
|
||||
@Args('applicationRegistrationId') applicationRegistrationId: string,
|
||||
): Promise<AdminApplicationRegistrationClaimDTO[]> {
|
||||
return this.applicationRegistrationClaimService.findClaimsForRegistration(
|
||||
applicationRegistrationId,
|
||||
);
|
||||
}
|
||||
|
||||
@UseGuards(AdminPanelGuard)
|
||||
@Mutation(() => ApplicationRegistrationEntity)
|
||||
async updateAdminApplicationRegistration(
|
||||
|
||||
+400
@@ -0,0 +1,400 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
import { ApplicationRegistrationClaimService } from 'src/engine/core-modules/application/application-registration/application-registration-claim.service';
|
||||
import { type ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
|
||||
import {
|
||||
ApplicationRegistrationException,
|
||||
ApplicationRegistrationExceptionCode,
|
||||
} from 'src/engine/core-modules/application/application-registration/application-registration.exception';
|
||||
import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
|
||||
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
|
||||
import { type ApplicationRegistrationGithubClaimStateJwtPayload } from 'src/engine/core-modules/auth/types/application-registration-github-claim-state-jwt-payload.type';
|
||||
import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/jwt-token-type.enum';
|
||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
jest.mock('axios');
|
||||
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
||||
|
||||
const WORKSPACE_ID = 'workspace-1';
|
||||
const REGISTRATION_ID = 'registration-1';
|
||||
|
||||
const CONFIG_VALUES: Record<string, string> = {
|
||||
APP_CLAIM_GITHUB_CLIENT_ID: 'github-client-id',
|
||||
APP_CLAIM_GITHUB_CLIENT_SECRET: 'github-client-secret',
|
||||
APP_REGISTRY_URL: 'https://registry.npmjs.org',
|
||||
SERVER_URL: 'https://server.example.com',
|
||||
};
|
||||
|
||||
const buildRegistration = (
|
||||
overrides: Partial<ApplicationRegistrationEntity> = {},
|
||||
): ApplicationRegistrationEntity =>
|
||||
({
|
||||
id: REGISTRATION_ID,
|
||||
universalIdentifier: 'universal-identifier-1',
|
||||
ownerWorkspaceId: null,
|
||||
sourceType: ApplicationRegistrationSourceType.NPM,
|
||||
sourcePackage: '@acme/my-twenty-app',
|
||||
latestAvailableVersion: '1.2.3',
|
||||
...overrides,
|
||||
}) as ApplicationRegistrationEntity;
|
||||
|
||||
const buildAttestationsResponse = (repository: string) => ({
|
||||
attestations: [
|
||||
{
|
||||
predicateType: 'https://slsa.dev/provenance/v1',
|
||||
bundle: {
|
||||
dsseEnvelope: {
|
||||
payload: Buffer.from(
|
||||
JSON.stringify({
|
||||
predicate: {
|
||||
buildDefinition: {
|
||||
externalParameters: { workflow: { repository } },
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toString('base64'),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const STATE_PAYLOAD: ApplicationRegistrationGithubClaimStateJwtPayload = {
|
||||
sub: REGISTRATION_ID,
|
||||
type: JwtTokenTypeEnum.APPLICATION_REGISTRATION_GITHUB_CLAIM_STATE,
|
||||
applicationRegistrationId: REGISTRATION_ID,
|
||||
workspaceId: WORKSPACE_ID,
|
||||
userId: 'user-1',
|
||||
};
|
||||
|
||||
describe('ApplicationRegistrationClaimService', () => {
|
||||
let service: ApplicationRegistrationClaimService;
|
||||
let applicationRegistrationService: jest.Mocked<ApplicationRegistrationService>;
|
||||
let jwtWrapperService: jest.Mocked<JwtWrapperService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ApplicationRegistrationClaimService,
|
||||
{
|
||||
provide: getRepositoryToken(WorkspaceEntity),
|
||||
useValue: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ApplicationRegistrationService,
|
||||
useValue: {
|
||||
findOneByIdGlobal: jest.fn(),
|
||||
claimOwnership: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: JwtWrapperService,
|
||||
useValue: {
|
||||
signAsyncOrThrow: jest.fn().mockResolvedValue('signed-state'),
|
||||
verifyJwtToken: jest.fn(),
|
||||
decode: jest.fn().mockReturnValue(STATE_PAYLOAD),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: TwentyConfigService,
|
||||
useValue: {
|
||||
get: jest.fn((key: string) => CONFIG_VALUES[key]),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get(ApplicationRegistrationClaimService);
|
||||
applicationRegistrationService = module.get(ApplicationRegistrationService);
|
||||
jwtWrapperService = module.get(JwtWrapperService);
|
||||
mockedAxios.isAxiosError.mockReturnValue(false);
|
||||
});
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
const expectException = async (
|
||||
promise: Promise<unknown>,
|
||||
code: ApplicationRegistrationExceptionCode,
|
||||
) => {
|
||||
await expect(promise).rejects.toThrow(ApplicationRegistrationException);
|
||||
await promise.catch((error) => expect(error.code).toBe(code));
|
||||
};
|
||||
|
||||
describe('buildGithubAuthorizationUrl', () => {
|
||||
it('rejects an already-owned registration', async () => {
|
||||
applicationRegistrationService.findOneByIdGlobal.mockResolvedValue(
|
||||
buildRegistration({ ownerWorkspaceId: 'other-workspace' }),
|
||||
);
|
||||
|
||||
await expectException(
|
||||
service.buildGithubAuthorizationUrl({
|
||||
applicationRegistrationId: REGISTRATION_ID,
|
||||
workspaceId: WORKSPACE_ID,
|
||||
userId: null,
|
||||
}),
|
||||
ApplicationRegistrationExceptionCode.APPLICATION_REGISTRATION_ALREADY_OWNED,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a non-npm registration', async () => {
|
||||
applicationRegistrationService.findOneByIdGlobal.mockResolvedValue(
|
||||
buildRegistration({
|
||||
sourceType: ApplicationRegistrationSourceType.LOCAL,
|
||||
sourcePackage: null,
|
||||
}),
|
||||
);
|
||||
|
||||
await expectException(
|
||||
service.buildGithubAuthorizationUrl({
|
||||
applicationRegistrationId: REGISTRATION_ID,
|
||||
workspaceId: WORKSPACE_ID,
|
||||
userId: null,
|
||||
}),
|
||||
ApplicationRegistrationExceptionCode.CLAIM_NOT_SUPPORTED,
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when the GitHub OAuth app is not configured', async () => {
|
||||
applicationRegistrationService.findOneByIdGlobal.mockResolvedValue(
|
||||
buildRegistration(),
|
||||
);
|
||||
const configService = { APP_CLAIM_GITHUB_CLIENT_ID: '' };
|
||||
|
||||
(
|
||||
service as unknown as {
|
||||
twentyConfigService: { get: jest.Mock };
|
||||
}
|
||||
).twentyConfigService.get = jest.fn(
|
||||
(key: string) =>
|
||||
({ ...CONFIG_VALUES, ...configService })[key] as string,
|
||||
);
|
||||
|
||||
await expectException(
|
||||
service.buildGithubAuthorizationUrl({
|
||||
applicationRegistrationId: REGISTRATION_ID,
|
||||
workspaceId: WORKSPACE_ID,
|
||||
userId: null,
|
||||
}),
|
||||
ApplicationRegistrationExceptionCode.CLAIM_NOT_CONFIGURED,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the GitHub authorization url with a signed state', async () => {
|
||||
applicationRegistrationService.findOneByIdGlobal.mockResolvedValue(
|
||||
buildRegistration(),
|
||||
);
|
||||
|
||||
const url = new URL(
|
||||
await service.buildGithubAuthorizationUrl({
|
||||
applicationRegistrationId: REGISTRATION_ID,
|
||||
workspaceId: WORKSPACE_ID,
|
||||
userId: 'user-1',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(url.origin + url.pathname).toBe(
|
||||
'https://github.com/login/oauth/authorize',
|
||||
);
|
||||
expect(url.searchParams.get('client_id')).toBe('github-client-id');
|
||||
expect(url.searchParams.get('scope')).toBe('read:org');
|
||||
expect(url.searchParams.get('state')).toBe('signed-state');
|
||||
expect(url.searchParams.get('prompt')).toBe('select_account');
|
||||
expect(url.searchParams.get('redirect_uri')).toBe(
|
||||
'https://server.example.com/application-registration-claim/github/callback',
|
||||
);
|
||||
expect(jwtWrapperService.signAsyncOrThrow).toHaveBeenCalledWith(
|
||||
STATE_PAYLOAD,
|
||||
expect.objectContaining({ expiresIn: '15m' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('completeGithubClaim', () => {
|
||||
it('throws when no provenance attestation exists', async () => {
|
||||
applicationRegistrationService.findOneByIdGlobal.mockResolvedValue(
|
||||
buildRegistration(),
|
||||
);
|
||||
const notFoundError = Object.assign(new Error('Request failed'), {
|
||||
response: { status: 404 },
|
||||
});
|
||||
|
||||
mockedAxios.get.mockRejectedValueOnce(notFoundError);
|
||||
mockedAxios.isAxiosError.mockReturnValue(true);
|
||||
|
||||
await expectException(
|
||||
service.completeGithubClaim({
|
||||
statePayload: STATE_PAYLOAD,
|
||||
code: 'oauth-code',
|
||||
}),
|
||||
ApplicationRegistrationExceptionCode.PROVENANCE_NOT_FOUND,
|
||||
);
|
||||
});
|
||||
|
||||
it('reports a registry outage without blaming the provenance', async () => {
|
||||
applicationRegistrationService.findOneByIdGlobal.mockResolvedValue(
|
||||
buildRegistration(),
|
||||
);
|
||||
mockedAxios.get.mockRejectedValueOnce(new Error('ETIMEDOUT'));
|
||||
|
||||
await expectException(
|
||||
service.completeGithubClaim({
|
||||
statePayload: STATE_PAYLOAD,
|
||||
code: 'oauth-code',
|
||||
}),
|
||||
ApplicationRegistrationExceptionCode.PROVENANCE_CHECK_UNAVAILABLE,
|
||||
);
|
||||
});
|
||||
|
||||
it('claims when the connected user is the publishing account', async () => {
|
||||
const claimed = buildRegistration({ ownerWorkspaceId: WORKSPACE_ID });
|
||||
|
||||
applicationRegistrationService.findOneByIdGlobal.mockResolvedValue(
|
||||
buildRegistration(),
|
||||
);
|
||||
applicationRegistrationService.claimOwnership.mockResolvedValue(claimed);
|
||||
mockedAxios.get.mockImplementation(async (url: string) => {
|
||||
if (url.includes('/attestations/')) {
|
||||
return {
|
||||
data: buildAttestationsResponse('https://github.com/acme/my-app'),
|
||||
};
|
||||
}
|
||||
if (url.endsWith('/user')) {
|
||||
return { data: { login: 'Acme' } };
|
||||
}
|
||||
throw new Error(`Unexpected GET ${url}`);
|
||||
});
|
||||
mockedAxios.post.mockResolvedValue({
|
||||
data: { access_token: 'github-token' },
|
||||
});
|
||||
|
||||
const result = await service.completeGithubClaim({
|
||||
statePayload: STATE_PAYLOAD,
|
||||
code: 'oauth-code',
|
||||
});
|
||||
|
||||
expect(
|
||||
applicationRegistrationService.claimOwnership,
|
||||
).toHaveBeenCalledWith({
|
||||
applicationRegistrationId: REGISTRATION_ID,
|
||||
claimingWorkspaceId: WORKSPACE_ID,
|
||||
});
|
||||
expect(result).toBe(claimed);
|
||||
});
|
||||
|
||||
it('claims when the connected user owns the publishing organization', async () => {
|
||||
const claimed = buildRegistration({ ownerWorkspaceId: WORKSPACE_ID });
|
||||
|
||||
applicationRegistrationService.findOneByIdGlobal.mockResolvedValue(
|
||||
buildRegistration(),
|
||||
);
|
||||
applicationRegistrationService.claimOwnership.mockResolvedValue(claimed);
|
||||
mockedAxios.get.mockImplementation(async (url: string) => {
|
||||
if (url.includes('/attestations/')) {
|
||||
return {
|
||||
data: buildAttestationsResponse('https://github.com/acme/my-app'),
|
||||
};
|
||||
}
|
||||
if (url.endsWith('/user')) {
|
||||
return { data: { login: 'someone-else' } };
|
||||
}
|
||||
if (url.includes('/user/memberships/orgs/acme')) {
|
||||
return { data: { state: 'active', role: 'admin' } };
|
||||
}
|
||||
throw new Error(`Unexpected GET ${url}`);
|
||||
});
|
||||
mockedAxios.post.mockResolvedValue({
|
||||
data: { access_token: 'github-token' },
|
||||
});
|
||||
|
||||
const result = await service.completeGithubClaim({
|
||||
statePayload: STATE_PAYLOAD,
|
||||
code: 'oauth-code',
|
||||
});
|
||||
|
||||
expect(result).toBe(claimed);
|
||||
});
|
||||
|
||||
it('rejects a member who is not an owner of the publishing organization', async () => {
|
||||
applicationRegistrationService.findOneByIdGlobal.mockResolvedValue(
|
||||
buildRegistration(),
|
||||
);
|
||||
mockedAxios.get.mockImplementation(async (url: string) => {
|
||||
if (url.includes('/attestations/')) {
|
||||
return {
|
||||
data: buildAttestationsResponse('https://github.com/acme/my-app'),
|
||||
};
|
||||
}
|
||||
if (url.endsWith('/user')) {
|
||||
return { data: { login: 'someone-else' } };
|
||||
}
|
||||
if (url.includes('/user/memberships/orgs/acme')) {
|
||||
return { data: { state: 'active', role: 'member' } };
|
||||
}
|
||||
throw new Error(`Unexpected GET ${url}`);
|
||||
});
|
||||
mockedAxios.post.mockResolvedValue({
|
||||
data: { access_token: 'github-token' },
|
||||
});
|
||||
|
||||
await expectException(
|
||||
service.completeGithubClaim({
|
||||
statePayload: STATE_PAYLOAD,
|
||||
code: 'oauth-code',
|
||||
}),
|
||||
ApplicationRegistrationExceptionCode.GITHUB_ORG_OWNERSHIP_REQUIRED,
|
||||
);
|
||||
expect(
|
||||
applicationRegistrationService.claimOwnership,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fails when the GitHub code exchange fails', async () => {
|
||||
applicationRegistrationService.findOneByIdGlobal.mockResolvedValue(
|
||||
buildRegistration(),
|
||||
);
|
||||
mockedAxios.get.mockResolvedValue({
|
||||
data: buildAttestationsResponse('https://github.com/acme/my-app'),
|
||||
});
|
||||
mockedAxios.post.mockRejectedValue(new Error('bad code'));
|
||||
|
||||
await expectException(
|
||||
service.completeGithubClaim({
|
||||
statePayload: STATE_PAYLOAD,
|
||||
code: 'oauth-code',
|
||||
}),
|
||||
ApplicationRegistrationExceptionCode.GITHUB_AUTH_FAILED,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyClaimState', () => {
|
||||
it('rejects a state token of the wrong type', async () => {
|
||||
jwtWrapperService.decode.mockReturnValue({
|
||||
...STATE_PAYLOAD,
|
||||
type: 'SOMETHING_ELSE',
|
||||
} as never);
|
||||
|
||||
await expectException(
|
||||
service.verifyClaimState('state-token'),
|
||||
ApplicationRegistrationExceptionCode.GITHUB_AUTH_FAILED,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the payload of a valid state token', async () => {
|
||||
const payload = await service.verifyClaimState('state-token');
|
||||
|
||||
expect(payload).toEqual(STATE_PAYLOAD);
|
||||
expect(jwtWrapperService.verifyJwtToken).toHaveBeenCalledWith(
|
||||
'state-token',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import { Controller, Get, Query, Res, UseGuards } from '@nestjs/common';
|
||||
|
||||
import { Response } from 'express';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationRegistrationClaimService } from 'src/engine/core-modules/application/application-registration/application-registration-claim.service';
|
||||
import {
|
||||
ApplicationRegistrationException,
|
||||
ApplicationRegistrationExceptionCode,
|
||||
} from 'src/engine/core-modules/application/application-registration/application-registration.exception';
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
|
||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
|
||||
|
||||
// GitHub OAuth callback of the trusted-publishers claim flow. Auth context
|
||||
// travels in the signed state token, not in a session, hence the public
|
||||
// endpoint.
|
||||
@Controller('application-registration-claim')
|
||||
export class ApplicationRegistrationClaimController {
|
||||
constructor(
|
||||
private readonly applicationRegistrationClaimService: ApplicationRegistrationClaimService,
|
||||
private readonly workspaceDomainsService: WorkspaceDomainsService,
|
||||
private readonly guardRedirectService: GuardRedirectService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
) {}
|
||||
|
||||
@Get('github/callback')
|
||||
@UseGuards(PublicEndpointGuard, NoPermissionGuard)
|
||||
async githubCallback(
|
||||
@Query('code') code: string | undefined,
|
||||
@Query('state') state: string | undefined,
|
||||
@Query('error') oauthError: string | undefined,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
let workspace: WorkspaceEntity | null = null;
|
||||
|
||||
try {
|
||||
const statePayload =
|
||||
await this.applicationRegistrationClaimService.verifyClaimState(
|
||||
state ?? '',
|
||||
);
|
||||
|
||||
workspace =
|
||||
await this.applicationRegistrationClaimService.findWorkspaceById(
|
||||
statePayload.workspaceId,
|
||||
);
|
||||
|
||||
if (oauthError !== undefined || code === undefined) {
|
||||
throw new ApplicationRegistrationException(
|
||||
'GitHub authorization was denied',
|
||||
ApplicationRegistrationExceptionCode.GITHUB_AUTH_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
await this.applicationRegistrationClaimService.completeGithubClaim({
|
||||
statePayload,
|
||||
code,
|
||||
});
|
||||
|
||||
if (workspace === null) {
|
||||
throw new Error('Workspace not found');
|
||||
}
|
||||
|
||||
const url = this.workspaceDomainsService.buildWorkspaceURL({
|
||||
workspace,
|
||||
pathname: getSettingsPath(SettingsPath.Applications),
|
||||
});
|
||||
|
||||
url.hash = 'developer';
|
||||
|
||||
return res.redirect(url.toString());
|
||||
} catch (error) {
|
||||
const claimErrorCode =
|
||||
error instanceof CustomException ? error.code : 'CLAIM_FAILED';
|
||||
|
||||
if (workspace !== null) {
|
||||
const url = this.workspaceDomainsService.buildWorkspaceURL({
|
||||
workspace,
|
||||
pathname: getSettingsPath(SettingsPath.Applications),
|
||||
});
|
||||
|
||||
url.searchParams.set('claimErrorCode', claimErrorCode);
|
||||
url.hash = 'developer';
|
||||
|
||||
return res.redirect(url.toString());
|
||||
}
|
||||
|
||||
return res.redirect(
|
||||
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions({
|
||||
error,
|
||||
workspace: {
|
||||
subdomain: this.twentyConfigService.get('DEFAULT_SUBDOMAIN'),
|
||||
customDomain: null,
|
||||
},
|
||||
pathname: getSettingsPath(SettingsPath.Applications),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+450
@@ -0,0 +1,450 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import axios from 'axios';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type Repository } from 'typeorm';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
|
||||
import {
|
||||
ApplicationRegistrationException,
|
||||
ApplicationRegistrationExceptionCode,
|
||||
} from 'src/engine/core-modules/application/application-registration/application-registration.exception';
|
||||
import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
|
||||
import { type AdminApplicationRegistrationClaimDTO } from 'src/engine/core-modules/application/application-registration/dtos/admin-application-registration-claim.dto';
|
||||
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
|
||||
import { type ApplicationRegistrationGithubClaimStateJwtPayload } from 'src/engine/core-modules/auth/types/application-registration-github-claim-state-jwt-payload.type';
|
||||
import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/jwt-token-type.enum';
|
||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
const GITHUB_CLAIM_STATE_EXPIRES_IN = '15m';
|
||||
|
||||
const attestationsResponseSchema = z.object({
|
||||
attestations: z.array(
|
||||
z.object({
|
||||
predicateType: z.string(),
|
||||
bundle: z.object({
|
||||
dsseEnvelope: z.object({
|
||||
payload: z.string(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const provenancePayloadSchema = z.object({
|
||||
predicate: z
|
||||
.object({
|
||||
buildDefinition: z
|
||||
.object({
|
||||
externalParameters: z
|
||||
.object({
|
||||
workflow: z.object({ repository: z.string() }).partial(),
|
||||
})
|
||||
.partial()
|
||||
.optional(),
|
||||
})
|
||||
.partial()
|
||||
.optional(),
|
||||
invocation: z
|
||||
.object({
|
||||
configSource: z.object({ uri: z.string() }).partial().optional(),
|
||||
})
|
||||
.partial()
|
||||
.optional(),
|
||||
})
|
||||
.partial(),
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationRegistrationClaimService {
|
||||
private readonly logger = new Logger(
|
||||
ApplicationRegistrationClaimService.name,
|
||||
);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
private readonly applicationRegistrationService: ApplicationRegistrationService,
|
||||
private readonly jwtWrapperService: JwtWrapperService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
) {}
|
||||
|
||||
async buildGithubAuthorizationUrl(params: {
|
||||
applicationRegistrationId: string;
|
||||
workspaceId: string;
|
||||
userId: string | null;
|
||||
}): Promise<string> {
|
||||
const registration =
|
||||
await this.applicationRegistrationService.findOneByIdGlobal(
|
||||
params.applicationRegistrationId,
|
||||
);
|
||||
|
||||
this.assertClaimable(registration);
|
||||
|
||||
const clientId = this.twentyConfigService.get('APP_CLAIM_GITHUB_CLIENT_ID');
|
||||
|
||||
if (!isNonEmptyString(clientId)) {
|
||||
throw new ApplicationRegistrationException(
|
||||
'GitHub OAuth app is not configured (APP_CLAIM_GITHUB_CLIENT_ID)',
|
||||
ApplicationRegistrationExceptionCode.CLAIM_NOT_CONFIGURED,
|
||||
);
|
||||
}
|
||||
|
||||
const statePayload: ApplicationRegistrationGithubClaimStateJwtPayload = {
|
||||
sub: registration.id,
|
||||
type: JwtTokenTypeEnum.APPLICATION_REGISTRATION_GITHUB_CLAIM_STATE,
|
||||
applicationRegistrationId: registration.id,
|
||||
workspaceId: params.workspaceId,
|
||||
userId: params.userId,
|
||||
};
|
||||
|
||||
const state = await this.jwtWrapperService.signAsyncOrThrow(statePayload, {
|
||||
expiresIn: GITHUB_CLAIM_STATE_EXPIRES_IN,
|
||||
});
|
||||
|
||||
const authorizationUrl = new URL(
|
||||
'https://github.com/login/oauth/authorize',
|
||||
);
|
||||
|
||||
authorizationUrl.searchParams.set('client_id', clientId);
|
||||
authorizationUrl.searchParams.set('scope', 'read:org');
|
||||
authorizationUrl.searchParams.set('redirect_uri', this.buildCallbackUrl());
|
||||
authorizationUrl.searchParams.set('state', state);
|
||||
// Always show the account picker: without it GitHub silently reuses the
|
||||
// previous authorization, leaving no way to retry with another account.
|
||||
authorizationUrl.searchParams.set('prompt', 'select_account');
|
||||
|
||||
return authorizationUrl.toString();
|
||||
}
|
||||
|
||||
async verifyClaimState(
|
||||
state: string,
|
||||
): Promise<ApplicationRegistrationGithubClaimStateJwtPayload> {
|
||||
await this.jwtWrapperService.verifyJwtToken(state);
|
||||
|
||||
const payload =
|
||||
this.jwtWrapperService.decode<ApplicationRegistrationGithubClaimStateJwtPayload>(
|
||||
state,
|
||||
);
|
||||
|
||||
if (
|
||||
payload.type !==
|
||||
JwtTokenTypeEnum.APPLICATION_REGISTRATION_GITHUB_CLAIM_STATE
|
||||
) {
|
||||
throw new ApplicationRegistrationException(
|
||||
'Invalid claim state token',
|
||||
ApplicationRegistrationExceptionCode.GITHUB_AUTH_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
async completeGithubClaim(params: {
|
||||
statePayload: ApplicationRegistrationGithubClaimStateJwtPayload;
|
||||
code: string;
|
||||
}): Promise<ApplicationRegistrationEntity> {
|
||||
const registration =
|
||||
await this.applicationRegistrationService.findOneByIdGlobal(
|
||||
params.statePayload.applicationRegistrationId,
|
||||
);
|
||||
|
||||
const sourcePackage = this.assertClaimable(registration);
|
||||
|
||||
const publisherLogin = await this.fetchProvenancePublisherLogin({
|
||||
packageName: sourcePackage,
|
||||
version: registration.latestAvailableVersion,
|
||||
});
|
||||
|
||||
const accessToken = await this.exchangeGithubCode(params.code);
|
||||
|
||||
await this.assertGithubOwnership({ accessToken, publisherLogin });
|
||||
|
||||
return this.applicationRegistrationService.claimOwnership({
|
||||
applicationRegistrationId: registration.id,
|
||||
claimingWorkspaceId: params.statePayload.workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
async findWorkspaceById(
|
||||
workspaceId: string,
|
||||
): Promise<WorkspaceEntity | null> {
|
||||
return this.workspaceRepository.findOne({ where: { id: workspaceId } });
|
||||
}
|
||||
|
||||
async findClaimsForRegistration(
|
||||
applicationRegistrationId: string,
|
||||
): Promise<AdminApplicationRegistrationClaimDTO[]> {
|
||||
const registration =
|
||||
await this.applicationRegistrationService.findOneByIdGlobal(
|
||||
applicationRegistrationId,
|
||||
);
|
||||
|
||||
if (!isDefined(registration.ownerWorkspaceId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const ownerWorkspace = await this.workspaceRepository.findOne({
|
||||
where: { id: registration.ownerWorkspaceId },
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
workspaceId: registration.ownerWorkspaceId,
|
||||
workspaceDisplayName: ownerWorkspace?.displayName ?? null,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private assertClaimable(registration: ApplicationRegistrationEntity): string {
|
||||
if (isDefined(registration.ownerWorkspaceId)) {
|
||||
throw new ApplicationRegistrationException(
|
||||
'Application registration is already owned by a workspace',
|
||||
ApplicationRegistrationExceptionCode.APPLICATION_REGISTRATION_ALREADY_OWNED,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
registration.sourceType !== ApplicationRegistrationSourceType.NPM ||
|
||||
!isDefined(registration.sourcePackage)
|
||||
) {
|
||||
throw new ApplicationRegistrationException(
|
||||
'Only npm-sourced applications can be claimed',
|
||||
ApplicationRegistrationExceptionCode.CLAIM_NOT_SUPPORTED,
|
||||
);
|
||||
}
|
||||
|
||||
return registration.sourcePackage;
|
||||
}
|
||||
|
||||
private buildCallbackUrl(): string {
|
||||
const serverUrl = this.twentyConfigService.get('SERVER_URL');
|
||||
|
||||
return `${serverUrl}/application-registration-claim/github/callback`;
|
||||
}
|
||||
|
||||
private async fetchProvenancePublisherLogin(params: {
|
||||
packageName: string;
|
||||
version: string | null;
|
||||
}): Promise<string> {
|
||||
const registryUrl = this.twentyConfigService.get('APP_REGISTRY_URL');
|
||||
const version =
|
||||
params.version ??
|
||||
(await this.fetchLatestVersion(registryUrl, params.packageName));
|
||||
|
||||
// Scoped packages need their slash percent-encoded for the registry path.
|
||||
const encodedName = params.packageName.replace(/\//g, '%2F');
|
||||
|
||||
let data: unknown;
|
||||
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${registryUrl}/-/npm/v1/attestations/${encodedName}@${version}`,
|
||||
{
|
||||
headers: { 'User-Agent': 'Twenty-Marketplace' },
|
||||
timeout: 10_000,
|
||||
},
|
||||
);
|
||||
|
||||
data = response.data;
|
||||
} catch (error) {
|
||||
if (
|
||||
axios.isAxiosError(error) &&
|
||||
isDefined(error.response) &&
|
||||
error.response.status === 404
|
||||
) {
|
||||
throw new ApplicationRegistrationException(
|
||||
`No provenance attestation found for ${params.packageName}@${version}`,
|
||||
ApplicationRegistrationExceptionCode.PROVENANCE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.warn(
|
||||
`Failed to fetch attestations for ${params.packageName}@${version}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
|
||||
throw new ApplicationRegistrationException(
|
||||
`Could not reach the package registry to verify provenance for ${params.packageName}`,
|
||||
ApplicationRegistrationExceptionCode.PROVENANCE_CHECK_UNAVAILABLE,
|
||||
);
|
||||
}
|
||||
|
||||
const repositoryUrl = this.extractProvenanceRepositoryUrl(data);
|
||||
|
||||
const match = repositoryUrl?.match(
|
||||
/(?:^|\/\/|@)github\.com[/:]([^/]+)\/[^/@]+/i,
|
||||
);
|
||||
|
||||
if (!isDefined(match)) {
|
||||
throw new ApplicationRegistrationException(
|
||||
`No GitHub source repository found in the provenance of ${params.packageName}@${version}`,
|
||||
ApplicationRegistrationExceptionCode.PROVENANCE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return match[1];
|
||||
}
|
||||
|
||||
private extractProvenanceRepositoryUrl(data: unknown): string | null {
|
||||
const parsed = attestationsResponseSchema.safeParse(data);
|
||||
|
||||
if (!parsed.success) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const attestation of parsed.data.attestations) {
|
||||
if (!attestation.predicateType.includes('slsa.dev/provenance')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = provenancePayloadSchema.parse(
|
||||
JSON.parse(
|
||||
Buffer.from(
|
||||
attestation.bundle.dsseEnvelope.payload,
|
||||
'base64',
|
||||
).toString('utf-8'),
|
||||
),
|
||||
);
|
||||
|
||||
const repository =
|
||||
payload.predicate.buildDefinition?.externalParameters?.workflow
|
||||
?.repository ?? payload.predicate.invocation?.configSource?.uri;
|
||||
|
||||
if (isNonEmptyString(repository)) {
|
||||
return repository;
|
||||
}
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async fetchLatestVersion(
|
||||
registryUrl: string,
|
||||
packageName: string,
|
||||
): Promise<string> {
|
||||
const encodedName = packageName.replace(/\//g, '%2F');
|
||||
|
||||
try {
|
||||
const { data } = await axios.get(`${registryUrl}/${encodedName}/latest`, {
|
||||
headers: { 'User-Agent': 'Twenty-Marketplace' },
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
const version = z.object({ version: z.string() }).parse(data).version;
|
||||
|
||||
return version;
|
||||
} catch {
|
||||
throw new ApplicationRegistrationException(
|
||||
`Could not resolve the latest published version of ${packageName}`,
|
||||
ApplicationRegistrationExceptionCode.PROVENANCE_CHECK_UNAVAILABLE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async exchangeGithubCode(code: string): Promise<string> {
|
||||
const clientId = this.twentyConfigService.get('APP_CLAIM_GITHUB_CLIENT_ID');
|
||||
const clientSecret = this.twentyConfigService.get(
|
||||
'APP_CLAIM_GITHUB_CLIENT_SECRET',
|
||||
);
|
||||
|
||||
if (!isNonEmptyString(clientId) || !isNonEmptyString(clientSecret)) {
|
||||
throw new ApplicationRegistrationException(
|
||||
'GitHub OAuth app is not configured',
|
||||
ApplicationRegistrationExceptionCode.CLAIM_NOT_CONFIGURED,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await axios.post(
|
||||
'https://github.com/login/oauth/access_token',
|
||||
{ client_id: clientId, client_secret: clientSecret, code },
|
||||
{
|
||||
headers: { Accept: 'application/json' },
|
||||
timeout: 10_000,
|
||||
},
|
||||
);
|
||||
|
||||
const accessToken = z
|
||||
.object({ access_token: z.string() })
|
||||
.parse(data).access_token;
|
||||
|
||||
return accessToken;
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`GitHub code exchange failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
|
||||
throw new ApplicationRegistrationException(
|
||||
'GitHub authentication failed',
|
||||
ApplicationRegistrationExceptionCode.GITHUB_AUTH_FAILED,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async assertGithubOwnership(params: {
|
||||
accessToken: string;
|
||||
publisherLogin: string;
|
||||
}): Promise<void> {
|
||||
const headers = {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Accept: 'application/vnd.github+json',
|
||||
'User-Agent': 'Twenty-Marketplace',
|
||||
};
|
||||
|
||||
let viewerLogin: string;
|
||||
|
||||
try {
|
||||
const { data } = await axios.get('https://api.github.com/user', {
|
||||
headers,
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
viewerLogin = z.object({ login: z.string() }).parse(data).login;
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`GitHub user lookup failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
|
||||
throw new ApplicationRegistrationException(
|
||||
'GitHub authentication failed',
|
||||
ApplicationRegistrationExceptionCode.GITHUB_AUTH_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
if (viewerLogin.toLowerCase() === params.publisherLogin.toLowerCase()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await axios.get(
|
||||
`https://api.github.com/user/memberships/orgs/${params.publisherLogin}`,
|
||||
{ headers, timeout: 10_000 },
|
||||
);
|
||||
|
||||
const membership = z
|
||||
.object({ state: z.string(), role: z.string() })
|
||||
.parse(data);
|
||||
|
||||
if (membership.state === 'active' && membership.role === 'admin') {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// 404 means the user is not a member of the organization; fall through
|
||||
// to the ownership error below.
|
||||
}
|
||||
|
||||
throw new ApplicationRegistrationException(
|
||||
`The connected GitHub account is not an owner of ${params.publisherLogin}`,
|
||||
ApplicationRegistrationExceptionCode.GITHUB_ORG_OWNERSHIP_REQUIRED,
|
||||
);
|
||||
}
|
||||
}
|
||||
+21
@@ -16,6 +16,13 @@ export enum ApplicationRegistrationExceptionCode {
|
||||
SERVER_VERSION_INCOMPATIBLE = 'SERVER_VERSION_INCOMPATIBLE',
|
||||
INVALID_APP_ENGINE_REQUIREMENT = 'INVALID_APP_ENGINE_REQUIREMENT',
|
||||
INVALID_SERVER_VERSION = 'INVALID_SERVER_VERSION',
|
||||
APPLICATION_REGISTRATION_ALREADY_OWNED = 'APPLICATION_REGISTRATION_ALREADY_OWNED',
|
||||
CLAIM_NOT_SUPPORTED = 'CLAIM_NOT_SUPPORTED',
|
||||
CLAIM_NOT_CONFIGURED = 'CLAIM_NOT_CONFIGURED',
|
||||
PROVENANCE_NOT_FOUND = 'PROVENANCE_NOT_FOUND',
|
||||
PROVENANCE_CHECK_UNAVAILABLE = 'PROVENANCE_CHECK_UNAVAILABLE',
|
||||
GITHUB_AUTH_FAILED = 'GITHUB_AUTH_FAILED',
|
||||
GITHUB_ORG_OWNERSHIP_REQUIRED = 'GITHUB_ORG_OWNERSHIP_REQUIRED',
|
||||
}
|
||||
|
||||
const getExceptionUserFriendlyMessage = (
|
||||
@@ -44,6 +51,20 @@ const getExceptionUserFriendlyMessage = (
|
||||
return msg`The app manifest declares an invalid server version requirement.`;
|
||||
case ApplicationRegistrationExceptionCode.INVALID_SERVER_VERSION:
|
||||
return msg`The server's APP_VERSION is not a valid semver version. Self-hosted instances must configure a valid APP_VERSION.`;
|
||||
case ApplicationRegistrationExceptionCode.APPLICATION_REGISTRATION_ALREADY_OWNED:
|
||||
return msg`This application is already owned by a workspace.`;
|
||||
case ApplicationRegistrationExceptionCode.CLAIM_NOT_SUPPORTED:
|
||||
return msg`Only applications published to npm can be claimed this way.`;
|
||||
case ApplicationRegistrationExceptionCode.CLAIM_NOT_CONFIGURED:
|
||||
return msg`Claiming is not configured on this server. Ask an administrator to configure the GitHub OAuth app.`;
|
||||
case ApplicationRegistrationExceptionCode.PROVENANCE_NOT_FOUND:
|
||||
return msg`No provenance attestation was found for the published package. Publish it with npm trusted publishing from GitHub Actions, then try again.`;
|
||||
case ApplicationRegistrationExceptionCode.PROVENANCE_CHECK_UNAVAILABLE:
|
||||
return msg`The package registry could not be reached to verify the package provenance. Try again later.`;
|
||||
case ApplicationRegistrationExceptionCode.GITHUB_AUTH_FAILED:
|
||||
return msg`GitHub authentication failed. Try connecting your GitHub account again.`;
|
||||
case ApplicationRegistrationExceptionCode.GITHUB_ORG_OWNERSHIP_REQUIRED:
|
||||
return msg`Your GitHub account does not own the organization that publishes this package. If you are an owner, make sure you granted this app access to the organization on GitHub's authorization screen (Organization access section).`;
|
||||
default:
|
||||
assertUnreachable(code);
|
||||
}
|
||||
|
||||
+11
@@ -4,6 +4,8 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CoreEntityCacheModule } from 'src/engine/core-entity-cache/core-entity-cache.module';
|
||||
import { ApplicationRegistrationAssetUrlService } from 'src/engine/core-modules/application/application-registration/application-registration-asset-url.service';
|
||||
import { ApplicationRegistrationAssetService } from 'src/engine/core-modules/application/application-registration/application-registration-asset.service';
|
||||
import { ApplicationRegistrationClaimController } from 'src/engine/core-modules/application/application-registration/application-registration-claim.controller';
|
||||
import { ApplicationRegistrationClaimService } from 'src/engine/core-modules/application/application-registration/application-registration-claim.service';
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
|
||||
import { ApplicationRegistrationResolver } from 'src/engine/core-modules/application/application-registration/application-registration.resolver';
|
||||
import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
|
||||
@@ -15,9 +17,12 @@ import { ApplicationEntity } from 'src/engine/core-modules/application/applicati
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { CacheLockModule } from 'src/engine/core-modules/cache-lock/cache-lock.module';
|
||||
import { DomainServerConfigModule } from 'src/engine/core-modules/domain/domain-server-config/domain-server-config.module';
|
||||
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
|
||||
import { FileUrlModule } from 'src/engine/core-modules/file/file-url/file-url.module';
|
||||
import { GuardRedirectModule } from 'src/engine/core-modules/guard-redirect/guard-redirect.module';
|
||||
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
|
||||
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
@@ -36,15 +41,20 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
|
||||
CacheLockModule,
|
||||
CoreEntityCacheModule,
|
||||
DomainServerConfigModule,
|
||||
WorkspaceDomainsModule,
|
||||
FeatureFlagModule,
|
||||
GuardRedirectModule,
|
||||
JwtModule,
|
||||
PermissionsModule,
|
||||
FileStorageModule,
|
||||
FileUrlModule,
|
||||
MetricsModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
],
|
||||
controllers: [ApplicationRegistrationClaimController],
|
||||
providers: [
|
||||
ApplicationRegistrationService,
|
||||
ApplicationRegistrationClaimService,
|
||||
ApplicationRegistrationResolver,
|
||||
ApplicationRegistrationSummaryResolver,
|
||||
ApplicationTarballService,
|
||||
@@ -53,6 +63,7 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
|
||||
],
|
||||
exports: [
|
||||
ApplicationRegistrationService,
|
||||
ApplicationRegistrationClaimService,
|
||||
ApplicationRegistrationVariableModule,
|
||||
ApplicationRegistrationAssetService,
|
||||
ApplicationRegistrationAssetUrlService,
|
||||
|
||||
+44
@@ -27,8 +27,12 @@ import { ApplicationRegistrationAssetUrlService } from 'src/engine/core-modules/
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
|
||||
import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
|
||||
import { ApplicationTarballService } from 'src/engine/core-modules/application/application-registration/application-tarball.service';
|
||||
import { ApplicationRegistrationClaimService } from 'src/engine/core-modules/application/application-registration/application-registration-claim.service';
|
||||
import { ApplicationRegistrationClaimInput } from 'src/engine/core-modules/application/application-registration/dtos/application-registration-claim.input';
|
||||
import { ApplicationRegistrationStatsDTO } from 'src/engine/core-modules/application/application-registration/dtos/application-registration-stats.dto';
|
||||
import { ClaimApplicationRegistrationOwnershipInput } from 'src/engine/core-modules/application/application-registration/dtos/claim-application-registration-ownership.input';
|
||||
import { ClaimableApplicationRegistrationDTO } from 'src/engine/core-modules/application/application-registration/dtos/claimable-application-registration.dto';
|
||||
import { FindClaimableApplicationRegistrationInput } from 'src/engine/core-modules/application/application-registration/dtos/find-claimable-application-registration.input';
|
||||
import { CreateApplicationRegistrationDTO } from 'src/engine/core-modules/application/application-registration/dtos/create-application-registration.dto';
|
||||
import { CreateApplicationRegistrationInput } from 'src/engine/core-modules/application/application-registration/dtos/create-application-registration.input';
|
||||
import { PublicApplicationRegistrationDTO } from 'src/engine/core-modules/application/application-registration/dtos/public-application-registration.dto';
|
||||
@@ -44,6 +48,7 @@ import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { AdminPanelGuard } from 'src/engine/guards/admin-panel-guard';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
@@ -66,6 +71,7 @@ import {
|
||||
export class ApplicationRegistrationResolver {
|
||||
constructor(
|
||||
private readonly applicationRegistrationService: ApplicationRegistrationService,
|
||||
private readonly applicationRegistrationClaimService: ApplicationRegistrationClaimService,
|
||||
private readonly applicationRegistrationVariableService: ApplicationRegistrationVariableService,
|
||||
private readonly applicationTarballService: ApplicationTarballService,
|
||||
private readonly applicationRegistrationAssetUrlService: ApplicationRegistrationAssetUrlService,
|
||||
@@ -317,6 +323,25 @@ export class ApplicationRegistrationResolver {
|
||||
WorkspaceAuthGuard,
|
||||
SettingsPermissionGuard(PermissionFlagType.APPLICATIONS),
|
||||
)
|
||||
@Query(() => ClaimableApplicationRegistrationDTO, { nullable: true })
|
||||
async findClaimableApplicationRegistration(
|
||||
@Args()
|
||||
{
|
||||
sourcePackage,
|
||||
universalIdentifier,
|
||||
}: FindClaimableApplicationRegistrationInput,
|
||||
): Promise<ClaimableApplicationRegistrationDTO | null> {
|
||||
return this.applicationRegistrationService.findClaimable({
|
||||
sourcePackage,
|
||||
universalIdentifier,
|
||||
});
|
||||
}
|
||||
|
||||
@UseGuards(
|
||||
WorkspaceAuthGuard,
|
||||
AdminPanelGuard,
|
||||
SettingsPermissionGuard(PermissionFlagType.APPLICATIONS),
|
||||
)
|
||||
@Mutation(() => ApplicationRegistrationEntity)
|
||||
async claimApplicationRegistrationOwnership(
|
||||
@Args()
|
||||
@@ -329,6 +354,25 @@ export class ApplicationRegistrationResolver {
|
||||
});
|
||||
}
|
||||
|
||||
@UseGuards(
|
||||
WorkspaceAuthGuard,
|
||||
SettingsPermissionGuard(PermissionFlagType.APPLICATIONS),
|
||||
)
|
||||
@Query(() => String)
|
||||
async githubClaimAuthorizationUrl(
|
||||
@Args() { applicationRegistrationId }: ApplicationRegistrationClaimInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
@AuthUser({ allowUndefined: true }) user: UserEntity | undefined,
|
||||
): Promise<string> {
|
||||
return this.applicationRegistrationClaimService.buildGithubAuthorizationUrl(
|
||||
{
|
||||
applicationRegistrationId,
|
||||
workspaceId,
|
||||
userId: user?.id ?? null,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@UseGuards(
|
||||
WorkspaceAuthGuard,
|
||||
SettingsPermissionGuard(PermissionFlagType.APPLICATIONS),
|
||||
|
||||
+47
-2
@@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
} from 'src/engine/core-modules/application/application-registration/application-registration.exception';
|
||||
import { type ApplicationRegistrationInstalledWorkspacesDTO } from 'src/engine/core-modules/application/application-registration/dtos/application-registration-installed-workspaces.dto';
|
||||
import { type ApplicationRegistrationStatsDTO } from 'src/engine/core-modules/application/application-registration/dtos/application-registration-stats.dto';
|
||||
import { type ClaimableApplicationRegistrationDTO } from 'src/engine/core-modules/application/application-registration/dtos/claimable-application-registration.dto';
|
||||
import { type CreateApplicationRegistrationInput } from 'src/engine/core-modules/application/application-registration/dtos/create-application-registration.input';
|
||||
import { type PaginatedApplicationRegistrationsDTO } from 'src/engine/core-modules/application/application-registration/dtos/paginated-application-registrations.dto';
|
||||
import { type PublicApplicationRegistrationDTO } from 'src/engine/core-modules/application/application-registration/dtos/public-application-registration.dto';
|
||||
@@ -914,6 +916,49 @@ export class ApplicationRegistrationService {
|
||||
};
|
||||
}
|
||||
|
||||
async findClaimable(params: {
|
||||
sourcePackage?: string;
|
||||
universalIdentifier?: string;
|
||||
}): Promise<ClaimableApplicationRegistrationDTO | null> {
|
||||
const hasPackage = isNonEmptyString(params.sourcePackage);
|
||||
const hasUniversalIdentifier = isNonEmptyString(params.universalIdentifier);
|
||||
|
||||
if (hasPackage === hasUniversalIdentifier) {
|
||||
throw new ApplicationRegistrationException(
|
||||
'Provide exactly one of sourcePackage or universalIdentifier',
|
||||
ApplicationRegistrationExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const where: FindOptionsWhere<ApplicationRegistrationEntity> = {
|
||||
sourceType: ApplicationRegistrationSourceType.NPM,
|
||||
...(hasPackage
|
||||
? { sourcePackage: params.sourcePackage }
|
||||
: { universalIdentifier: params.universalIdentifier }),
|
||||
};
|
||||
|
||||
const registration = await this.applicationRegistrationRepository.findOne({
|
||||
select: APPLICATION_REGISTRATION_WITHOUT_MANIFEST_SELECT,
|
||||
where,
|
||||
});
|
||||
|
||||
if (!isDefined(registration)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: registration.id,
|
||||
universalIdentifier: registration.universalIdentifier,
|
||||
name: registration.name,
|
||||
sourcePackage: registration.sourcePackage,
|
||||
logoUrl:
|
||||
this.applicationRegistrationAssetUrlService.buildLogoUrl(registration),
|
||||
description: registration.description,
|
||||
author: registration.author,
|
||||
isOwned: isDefined(registration.ownerWorkspaceId),
|
||||
};
|
||||
}
|
||||
|
||||
async claimOwnership(params: {
|
||||
applicationRegistrationId: string;
|
||||
claimingWorkspaceId: string;
|
||||
@@ -926,7 +971,7 @@ export class ApplicationRegistrationService {
|
||||
if (isDefined(registration.ownerWorkspaceId)) {
|
||||
throw new ApplicationRegistrationException(
|
||||
'Application registration is already owned by a workspace',
|
||||
ApplicationRegistrationExceptionCode.INVALID_INPUT,
|
||||
ApplicationRegistrationExceptionCode.APPLICATION_REGISTRATION_ALREADY_OWNED,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -940,7 +985,7 @@ export class ApplicationRegistrationService {
|
||||
if (updateResult.affected === 0) {
|
||||
throw new ApplicationRegistrationException(
|
||||
'Application registration is already owned by a workspace',
|
||||
ApplicationRegistrationExceptionCode.INVALID_INPUT,
|
||||
ApplicationRegistrationExceptionCode.APPLICATION_REGISTRATION_ALREADY_OWNED,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType('AdminApplicationRegistrationClaim')
|
||||
export class AdminApplicationRegistrationClaimDTO {
|
||||
@Field(() => String)
|
||||
workspaceId: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
workspaceDisplayName: string | null;
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { ArgsType, Field } from '@nestjs/graphql';
|
||||
|
||||
import { IsUUID } from 'class-validator';
|
||||
|
||||
@ArgsType()
|
||||
export class ApplicationRegistrationClaimInput {
|
||||
@Field(() => String)
|
||||
@IsUUID()
|
||||
applicationRegistrationId: string;
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType('ClaimableApplicationRegistration')
|
||||
export class ClaimableApplicationRegistrationDTO {
|
||||
@Field(() => String)
|
||||
id: string;
|
||||
|
||||
@Field(() => String)
|
||||
universalIdentifier: string;
|
||||
|
||||
@Field(() => String)
|
||||
name: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
sourcePackage: string | null;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
logoUrl: string | null;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
description: string | null;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
author: string | null;
|
||||
|
||||
@Field(() => Boolean)
|
||||
isOwned: boolean;
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { ArgsType, Field } from '@nestjs/graphql';
|
||||
|
||||
import { IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
|
||||
@ArgsType()
|
||||
export class FindClaimableApplicationRegistrationInput {
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
sourcePackage?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
universalIdentifier?: string;
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { type CommonPropertiesJwtPayload } from 'src/engine/core-modules/auth/types/common-properties-jwt-payload.type';
|
||||
import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/jwt-token-type.enum';
|
||||
|
||||
export type ApplicationRegistrationGithubClaimStateJwtPayload =
|
||||
CommonPropertiesJwtPayload & {
|
||||
type: JwtTokenTypeEnum.APPLICATION_REGISTRATION_GITHUB_CLAIM_STATE;
|
||||
applicationRegistrationId: string;
|
||||
workspaceId: string;
|
||||
userId: string | null;
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import { type ApiKeyTokenJwtPayload } from 'src/engine/core-modules/auth/types/a
|
||||
import { type ApplicationAccessTokenJwtPayload } from 'src/engine/core-modules/auth/types/application-access-token-jwt-payload.type';
|
||||
import { type ApplicationRefreshTokenJwtPayload } from 'src/engine/core-modules/auth/types/application-refresh-token-jwt-payload.type';
|
||||
import { type AppOAuthStateJwtPayload } from 'src/engine/core-modules/auth/types/app-oauth-state-jwt-payload.type';
|
||||
import { type ApplicationRegistrationGithubClaimStateJwtPayload } from 'src/engine/core-modules/auth/types/application-registration-github-claim-state-jwt-payload.type';
|
||||
import { type ApprovedAccessDomainJwtPayload } from 'src/engine/core-modules/auth/types/approved-access-domain-jwt-payload.type';
|
||||
import { type FileTokenJwtPayload } from 'src/engine/core-modules/auth/types/file-token-jwt-payload.type';
|
||||
import { type FileUploadTokenJwtPayload } from 'src/engine/core-modules/auth/types/file-upload-token-jwt-payload.type';
|
||||
@@ -26,5 +27,6 @@ export type JwtPayload =
|
||||
| FileTokenJwtPayloadLegacy
|
||||
| FileUploadTokenJwtPayload
|
||||
| AppOAuthStateJwtPayload
|
||||
| ApplicationRegistrationGithubClaimStateJwtPayload
|
||||
| ApprovedAccessDomainJwtPayload
|
||||
| PlaygroundTokenJwtPayload;
|
||||
|
||||
@@ -11,6 +11,7 @@ export enum JwtTokenTypeEnum {
|
||||
APPLICATION_ACCESS = 'APPLICATION_ACCESS',
|
||||
APPLICATION_REFRESH = 'APPLICATION_REFRESH',
|
||||
APP_OAUTH_STATE = 'APP_OAUTH_STATE',
|
||||
APPLICATION_REGISTRATION_GITHUB_CLAIM_STATE = 'APPLICATION_REGISTRATION_GITHUB_CLAIM_STATE',
|
||||
APPROVED_ACCESS_DOMAIN = 'APPROVED_ACCESS_DOMAIN',
|
||||
PLAYGROUND = 'PLAYGROUND',
|
||||
}
|
||||
|
||||
@@ -2066,6 +2066,27 @@ export class ConfigVariables {
|
||||
@IsOptional()
|
||||
APP_REGISTRY_CDN_URL: string = 'https://unpkg.com';
|
||||
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.ADVANCED_SETTINGS,
|
||||
description:
|
||||
'Client ID of the GitHub OAuth app used to verify app ownership when claiming a marketplace application',
|
||||
type: ConfigVariableType.STRING,
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
APP_CLAIM_GITHUB_CLIENT_ID: string;
|
||||
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.ADVANCED_SETTINGS,
|
||||
isSensitive: true,
|
||||
description:
|
||||
'Client secret of the GitHub OAuth app used to verify app ownership when claiming a marketplace application',
|
||||
type: ConfigVariableType.STRING,
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
APP_CLAIM_GITHUB_CLIENT_SECRET: string;
|
||||
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.ADVANCED_SETTINGS,
|
||||
isSensitive: true,
|
||||
|
||||
+1
@@ -236,6 +236,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
'test-entity': 'test-entity-id',
|
||||
},
|
||||
featureFlagsMap: {
|
||||
IS_APP_CLAIMING_ENABLED: false,
|
||||
IS_UNIQUE_INDEXES_ENABLED: false,
|
||||
IS_JSON_FILTER_ENABLED: false,
|
||||
IS_CALENDAR_WEEK_VIEW_ENABLED: false,
|
||||
|
||||
+5
@@ -20,6 +20,11 @@ export const seedFeatureFlags = async ({
|
||||
.into(`${schemaName}.${tableName}`, ['key', 'workspaceId', 'value'])
|
||||
.orIgnore()
|
||||
.values([
|
||||
{
|
||||
key: FeatureFlagKey.IS_APP_CLAIMING_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_UNIQUE_INDEXES_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum FeatureFlagKey {
|
||||
IS_APP_CLAIMING_ENABLED = 'IS_APP_CLAIMING_ENABLED',
|
||||
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
|
||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||
IS_CALENDAR_WEEK_VIEW_ENABLED = 'IS_CALENDAR_WEEK_VIEW_ENABLED',
|
||||
|
||||
@@ -39,3 +39,7 @@ ENTERPRISE_JWT_PUBLIC_KEY=
|
||||
# Shared secret guarding the internal enterprise key reissue endpoint
|
||||
# (POST /api/enterprise/reissue), used to regenerate an enterprise key.
|
||||
ENTERPRISE_ADMIN_API_SECRET=
|
||||
|
||||
# Optional: GitHub Claim
|
||||
# APP_CLAIM_GITHUB_CLIENT_ID=
|
||||
# APP_CLAIM_GITHUB_CLIENT_SECRET=
|
||||
|
||||
Reference in New Issue
Block a user