From 81760fd45c7edb6d9736ebddcf3489932b0e6730 Mon Sep 17 00:00:00 2001 From: DarkSky <25152247+darkskygit@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:20:15 +0800 Subject: [PATCH] chore: cleanup legacy logic (#15072) --- .docker/selfhost/schema.json | 26 - Cargo.lock | 373 +++++++++-- Cargo.toml | 1 + packages/backend/native/Cargo.toml | 8 +- packages/backend/native/index.d.ts | 71 ++- packages/backend/native/src/lib.rs | 1 + packages/backend/native/src/license.rs | 501 +++++++++++++++ packages/backend/native/src/safe_fetch.rs | 577 +++-------------- .../server/src/__tests__/auth/auth.e2e.ts | 7 +- .../server/src/__tests__/copilot/byok.spec.ts | 12 +- .../src/__tests__/copilot/copilot.spec.ts | 1 + .../src/__tests__/models/feature-user.spec.ts | 4 +- .../src/__tests__/oauth/controller.spec.ts | 2 +- .../payment/__snapshots__/service.spec.ts.md | 121 ---- .../__snapshots__/service.spec.ts.snap | Bin 527 -> 0 bytes .../src/__tests__/payment/event.spec.ts | 42 +- .../src/__tests__/payment/service.spec.ts | 586 ++---------------- .../server/src/__tests__/utils/invite.ts | 3 +- .../src/__tests__/utils/notification.ts | 13 - .../server/src/__tests__/utils/user.ts | 3 +- packages/backend/server/src/app.module.ts | 2 - .../backend/server/src/base/utils/ssrf.ts | 37 +- .../backend/server/src/core/auth/resolver.ts | 4 +- .../server/src/core/comment/resolver.ts | 9 +- .../backend/server/src/core/doc/reader.ts | 13 +- .../server/src/core/entitlement/service.ts | 67 +- .../backend/server/src/core/features/index.ts | 4 +- .../server/src/core/features/service.ts | 16 - .../backend/server/src/core/features/types.ts | 8 +- .../server/src/core/notification/resolver.ts | 19 +- .../src/core/quota/__tests__/state.spec.ts | 1 - .../backend/server/src/core/quota/service.ts | 1 - .../backend/server/src/core/quota/types.ts | 8 - .../server/src/core/workspaces/types.ts | 10 - .../server/src/models/common/feature.ts | 13 - packages/backend/server/src/native.ts | 25 + .../src/plugins/calendar/providers/caldav.ts | 141 +++-- .../src/plugins/copilot/byok/service.ts | 38 +- .../src/plugins/copilot/context/realtime.ts | 26 +- .../src/plugins/copilot/context/service.ts | 2 + .../src/plugins/copilot/embedding/job.ts | 26 +- .../src/plugins/copilot/embedding/types.ts | 6 +- .../server/src/plugins/customerio/config.ts | 22 - .../server/src/plugins/customerio/index.ts | 10 - .../server/src/plugins/customerio/service.ts | 72 --- .../providers/manticoresearch.spec.ts | 35 ++ .../indexer/providers/elasticsearch.ts | 19 +- .../indexer/providers/manticoresearch.ts | 58 +- .../server/src/plugins/license/service.ts | 180 +++--- .../server/src/plugins/oauth/providers/def.ts | 15 +- .../src/plugins/oauth/providers/oidc.ts | 28 +- .../server/src/plugins/payment/config.ts | 18 - .../server/src/plugins/payment/controller.ts | 4 +- .../src/plugins/payment/manager/common.ts | 16 +- .../src/plugins/payment/manager/user.ts | 307 +-------- .../server/src/plugins/payment/resolver.ts | 15 +- .../server/src/plugins/payment/service.ts | 14 +- .../server/src/plugins/payment/stripe.ts | 12 +- .../server/src/plugins/payment/types.ts | 32 +- .../server/src/plugins/worker/controller.ts | 2 + packages/backend/server/src/schema.gql | 13 +- packages/common/graphql/src/graphql/index.ts | 6 - .../common/graphql/src/graphql/invoices.gql | 1 - .../graphql/workspace-intive-by-emails.gql | 1 - .../src/graphql/workspace-invoices.gql | 1 - packages/common/graphql/src/schema.ts | 21 - packages/frontend/admin/src/config.json | 20 - .../admin/src/modules/settings/config.ts | 8 +- ...AdminUpdateWorkspaceMutation.graphql.swift | 2 +- ...WorkspaceByokConfigsMutation.graphql.swift | 35 ++ ...kspaceByokLocalLeaseMutation.graphql.swift | 56 ++ ...eWorkspaceByokConfigMutation.graphql.swift | 46 ++ .../InviteByEmailsMutation.graphql.swift | 6 +- .../PreviewLicenseMutation.graphql.swift | 74 +++ ...WorkspaceByokConfigsMutation.graphql.swift | 56 ++ .../RetryTranscriptTaskMutation.graphql.swift | 67 ++ .../SendTestEmailMutation.graphql.swift | 7 +- ...SettleTranscriptTaskMutation.graphql.swift | 274 ++++++++ ...SubmitTranscriptTaskMutation.graphql.swift | 82 +++ ...tWorkspaceByokConfigMutation.graphql.swift | 58 ++ ...tWorkspaceByokConfigMutation.graphql.swift | 54 ++ .../GetTranscriptTaskQuery.graphql.swift | 316 ++++++++++ .../Queries/InvoicesQuery.graphql.swift | 5 +- .../WorkspaceByokSettingsQuery.graphql.swift | 212 +++++++ .../WorkspaceInvoicesQuery.graphql.swift | 5 +- .../Schema/Enums/ByokKeyStorage.graphql.swift | 9 + .../Enums/ByokKeyTestStatus.graphql.swift | 10 + .../Schema/Enums/ByokProvider.graphql.swift | 11 + .../Schema/Enums/FeatureType.graphql.swift | 2 - .../Enums/SubscriptionVariant.graphql.swift | 1 - .../CreateChatMessageInput.graphql.swift | 18 + .../CreateCheckoutSessionInput.graphql.swift | 20 + ...WorkspaceByokLocalLeaseInput.graphql.swift | 32 + ...eByokLocalLeaseProviderInput.graphql.swift | 67 ++ ...derWorkspaceByokConfigsInput.graphql.swift | 39 ++ ...ubmitAudioTranscriptionInput.graphql.swift | 11 +- ...TestWorkspaceByokConfigInput.graphql.swift | 60 ++ ...sertWorkspaceByokConfigInput.graphql.swift | 88 +++ ...wift => AdminLicensePreview.graphql.swift} | 4 +- ...paceByokLocalLeaseResultType.graphql.swift | 12 + ...rkspaceByokConfigResultType.graphql.swift} | 4 +- ...aceByokCapabilityWarningType.graphql.swift | 12 + ... WorkspaceByokKeyConfigType.graphql.swift} | 4 +- ...> WorkspaceByokSettingsType.graphql.swift} | 4 +- ...WorkspaceByokUsagePointType.graphql.swift} | 4 +- .../Schema/SchemaMetadata.graphql.swift | 17 +- .../ai/runtime/request/action-definitions.ts | 10 +- .../general-setting/billing/ai-plan-card.tsx | 10 +- .../billing/billing-history.tsx | 8 +- .../billing/payment-method.tsx | 3 +- .../general-setting/billing/pro-plan-card.tsx | 8 - .../plans/ai/actions/redeem.tsx | 52 -- .../general-setting/plans/ai/ai-plan.tsx | 6 +- .../general-setting/plans/cloud-plans.tsx | 5 +- .../general-setting/plans/plan-card.tsx | 44 +- .../billing/billing-history.tsx | 8 +- .../members/cloud-members-panel.tsx | 34 +- .../src/desktop/pages/subscribe/index.tsx | 24 +- .../modules/cloud/entities/subscription.ts | 7 - .../modules/cloud/entities/user-feature.ts | 6 - packages/frontend/i18n/src/i18n.gen.ts | 9 +- packages/frontend/i18n/src/resources/ar.json | 1 - packages/frontend/i18n/src/resources/ca.json | 1 - packages/frontend/i18n/src/resources/de.json | 1 - .../frontend/i18n/src/resources/el-GR.json | 1 - packages/frontend/i18n/src/resources/en.json | 2 +- packages/frontend/i18n/src/resources/es.json | 1 - packages/frontend/i18n/src/resources/fa.json | 1 - packages/frontend/i18n/src/resources/fr.json | 1 - packages/frontend/i18n/src/resources/it.json | 1 - packages/frontend/i18n/src/resources/ja.json | 1 - packages/frontend/i18n/src/resources/kk.json | 1 - packages/frontend/i18n/src/resources/ko.json | 1 - packages/frontend/i18n/src/resources/pl.json | 1 - .../frontend/i18n/src/resources/pt-BR.json | 1 - packages/frontend/i18n/src/resources/ru.json | 1 - .../frontend/i18n/src/resources/sv-SE.json | 1 - packages/frontend/i18n/src/resources/tr.json | 1 - packages/frontend/i18n/src/resources/uk.json | 1 - packages/frontend/i18n/src/resources/ur.json | 1 - .../frontend/i18n/src/resources/zh-Hans.json | 2 +- .../frontend/i18n/src/resources/zh-Hant.json | 2 +- 142 files changed, 3351 insertions(+), 2449 deletions(-) create mode 100644 packages/backend/native/src/license.rs delete mode 100644 packages/backend/server/src/__tests__/payment/__snapshots__/service.spec.ts.md delete mode 100644 packages/backend/server/src/__tests__/payment/__snapshots__/service.spec.ts.snap delete mode 100644 packages/backend/server/src/plugins/customerio/config.ts delete mode 100644 packages/backend/server/src/plugins/customerio/index.ts delete mode 100644 packages/backend/server/src/plugins/customerio/service.ts create mode 100644 packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/ClearWorkspaceByokConfigsMutation.graphql.swift create mode 100644 packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/CreateWorkspaceByokLocalLeaseMutation.graphql.swift create mode 100644 packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/DeleteWorkspaceByokConfigMutation.graphql.swift create mode 100644 packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/PreviewLicenseMutation.graphql.swift create mode 100644 packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/ReorderWorkspaceByokConfigsMutation.graphql.swift create mode 100644 packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/RetryTranscriptTaskMutation.graphql.swift create mode 100644 packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/SettleTranscriptTaskMutation.graphql.swift create mode 100644 packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/SubmitTranscriptTaskMutation.graphql.swift create mode 100644 packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/TestWorkspaceByokConfigMutation.graphql.swift create mode 100644 packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/UpsertWorkspaceByokConfigMutation.graphql.swift create mode 100644 packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetTranscriptTaskQuery.graphql.swift create mode 100644 packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/WorkspaceByokSettingsQuery.graphql.swift create mode 100644 packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Enums/ByokKeyStorage.graphql.swift create mode 100644 packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Enums/ByokKeyTestStatus.graphql.swift create mode 100644 packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Enums/ByokProvider.graphql.swift create mode 100644 packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/CreateWorkspaceByokLocalLeaseInput.graphql.swift create mode 100644 packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/CreateWorkspaceByokLocalLeaseProviderInput.graphql.swift create mode 100644 packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/ReorderWorkspaceByokConfigsInput.graphql.swift create mode 100644 packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/TestWorkspaceByokConfigInput.graphql.swift create mode 100644 packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/UpsertWorkspaceByokConfigInput.graphql.swift rename packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/{GrantedDocUserType.graphql.swift => AdminLicensePreview.graphql.swift} (66%) create mode 100644 packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/CreateWorkspaceByokLocalLeaseResultType.graphql.swift rename packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/{ContextWorkspaceEmbeddingStatus.graphql.swift => TestWorkspaceByokConfigResultType.graphql.swift} (60%) create mode 100644 packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/WorkspaceByokCapabilityWarningType.graphql.swift rename packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/{InviteUserType.graphql.swift => WorkspaceByokKeyConfigType.graphql.swift} (63%) rename packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/{GrantedDocUserTypeEdge.graphql.swift => WorkspaceByokSettingsType.graphql.swift} (64%) rename packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/{PaginatedGrantedDocUserType.graphql.swift => WorkspaceByokUsagePointType.graphql.swift} (63%) delete mode 100644 packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/ai/actions/redeem.tsx diff --git a/.docker/selfhost/schema.json b/.docker/selfhost/schema.json index d08e11ae52..c10bef1e3b 100644 --- a/.docker/selfhost/schema.json +++ b/.docker/selfhost/schema.json @@ -1410,22 +1410,6 @@ } } }, - "customerIo": { - "type": "object", - "description": "Configuration for customerIo module", - "properties": { - "enabled": { - "type": "boolean", - "description": "Enable customer.io integration\n@default false", - "default": false - }, - "token": { - "type": "string", - "description": "Customer.io token\n@default \"\"", - "default": "" - } - } - }, "oauth": { "type": "object", "description": "Configuration for oauth module", @@ -1524,16 +1508,6 @@ "description": "Whether enable lifetime price and allow user to pay for it.\n@default true", "default": true }, - "apiKey": { - "type": "string", - "description": "[Deprecated] Stripe API key. Use payment.stripe.apiKey instead.\n@default \"\"\n@environment `STRIPE_API_KEY`", - "default": "" - }, - "webhookKey": { - "type": "string", - "description": "[Deprecated] Stripe webhook key. Use payment.stripe.webhookKey instead.\n@default \"\"\n@environment `STRIPE_WEBHOOK_KEY`", - "default": "" - }, "stripe": { "type": "object", "description": "Stripe sdk options and credentials\n@default {\"apiKey\":\"\",\"webhookKey\":\"\"}\n@link https://docs.stripe.com/api", diff --git a/Cargo.lock b/Cargo.lock index d88b5dfb18..3379e96503 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,7 +40,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -104,7 +104,7 @@ name = "affine_media_capture" version = "0.0.0" dependencies = [ "block2", - "core-foundation", + "core-foundation 0.10.1", "coreaudio-rs 0.12.1", "cpal", "criterion2", @@ -215,7 +215,6 @@ dependencies = [ "aes-gcm", "affine_common", "anyhow", - "base64-simd", "chrono", "file-format", "hex", @@ -235,8 +234,7 @@ dependencies = [ "p256", "rand 0.9.4", "rayon", - "reqwest", - "rustls", + "safefetch", "schemars", "serde", "serde_json", @@ -246,7 +244,6 @@ dependencies = [ "tokio", "url", "v_htmlescape", - "webpki-roots", "y-octo", ] @@ -1036,6 +1033,17 @@ dependencies = [ "libc", ] +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chinese-number" version = "0.7.8" @@ -1272,7 +1280,17 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bebfc66b5ef3f159b3c4a6097e7c2a2e2cc71ec77fff7913e9895fae45f4c349" dependencies = [ - "core-foundation", + "core-foundation 0.10.1", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] @@ -1299,7 +1317,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags 2.11.0", - "core-foundation", + "core-foundation 0.10.1", "core-graphics-types", "foreign-types", "libc", @@ -1312,7 +1330,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ "bitflags 2.11.0", - "core-foundation", + "core-foundation 0.10.1", "core-graphics-types", "foreign-types", "libc", @@ -1325,7 +1343,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.11.0", - "core-foundation", + "core-foundation 0.10.1", "libc", ] @@ -1336,7 +1354,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6592bdc3e2b3479140e8c64d419b62d613526c8b61136ca990cd0bae4377db4f" dependencies = [ "core-audio-types-rs", - "core-foundation", + "core-foundation 0.10.1", "core-utils-rs", "core-video-rs", "thiserror 2.0.18", @@ -1348,7 +1366,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "791d9e8eb99cff8db5e2b6a5bb420b893476b7a064bb6889b2a691c1bb6d745c" dependencies = [ - "core-foundation", + "core-foundation 0.10.1", "four-char-code", ] @@ -1358,7 +1376,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1f1614e163a5d6fb972305fa14fc5c2989143da0426c213b74785b31e1fadb8" dependencies = [ - "core-foundation", + "core-foundation 0.10.1", "core-graphics 0.24.0", "core-utils-rs", "io-surface", @@ -1424,7 +1442,7 @@ dependencies = [ "core-foundation-sys", "coreaudio-rs 0.11.3", "dasp_sample", - "jni", + "jni 0.21.1", "js-sys", "libc", "mach2", @@ -1446,6 +1464,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.4.0" @@ -1523,6 +1550,12 @@ dependencies = [ "walkdir", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -2473,6 +2506,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -2741,6 +2775,85 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hickory-net" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" +dependencies = [ + "async-trait", + "bytes", + "cfg-if", + "data-encoding", + "futures-channel", + "futures-io", + "futures-util", + "h2", + "hickory-proto", + "http", + "idna", + "ipnet", + "jni 0.22.4", + "rand 0.10.1", + "rustls", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tokio-rustls", + "tracing", + "url", + "webpki-roots", +] + +[[package]] +name = "hickory-proto" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" +dependencies = [ + "data-encoding", + "idna", + "ipnet", + "jni 0.22.4", + "once_cell", + "prefix-trie", + "rand 0.10.1", + "ring", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-net", + "hickory-proto", + "ipconfig", + "ipnet", + "jni 0.22.4", + "moka", + "ndk-context", + "once_cell", + "parking_lot", + "rand 0.10.1", + "resolv-conf", + "rustls", + "smallvec", + "system-configuration", + "thiserror 2.0.18", + "tokio", + "tokio-rustls", + "tracing", + "webpki-roots", +] + [[package]] name = "hkdf" version = "0.12.4" @@ -3333,16 +3446,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "554b8c5d64ec09a3a520fe58e4d48a73e00ff32899cdcbe32a4877afd4968b8e" dependencies = [ "cgl", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "leaky-cow", ] +[[package]] +name = "ipconfig" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +dependencies = [ + "socket2", + "widestring", + "windows-registry", + "windows-result 0.4.1", + "windows-sys 0.61.2", +] + [[package]] name = "ipnet" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +dependencies = [ + "serde", +] [[package]] name = "iri-string" @@ -3442,19 +3571,68 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-sys 0.3.0", "log", "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.117", +] + [[package]] name = "jni-sys" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -3529,7 +3707,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -4011,15 +4189,32 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", "windows-sys 0.61.2", ] +[[package]] +name = "moka" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + [[package]] name = "moxcms" version = "0.7.11" @@ -4138,7 +4333,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" dependencies = [ "bitflags 2.11.0", - "jni-sys", + "jni-sys 0.3.0", "log", "ndk-sys", "num_enum", @@ -4157,7 +4352,7 @@ version = "0.5.0+25.2.9519653" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" dependencies = [ - "jni-sys", + "jni-sys 0.3.0", ] [[package]] @@ -4431,7 +4626,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" dependencies = [ - "jni", + "jni 0.21.1", "ndk", "ndk-context", "num-derive", @@ -4462,6 +4657,10 @@ name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "once_cell_polyfill" @@ -4900,7 +5099,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -4970,6 +5169,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "prefix-trie" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" +dependencies = [ + "either", + "ipnet", + "num-traits", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -5246,6 +5456,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -5284,6 +5505,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_distr" version = "0.5.1" @@ -5458,9 +5685,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64", "bytes", @@ -5496,6 +5723,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + [[package]] name = "rfc6979" version = "0.4.0" @@ -5706,9 +5939,9 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" dependencies = [ - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", - "jni", + "jni 0.21.1", "log", "once_cell", "rustls", @@ -5781,6 +6014,22 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "safefetch" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69a2a7c208b48e0fb427db7d16ce97ed87982cfc2484ba4407f1eafb45055364" +dependencies = [ + "anyhow", + "hickory-resolver", + "image", + "reqwest", + "rustls", + "tokio", + "url", + "webpki-roots", +] + [[package]] name = "same-file" version = "1.0.6" @@ -5842,7 +6091,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c62ae379564f834110c5b020cc731a7c14b8ec4879b4367579a467a0a704c21" dependencies = [ "block2", - "core-foundation", + "core-foundation 0.10.1", "core-graphics 0.25.0", "core-media-rs", "core-utils-rs", @@ -5892,7 +6141,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags 2.11.0", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -6014,7 +6263,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -6025,7 +6274,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -6080,6 +6329,22 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "simplecss" version = "0.2.2" @@ -6765,6 +7030,33 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tempfile" version = "3.27.0" @@ -6978,9 +7270,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -6995,9 +7287,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -8581,6 +8873,17 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.1.2" diff --git a/Cargo.toml b/Cargo.toml index da34f4ee8c..d9e753ab12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,6 +97,7 @@ resolver = "3" readability = { version = "0.3.0", default-features = false } regex = "1.10" rubato = "0.16" + safefetch = "0.1.0" schemars = "0.8" screencapturekit = "0.3" serde = "1" diff --git a/packages/backend/native/Cargo.toml b/packages/backend/native/Cargo.toml index 9cf1a0194d..5885c11a7e 100644 --- a/packages/backend/native/Cargo.toml +++ b/packages/backend/native/Cargo.toml @@ -17,7 +17,6 @@ affine_common = { workspace = true, features = [ "ydoc-loader", ] } anyhow = { workspace = true } -base64-simd = { workspace = true } chrono = { workspace = true } file-format = { workspace = true } hex = { workspace = true } @@ -34,11 +33,7 @@ napi = { workspace = true, features = ["async", "serde-json"] } napi-derive = { workspace = true } p256 = { workspace = true } rand = { workspace = true } -reqwest = { version = "0.13.3", default-features = false, features = [ - "blocking", - "rustls", -] } -rustls = "0.23" +safefetch = { workspace = true } schemars = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } @@ -47,7 +42,6 @@ sha3 = { workspace = true } tiktoken-rs = { workspace = true } url = { workspace = true } v_htmlescape = { workspace = true } -webpki-roots = "1" y-octo = { workspace = true, features = ["large_refs"] } [target.'cfg(not(target_os = "linux"))'.dependencies] diff --git a/packages/backend/native/index.d.ts b/packages/backend/native/index.d.ts index aa98677b87..e19c6138f2 100644 --- a/packages/backend/native/index.d.ts +++ b/packages/backend/native/index.d.ts @@ -48,6 +48,8 @@ export interface ActionTrace { errorCode?: string } +export declare function activateLicense(request: LicenseKeyRequest): Promise + /** * Adds a document ID to the workspace root doc's meta.pages array. * This registers the document in the workspace so it appears in the UI. @@ -151,11 +153,17 @@ export interface CapabilityModelContract { capabilities: Array } +export declare function checkLicenseHealth(request: LicenseHealthRequest): Promise + export interface Chunk { index: number content: string } +export interface CommandResponse { + error?: LicenseError +} + /** * Converts markdown content to AFFiNE-compatible y-octo document binary. * @@ -169,6 +177,10 @@ export interface Chunk { */ export declare function createDocWithMarkdown(title: string, markdown: string, docId: string): Buffer +export declare function createLicenseCustomerPortal(request: LicenseKeyRequest): Promise + +export declare function deactivateLicense(request: LicenseKeyRequest): Promise + export declare function evaluatePermissionV1(input: any): any export declare function fetchRemoteAttachment(request: RemoteAttachmentFetchRequest): Promise @@ -195,6 +207,43 @@ export declare function inferRemoteMimeType(request: RemoteMimeTypeRequest): Pro export declare function inspectImageForProxy(input: Buffer, options?: ImageInspectionOptions | undefined | null): ImageInspection +export interface LicenseError { + status: number + body: string +} + +export interface LicenseHealthRequest { + licenseKey: string + validateKey: string +} + +export interface LicenseInfo { + plan: string + recurring: string + quantity: number + expiresAt: number + validateKey: string +} + +export interface LicenseKeyRequest { + licenseKey: string +} + +export interface LicenseRecurringRequest { + licenseKey: string + recurring: string +} + +export interface LicenseResponse { + license?: LicenseInfo + error?: LicenseError +} + +export interface LicenseSeatsRequest { + licenseKey: string + seats: number +} + export declare function llmBuildCanonicalRequest(request: CanonicalChatRequestContract): LlmRequestContract export declare function llmBuildCanonicalStructuredRequest(request: CanonicalStructuredRequestContract): LlmStructuredRequestContract @@ -481,6 +530,11 @@ export declare function permissionActionRoleMatrixV1(): any export declare function permissionActionRoleMatrixV1Json(): string +export interface PortalResponse { + url?: string + error?: LicenseError +} + export declare function processImage(input: Buffer, maxEdge: number, keepExif: boolean): Promise export type PromptBuiltin = 'Date'| @@ -687,15 +741,26 @@ export declare function runNativeActionRecipePreparedStream(input: ActionRuntime export declare function safeFetch(request: SafeFetchRequest): Promise export type SafeFetchMethod = 'get'| -'head'; +'head'| +'post'| +'put'| +'propfind'| +'report'; export interface SafeFetchRequest { url: string method?: SafeFetchMethod headers?: Record + body?: Buffer timeoutMs?: number maxRedirects?: number maxBytes?: number + allowedHeaders?: Array + allowedHosts?: Array + allowHttp?: boolean + allowPrivateTargetOrigin?: boolean + enableEch?: boolean + echConfigList?: Buffer } export interface SafeFetchResponse { @@ -754,6 +819,10 @@ export declare function updateDocTitle(existingBinary: Buffer, title: string, do */ export declare function updateDocWithMarkdown(existingBinary: Buffer, newMarkdown: string, docId: string): Buffer +export declare function updateLicenseRecurring(request: LicenseRecurringRequest): Promise + +export declare function updateLicenseSeats(request: LicenseSeatsRequest): Promise + /** * Updates a document title in the workspace root doc's meta.pages array. * diff --git a/packages/backend/native/src/lib.rs b/packages/backend/native/src/lib.rs index 3130bd0f50..faac1a1ba6 100644 --- a/packages/backend/native/src/lib.rs +++ b/packages/backend/native/src/lib.rs @@ -9,6 +9,7 @@ pub mod file_type; pub mod hashcash; pub mod html_sanitize; pub mod image; +pub mod license; pub mod llm; pub mod permission; pub mod safe_fetch; diff --git a/packages/backend/native/src/license.rs b/packages/backend/native/src/license.rs new file mode 100644 index 0000000000..f82782ca15 --- /dev/null +++ b/packages/backend/native/src/license.rs @@ -0,0 +1,501 @@ +use std::{ + collections::HashMap, + sync::{Mutex, OnceLock}, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +use anyhow::{Context, Result as AnyResult, bail}; +use napi::{Env, Error, Result, Status, Task, bindgen_prelude::AsyncTask}; +use napi_derive::napi; +use serde::de::DeserializeOwned; +use url::Url; + +const AFFINE_PRO_ENDPOINT: &str = "https://app.affine.pro"; +const AFFINE_PRO_HOST: &str = "app.affine.pro"; +const AFFINE_PRO_REQUEST_TIMEOUT_MS: u32 = 10_000; +const AFFINE_PRO_MAX_BYTES: u32 = 1024 * 1024; +const ECH_DNS_QUERY_TIMEOUT_MS: u32 = 5_000; + +static AFFINE_PRO_ECH_CONFIG: OnceLock>>> = OnceLock::new(); + +#[napi(object)] +pub struct LicenseKeyRequest { + pub license_key: String, +} + +#[napi(object)] +pub struct LicenseHealthRequest { + pub license_key: String, + pub validate_key: String, +} + +#[napi(object)] +pub struct LicenseRecurringRequest { + pub license_key: String, + pub recurring: String, +} + +#[napi(object)] +pub struct LicenseSeatsRequest { + pub license_key: String, + pub seats: u32, +} + +#[napi(object)] +pub struct LicenseInfo { + pub plan: String, + pub recurring: String, + pub quantity: u32, + pub expires_at: f64, + pub validate_key: String, +} + +#[napi(object)] +pub struct LicenseError { + pub status: u16, + pub body: String, +} + +#[napi(object)] +pub struct LicenseResponse { + pub license: Option, + pub error: Option, +} + +#[napi(object)] +pub struct CommandResponse { + pub error: Option, +} + +#[napi(object)] +pub struct PortalResponse { + pub url: Option, + pub error: Option, +} + +pub struct AsyncActivateLicenseTask { + request: LicenseKeyRequest, +} + +pub struct AsyncDeactivateLicenseTask { + request: LicenseKeyRequest, +} + +pub struct AsyncCheckLicenseHealthTask { + request: LicenseHealthRequest, +} + +pub struct AsyncUpdateLicenseRecurringTask { + request: LicenseRecurringRequest, +} + +pub struct AsyncUpdateLicenseSeatsTask { + request: LicenseSeatsRequest, +} + +pub struct AsyncCreateCustomerPortalTask { + request: LicenseKeyRequest, +} + +#[napi] +impl Task for AsyncActivateLicenseTask { + type Output = LicenseResponse; + type JsValue = LicenseResponse; + + fn compute(&mut self) -> Result { + license_info( + &format!("/api/team/licenses/{}/activate", self.request.license_key), + safefetch::SafeFetchMethod::Post, + None, + None, + ) + .map_err(invalid_arg) + } + + fn resolve(&mut self, _: Env, output: Self::Output) -> Result { + Ok(output) + } +} + +#[napi] +pub fn activate_license(request: LicenseKeyRequest) -> AsyncTask { + AsyncTask::new(AsyncActivateLicenseTask { request }) +} + +#[napi] +impl Task for AsyncDeactivateLicenseTask { + type Output = CommandResponse; + type JsValue = CommandResponse; + + fn compute(&mut self) -> Result { + command( + &format!("/api/team/licenses/{}/deactivate", self.request.license_key), + safefetch::SafeFetchMethod::Post, + None, + None, + ) + .map_err(invalid_arg) + } + + fn resolve(&mut self, _: Env, output: Self::Output) -> Result { + Ok(output) + } +} + +#[napi] +pub fn deactivate_license(request: LicenseKeyRequest) -> AsyncTask { + AsyncTask::new(AsyncDeactivateLicenseTask { request }) +} + +#[napi] +impl Task for AsyncCheckLicenseHealthTask { + type Output = LicenseResponse; + type JsValue = LicenseResponse; + + fn compute(&mut self) -> Result { + license_info( + &format!("/api/team/licenses/{}/health", self.request.license_key), + safefetch::SafeFetchMethod::Get, + Some(HashMap::from([( + "x-validate-key".to_string(), + self.request.validate_key.clone(), + )])), + None, + ) + .map_err(invalid_arg) + } + + fn resolve(&mut self, _: Env, output: Self::Output) -> Result { + Ok(output) + } +} + +#[napi] +pub fn check_license_health(request: LicenseHealthRequest) -> AsyncTask { + AsyncTask::new(AsyncCheckLicenseHealthTask { request }) +} + +#[napi] +impl Task for AsyncUpdateLicenseRecurringTask { + type Output = CommandResponse; + type JsValue = CommandResponse; + + fn compute(&mut self) -> Result { + let body = serde_json::to_vec(&serde_json::json!({ + "recurring": self.request.recurring, + })) + .map_err(invalid_arg)?; + command( + &format!("/api/team/licenses/{}/recurring", self.request.license_key), + safefetch::SafeFetchMethod::Post, + None, + Some(body), + ) + .map_err(invalid_arg) + } + + fn resolve(&mut self, _: Env, output: Self::Output) -> Result { + Ok(output) + } +} + +#[napi] +pub fn update_license_recurring(request: LicenseRecurringRequest) -> AsyncTask { + AsyncTask::new(AsyncUpdateLicenseRecurringTask { request }) +} + +#[napi] +impl Task for AsyncUpdateLicenseSeatsTask { + type Output = CommandResponse; + type JsValue = CommandResponse; + + fn compute(&mut self) -> Result { + let body = serde_json::to_vec(&serde_json::json!({ + "seats": self.request.seats, + })) + .map_err(invalid_arg)?; + command( + &format!("/api/team/licenses/{}/seats", self.request.license_key), + safefetch::SafeFetchMethod::Post, + None, + Some(body), + ) + .map_err(invalid_arg) + } + + fn resolve(&mut self, _: Env, output: Self::Output) -> Result { + Ok(output) + } +} + +#[napi] +pub fn update_license_seats(request: LicenseSeatsRequest) -> AsyncTask { + AsyncTask::new(AsyncUpdateLicenseSeatsTask { request }) +} + +#[napi] +impl Task for AsyncCreateCustomerPortalTask { + type Output = PortalResponse; + type JsValue = PortalResponse; + + fn compute(&mut self) -> Result { + let response = match affine_pro_request( + &format!("/api/team/licenses/{}/create-customer-portal", self.request.license_key), + safefetch::SafeFetchMethod::Post, + None, + None, + ) { + Ok(response) => response, + Err(_) => { + return Ok(PortalResponse { + url: None, + error: Some(internal_affine_pro_error()), + }); + } + }; + if let Some(error) = affine_pro_error(&response) { + return Ok(PortalResponse { + url: None, + error: Some(error), + }); + } + let body: PortalPayload = match parse_body(&response) { + Ok(body) => body, + Err(_) => { + return Ok(PortalResponse { + url: None, + error: Some(internal_affine_pro_error()), + }); + } + }; + if body.url.is_empty() { + return Ok(PortalResponse { + url: None, + error: Some(internal_affine_pro_error()), + }); + } + Ok(PortalResponse { + url: Some(body.url), + error: None, + }) + } + + fn resolve(&mut self, _: Env, output: Self::Output) -> Result { + Ok(output) + } +} + +#[napi] +pub fn create_license_customer_portal(request: LicenseKeyRequest) -> AsyncTask { + AsyncTask::new(AsyncCreateCustomerPortalTask { request }) +} + +fn license_info( + path: &str, + method: safefetch::SafeFetchMethod, + headers: Option>, + body: Option>, +) -> AnyResult { + let response = match affine_pro_request(path, method, headers, body) { + Ok(response) => response, + Err(_) => { + return Ok(LicenseResponse { + license: None, + error: Some(internal_affine_pro_error()), + }); + } + }; + if let Some(error) = affine_pro_error(&response) { + return Ok(LicenseResponse { + license: None, + error: Some(error), + }); + } + let license = match parse_license_info(&response) { + Ok(license) => license, + Err(error) if error.to_string() == "license_expired" => { + return Ok(LicenseResponse { + license: None, + error: Some(license_expired_error()), + }); + } + Err(_) => { + return Ok(LicenseResponse { + license: None, + error: Some(internal_affine_pro_error()), + }); + } + }; + Ok(LicenseResponse { + license: Some(license), + error: None, + }) +} + +fn command( + path: &str, + method: safefetch::SafeFetchMethod, + headers: Option>, + body: Option>, +) -> AnyResult { + let response = match affine_pro_request(path, method, headers, body) { + Ok(response) => response, + Err(_) => { + return Ok(CommandResponse { + error: Some(internal_affine_pro_error()), + }); + } + }; + Ok(CommandResponse { + error: affine_pro_error(&response), + }) +} + +fn affine_pro_request( + path: &str, + method: safefetch::SafeFetchMethod, + headers: Option>, + body: Option>, +) -> AnyResult { + let url = Url::parse(AFFINE_PRO_ENDPOINT) + .context("invalid affine pro endpoint")? + .join(path) + .context("invalid affine pro path")?; + let mut headers = headers.unwrap_or_default(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + + safefetch::safe_fetch(&safefetch::SafeFetchRequest { + url: url.to_string(), + method: Some(method), + headers: Some(headers), + body, + timeout_ms: Some(AFFINE_PRO_REQUEST_TIMEOUT_MS), + max_redirects: Some(3), + max_bytes: Some(AFFINE_PRO_MAX_BYTES), + allowed_headers: Some(vec![ + "authorization".to_string(), + "content-type".to_string(), + "x-validate-key".to_string(), + ]), + allowed_hosts: Some(vec![AFFINE_PRO_HOST.to_string()]), + allow_http: Some(false), + allow_private_target_origin: None, + ech_config_list: Some(affine_pro_ech_config()?), + }) +} + +fn parse_license_info(response: &safefetch::SafeFetchResponse) -> AnyResult { + let body: LicensePayload = parse_body(response)?; + let expires_at = parse_future_end_at(&body.end_at)?; + Ok(LicenseInfo { + plan: body.plan, + recurring: body.recurring, + quantity: body.quantity, + expires_at, + validate_key: response.headers.get("x-next-validate-key").cloned().unwrap_or_default(), + }) +} + +fn affine_pro_error(response: &safefetch::SafeFetchResponse) -> Option { + if (200..300).contains(&response.status) { + return None; + } + let body = String::from_utf8_lossy(&response.body).to_string(); + if serde_json::from_str::(&body).is_err() { + return Some(internal_affine_pro_error()); + } + Some(LicenseError { + status: response.status, + body, + }) +} + +fn internal_affine_pro_error() -> LicenseError { + LicenseError { + status: 500, + body: serde_json::json!({ + "status": 500, + "type": "internal_server_error", + "name": "internal_server_error", + "message": "Failed to contact with https://app.affine.pro", + "data": null, + }) + .to_string(), + } +} + +fn license_expired_error() -> LicenseError { + LicenseError { + status: 400, + body: serde_json::json!({ + "status": 400, + "type": "bad_request", + "name": "license_expired", + "message": "License has expired.", + "data": null, + }) + .to_string(), + } +} + +fn parse_body(response: &safefetch::SafeFetchResponse) -> AnyResult { + serde_json::from_slice(&response.body).context("invalid affine pro response") +} + +fn parse_future_end_at(value: &serde_json::Value) -> AnyResult { + let millis = match value { + serde_json::Value::Number(number) => number.as_f64().context("invalid license expiration")?, + serde_json::Value::String(value) => value + .parse::() + .or_else(|_| chrono::DateTime::parse_from_rfc3339(value).map(|date| date.timestamp_millis() as f64)) + .context("invalid license expiration")?, + _ => bail!("invalid license expiration"), + }; + if !millis.is_finite() || millis <= now_millis() { + bail!("license_expired"); + } + Ok(millis) +} + +fn now_millis() -> f64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as f64 +} + +fn affine_pro_ech_config() -> AnyResult> { + let cache = AFFINE_PRO_ECH_CONFIG.get_or_init(|| Mutex::new(None)); + { + let cached = cache.lock().map_err(|_| anyhow::anyhow!("ech cache poisoned"))?; + if let Some(config) = cached.as_ref() { + return Ok(config.clone()); + } + } + + let config = safefetch::ech::cloudflare_https_ech_config_list( + AFFINE_PRO_HOST, + Duration::from_millis(ECH_DNS_QUERY_TIMEOUT_MS as u64), + )?; + let mut cached = cache.lock().map_err(|_| anyhow::anyhow!("ech cache poisoned"))?; + *cached = Some(config.clone()); + Ok(config) +} + +fn invalid_arg(error: impl ToString) -> Error { + Error::new(Status::InvalidArg, error.to_string()) +} + +#[derive(serde::Deserialize)] +struct LicensePayload { + plan: String, + recurring: String, + quantity: u32, + #[serde(rename = "endAt")] + end_at: serde_json::Value, +} + +#[derive(serde::Deserialize)] +struct PortalPayload { + url: String, +} diff --git a/packages/backend/native/src/safe_fetch.rs b/packages/backend/native/src/safe_fetch.rs index b8389ea6d8..5c32201ca3 100644 --- a/packages/backend/native/src/safe_fetch.rs +++ b/packages/backend/native/src/safe_fetch.rs @@ -1,46 +1,37 @@ -use std::{ - collections::HashMap, - io::{Cursor, Read}, - net::{IpAddr, SocketAddr, ToSocketAddrs}, - time::Duration, -}; +use std::collections::HashMap; -use ::image::{ImageFormat, ImageReader}; -use anyhow::{Context, Result as AnyResult, bail}; use napi::{ Env, Error, Result, Status, Task, bindgen_prelude::{AsyncTask, Buffer}, }; use napi_derive::napi; -use reqwest::{ - Method, - blocking::{Client, Response}, - header::{HeaderMap, HeaderName, HeaderValue, LOCATION}, -}; -use url::Url; - -const DEFAULT_TIMEOUT_MS: u32 = 10_000; -const DEFAULT_MAX_REDIRECTS: u32 = 3; -const DEFAULT_MAX_BYTES: u32 = 10 * 1024 * 1024; -const MAX_IMAGE_DIMENSION: u32 = 16_384; -const MAX_IMAGE_PIXELS: u64 = 40_000_000; #[napi(string_enum = "snake_case")] #[derive(Clone, Copy, Debug)] pub enum SafeFetchMethod { Get, Head, + Post, + Put, + Propfind, + Report, } #[napi(object)] -#[derive(Clone, Debug)] pub struct SafeFetchRequest { pub url: String, pub method: Option, pub headers: Option>, + pub body: Option, pub timeout_ms: Option, pub max_redirects: Option, pub max_bytes: Option, + pub allowed_headers: Option>, + pub allowed_hosts: Option>, + pub allow_http: Option, + pub allow_private_target_origin: Option, + pub enable_ech: Option, + pub ech_config_list: Option, } #[napi(object)] @@ -111,36 +102,27 @@ pub struct AsyncRemoteMimeTypeTask { request: RemoteMimeTypeRequest, } -pub struct SafeFetchOutput { - status: u16, - final_url: String, - headers: HashMap, - body: Vec, -} - -struct SafeFetchParams { - url: String, - method: Option, - headers: Option>, - timeout_ms: Option, - max_redirects: Option, - max_bytes: Option, - allow_private_origins: Option>, -} - -pub struct RemoteAttachmentFetchOutput { - final_url: String, - mime_type: String, - body: Vec, +impl From for safefetch::SafeFetchMethod { + fn from(method: SafeFetchMethod) -> Self { + match method { + SafeFetchMethod::Get => safefetch::SafeFetchMethod::Get, + SafeFetchMethod::Head => safefetch::SafeFetchMethod::Head, + SafeFetchMethod::Post => safefetch::SafeFetchMethod::Post, + SafeFetchMethod::Put => safefetch::SafeFetchMethod::Put, + SafeFetchMethod::Propfind => safefetch::SafeFetchMethod::Propfind, + SafeFetchMethod::Report => safefetch::SafeFetchMethod::Report, + } + } } #[napi] impl Task for AsyncSafeFetchTask { - type Output = SafeFetchOutput; + type Output = safefetch::SafeFetchResponse; type JsValue = SafeFetchResponse; fn compute(&mut self) -> Result { - safe_fetch_inner(&self.request).map_err(|error| Error::new(Status::InvalidArg, error.to_string())) + let request = safe_fetch_request(&self.request).map_err(invalid_arg)?; + safefetch::safe_fetch(&request).map_err(invalid_arg) } fn resolve(&mut self, _: Env, output: Self::Output) -> Result { @@ -160,29 +142,44 @@ pub fn safe_fetch(request: SafeFetchRequest) -> AsyncTask { #[napi] pub fn assert_safe_url(request: AssertSafeUrlRequest) -> Result<()> { - assert_safe_url_inner(&request).map_err(|error| Error::new(Status::InvalidArg, error.to_string())) + safefetch::assert_safe_url(&request.url).map_err(invalid_arg) } #[napi] pub fn inspect_image_for_proxy(input: Buffer, options: Option) -> Result { - inspect_image_for_proxy_inner( + let output = safefetch::inspect_image( &input, - options.unwrap_or(ImageInspectionOptions { + image_inspection_options(options.unwrap_or(ImageInspectionOptions { max_width: None, max_height: None, max_pixels: None, - }), + })), ) - .map_err(|error| Error::new(Status::InvalidArg, error.to_string())) + .map_err(invalid_arg)?; + Ok(ImageInspection { + mime_type: output.mime_type, + width: output.width, + height: output.height, + }) } #[napi] impl Task for AsyncRemoteAttachmentFetchTask { - type Output = RemoteAttachmentFetchOutput; + type Output = safefetch::RemoteAttachmentFetchResponse; type JsValue = RemoteAttachmentFetchResponse; fn compute(&mut self) -> Result { - fetch_remote_attachment_inner(&self.request).map_err(|error| Error::new(Status::InvalidArg, error.to_string())) + safefetch::fetch_remote_attachment(&safefetch::RemoteAttachmentFetchRequest { + url: self.request.url.clone(), + timeout_ms: self.request.timeout_ms, + max_bytes: self.request.max_bytes, + allow_private_target_origin: self.request.allow_private_target_origin, + expected_content_type_prefix: self.request.expected_content_type_prefix.clone(), + max_image_width: self.request.max_image_width, + max_image_height: self.request.max_image_height, + max_image_pixels: self.request.max_image_pixels, + }) + .map_err(invalid_arg) } fn resolve(&mut self, _: Env, output: Self::Output) -> Result { @@ -205,7 +202,10 @@ impl Task for AsyncRemoteMimeTypeTask { type JsValue = String; fn compute(&mut self) -> Result { - Ok(infer_remote_mime_type_inner(&self.request)) + Ok(safefetch::infer_remote_mime_type(&safefetch::RemoteMimeTypeRequest { + url: self.request.url.clone(), + timeout_ms: self.request.timeout_ms, + })) } fn resolve(&mut self, _: Env, output: Self::Output) -> Result { @@ -218,472 +218,41 @@ pub fn infer_remote_mime_type(request: RemoteMimeTypeRequest) -> AsyncTask AnyResult { - safe_fetch_params_inner(&SafeFetchParams { +pub(crate) fn safe_fetch_request(request: &SafeFetchRequest) -> anyhow::Result { + Ok(safefetch::SafeFetchRequest { url: request.url.clone(), - method: request.method, + method: request.method.map(Into::into), headers: request.headers.clone(), + body: request.body.as_ref().map(|body| body.to_vec()), timeout_ms: request.timeout_ms, max_redirects: request.max_redirects, max_bytes: request.max_bytes, - allow_private_origins: None, + allowed_headers: request.allowed_headers.clone(), + allowed_hosts: request.allowed_hosts.clone(), + allow_http: request.allow_http, + allow_private_target_origin: request.allow_private_target_origin, + ech_config_list: ech_config_list(request)?, }) } -fn safe_fetch_params_inner(request: &SafeFetchParams) -> AnyResult { - let timeout = Duration::from_millis(u64::from(request.timeout_ms.unwrap_or(DEFAULT_TIMEOUT_MS))); - let max_redirects = request.max_redirects.unwrap_or(DEFAULT_MAX_REDIRECTS); - let max_bytes = usize::try_from(request.max_bytes.unwrap_or(DEFAULT_MAX_BYTES)).context("invalid maxBytes")?; - let method = request.method.unwrap_or(SafeFetchMethod::Get); - let mut current = parse_safe_url(&request.url)?; - let headers = build_headers(request.headers.as_ref())?; - - for redirect_count in 0..=max_redirects { - let addrs = resolve_safe_socket_addrs(¤t, request.allow_private_origins.as_deref())?; - let client = build_pinned_client(¤t, &addrs, timeout)?; - let response = send_request(&client, method, current.clone(), headers.clone())?; - - if response.status().is_redirection() { - if redirect_count >= max_redirects { - bail!("too_many_redirects"); - } - let Some(location) = response.headers().get(LOCATION) else { - return response_to_output(response, current, max_bytes, method); - }; - let location = location.to_str().context("invalid redirect location")?; - current = parse_safe_url(current.join(location).context("invalid redirect location")?.as_str())?; - continue; - } - - return response_to_output(response, current, max_bytes, method); +fn image_inspection_options(options: ImageInspectionOptions) -> safefetch::ImageInspectionOptions { + safefetch::ImageInspectionOptions { + max_width: options.max_width, + max_height: options.max_height, + max_pixels: options.max_pixels, } - - bail!("too_many_redirects") } -fn fetch_remote_attachment_inner(request: &RemoteAttachmentFetchRequest) -> AnyResult { - let allow_private_origins = - private_target_origin_allowlist(&request.url, request.allow_private_target_origin.unwrap_or(false))?; - let response = safe_fetch_params_inner(&SafeFetchParams { - url: request.url.clone(), - method: Some(SafeFetchMethod::Get), - headers: None, - timeout_ms: request.timeout_ms, - max_redirects: Some(DEFAULT_MAX_REDIRECTS), - max_bytes: Some(request.max_bytes), - allow_private_origins, - })?; - if !(200..300).contains(&response.status) { - bail!("fetch_failed_status: {}", response.status); - } - let mime_type = normalize_mime_type(response.headers.get("content-type")); - if let Some(expected) = request.expected_content_type_prefix.as_deref() { - if !mime_type.starts_with(expected) { - bail!("content_type_mismatch"); - } - if expected.starts_with("image/") { - inspect_image_for_proxy_inner( - &response.body, - ImageInspectionOptions { - max_width: request.max_image_width, - max_height: request.max_image_height, - max_pixels: request.max_image_pixels, - }, - )?; - } - } - - Ok(RemoteAttachmentFetchOutput { - final_url: response.final_url, - mime_type, - body: response.body, - }) -} - -fn infer_remote_mime_type_inner(request: &RemoteMimeTypeRequest) -> String { - let Ok(url) = Url::parse(&request.url) else { - return "application/octet-stream".to_string(); - }; - if let Some(mime_type) = infer_mime_type_from_extension(&url) { - return mime_type.to_string(); - } - let Ok(response) = safe_fetch_params_inner(&SafeFetchParams { - url: request.url.clone(), - method: Some(SafeFetchMethod::Head), - headers: None, - timeout_ms: request.timeout_ms, - max_redirects: Some(DEFAULT_MAX_REDIRECTS), - max_bytes: Some(0), - allow_private_origins: None, - }) else { - return "application/octet-stream".to_string(); - }; - normalize_mime_type(response.headers.get("content-type")) -} - -fn private_target_origin_allowlist(raw_url: &str, allow_private_target_origin: bool) -> AnyResult>> { - if !allow_private_target_origin { +fn ech_config_list(request: &SafeFetchRequest) -> anyhow::Result>> { + if !request.enable_ech.unwrap_or(false) { return Ok(None); } - Ok(Some(vec![parse_safe_url(raw_url)?.origin().ascii_serialization()])) -} - -fn normalize_mime_type(value: Option<&String>) -> String { - value - .and_then(|value| value.split(';').next()) - .map(str::trim) - .filter(|value| !value.is_empty()) - .unwrap_or("application/octet-stream") - .to_string() -} - -fn infer_mime_type_from_extension(url: &Url) -> Option<&'static str> { - let extension = url.path_segments()?.next_back()?.rsplit_once('.')?.1; - match extension.to_ascii_lowercase().as_str() { - "pdf" => Some("application/pdf"), - "mp3" => Some("audio/mpeg"), - "opus" => Some("audio/opus"), - "ogg" => Some("audio/ogg"), - "aac" => Some("audio/aac"), - "m4a" => Some("audio/aac"), - "flac" => Some("audio/flac"), - "ogv" => Some("video/ogg"), - "wav" => Some("audio/wav"), - "png" => Some("image/png"), - "jpeg" | "jpg" => Some("image/jpeg"), - "webp" => Some("image/webp"), - "txt" | "md" => Some("text/plain"), - "mov" => Some("video/mov"), - "mpeg" => Some("video/mpeg"), - "mp4" => Some("video/mp4"), - "avi" => Some("video/avi"), - "wmv" => Some("video/wmv"), - "flv" => Some("video/flv"), - _ => None, - } -} - -fn assert_safe_url_inner(request: &AssertSafeUrlRequest) -> AnyResult<()> { - let url = parse_safe_url(&request.url)?; - resolve_safe_socket_addrs(&url, None)?; - Ok(()) -} - -fn parse_safe_url(raw: &str) -> AnyResult { - let url = Url::parse(raw).context("invalid_url")?; - match url.scheme() { - "http" | "https" => {} - _ => bail!("disallowed_protocol"), - } - if !url.username().is_empty() || url.password().is_some() { - bail!("url_has_credentials"); - } - if url.host_str().is_none() { - bail!("blocked_hostname"); - } - Ok(url) -} - -fn resolve_safe_socket_addrs(url: &Url, allow_private_origins: Option<&[String]>) -> AnyResult> { - let host = url.host_str().context("blocked_hostname")?; - let port = url.port_or_known_default().context("blocked_hostname")?; - let origin = url.origin().ascii_serialization(); - let allow_private = allow_private_origins - .map(|origins| origins.iter().any(|allowed| allowed == &origin)) - .unwrap_or(false); - let addrs: Vec = (host, port) - .to_socket_addrs() - .context("unresolvable_hostname")? - .collect(); - if addrs.is_empty() { - bail!("unresolvable_hostname"); - } - for addr in &addrs { - if is_blocked_ip(addr.ip()) && !allow_private { - bail!("blocked_ip"); - } - } - Ok(addrs) -} - -fn build_pinned_client(url: &Url, addrs: &[SocketAddr], timeout: Duration) -> AnyResult { - let host = url.host_str().context("blocked_hostname")?; - Client::builder() - .timeout(timeout) - .no_proxy() - .redirect(reqwest::redirect::Policy::none()) - .tls_backend_preconfigured(webpki_tls_config()?) - .resolve_to_addrs(host, addrs) - .build() - .context("failed to build http client") -} - -fn webpki_tls_config() -> AnyResult { - let root_store = rustls::RootCertStore { - roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(), + let Some(config_list) = request.ech_config_list.as_ref() else { + anyhow::bail!("ech_config_required"); }; - Ok( - rustls::ClientConfig::builder_with_provider(rustls::crypto::aws_lc_rs::default_provider().into()) - .with_safe_default_protocol_versions() - .context("failed to build tls protocol config")? - .with_root_certificates(root_store) - .with_no_client_auth(), - ) + Ok(Some(config_list.to_vec())) } -fn build_headers(headers: Option<&HashMap>) -> AnyResult { - let mut out = HeaderMap::new(); - let Some(headers) = headers else { - return Ok(out); - }; - for (name, value) in headers { - let lower = name.to_ascii_lowercase(); - if !(lower.starts_with("sec-") || lower.starts_with("accept") || lower == "user-agent") { - continue; - } - out.insert( - HeaderName::from_bytes(name.as_bytes()).context("invalid header name")?, - HeaderValue::from_str(value).context("invalid header value")?, - ); - } - Ok(out) -} - -fn send_request(client: &Client, method: SafeFetchMethod, url: Url, headers: HeaderMap) -> AnyResult { - let method = match method { - SafeFetchMethod::Get => Method::GET, - SafeFetchMethod::Head => Method::HEAD, - }; - client - .request(method, url) - .headers(headers) - .send() - .context("failed to fetch url") -} - -fn response_to_output( - mut response: Response, - url: Url, - max_bytes: usize, - method: SafeFetchMethod, -) -> AnyResult { - let status = response.status().as_u16(); - let headers = response_headers(response.headers()); - if matches!(method, SafeFetchMethod::Head) { - return Ok(SafeFetchOutput { - status, - final_url: url.to_string(), - headers, - body: Vec::new(), - }); - } - let mut body = Vec::new(); - if let Some(len) = response.content_length() - && len > max_bytes as u64 - { - bail!("response_too_large"); - } - response - .by_ref() - .take(u64::try_from(max_bytes).unwrap_or(u64::MAX) + 1) - .read_to_end(&mut body) - .context("failed to read response")?; - if body.len() > max_bytes { - bail!("response_too_large"); - } - Ok(SafeFetchOutput { - status, - final_url: url.to_string(), - headers, - body, - }) -} - -fn response_headers(headers: &HeaderMap) -> HashMap { - headers - .iter() - .filter_map(|(name, value)| { - value - .to_str() - .ok() - .map(|value| (name.as_str().to_string(), value.to_string())) - }) - .collect() -} - -fn is_blocked_ip(ip: IpAddr) -> bool { - match ip { - IpAddr::V4(ip) => { - ip.is_private() - || ip.is_loopback() - || ip.is_link_local() - || ip.is_broadcast() - || ip.is_multicast() - || ip.is_documentation() - || ip.octets()[0] == 0 - || ip.octets()[0] >= 224 - || ip.octets()[0] == 100 && (64..=127).contains(&ip.octets()[1]) - || ip.octets()[0] == 169 && ip.octets()[1] == 254 - || ip.octets()[0] == 198 && (18..=19).contains(&ip.octets()[1]) - || ip.octets()[0] == 192 && ip.octets()[1] == 0 && ip.octets()[2] == 0 - } - IpAddr::V6(ip) => { - if let Some(v4) = ip.to_ipv4_mapped() { - return is_blocked_ip(IpAddr::V4(v4)); - } - if let Some(v4) = extract_6to4_ipv4(ip).or_else(|| extract_teredo_client_ipv4(ip)) { - return is_blocked_ip(IpAddr::V4(v4)); - } - (ip.segments()[0] & 0xe000 != 0x2000) - || ip.is_loopback() - || ip.is_unspecified() - || ip.is_multicast() - || (ip.segments()[0] & 0xfe00 == 0xfc00) - || (ip.segments()[0] & 0xffc0 == 0xfe80) - || (ip.segments()[0] == 0x2001 && ip.segments()[1] == 0x0db8) - } - } -} - -fn extract_6to4_ipv4(ip: std::net::Ipv6Addr) -> Option { - let segments = ip.segments(); - if segments[0] != 0x2002 { - return None; - } - Some(std::net::Ipv4Addr::new( - (segments[1] >> 8) as u8, - segments[1] as u8, - (segments[2] >> 8) as u8, - segments[2] as u8, - )) -} - -fn extract_teredo_client_ipv4(ip: std::net::Ipv6Addr) -> Option { - let segments = ip.segments(); - if segments[0] != 0x2001 || segments[1] != 0 { - return None; - } - Some(std::net::Ipv4Addr::new( - (!(segments[6] >> 8)) as u8, - (!segments[6]) as u8, - (!(segments[7] >> 8)) as u8, - (!segments[7]) as u8, - )) -} - -fn inspect_image_for_proxy_inner(input: &[u8], options: ImageInspectionOptions) -> AnyResult { - let inspection = parse_image_header(input).context("failed to decode image")?; - validate_image_dimensions(&inspection, options)?; - Ok(inspection) -} - -fn validate_image_dimensions(image: &ImageInspection, options: ImageInspectionOptions) -> AnyResult<()> { - let max_width = options.max_width.unwrap_or(MAX_IMAGE_DIMENSION); - let max_height = options.max_height.unwrap_or(MAX_IMAGE_DIMENSION); - let max_pixels = u64::from(options.max_pixels.unwrap_or(MAX_IMAGE_PIXELS as u32)); - if image.width == 0 || image.height == 0 { - bail!("failed to decode image"); - } - if image.width > max_width || image.height > max_height { - bail!("image dimensions exceed limit"); - } - if u64::from(image.width) * u64::from(image.height) > max_pixels { - bail!("image pixel count exceeds limit"); - } - Ok(()) -} - -fn parse_image_header(input: &[u8]) -> AnyResult { - let format = ::image::guess_format(input).context("unsupported image format")?; - let mime_type = image_mime_type(format).context("unsupported image format")?; - let (width, height) = ImageReader::with_format(Cursor::new(input), format) - .into_dimensions() - .context("failed to decode image")?; - Ok(ImageInspection { - mime_type: mime_type.to_string(), - width, - height, - }) -} - -fn image_mime_type(format: ImageFormat) -> Option<&'static str> { - match format { - ImageFormat::Png => Some("image/png"), - ImageFormat::Jpeg => Some("image/jpeg"), - ImageFormat::Gif => Some("image/gif"), - ImageFormat::WebP => Some("image/webp"), - ImageFormat::Bmp => Some("image/bmp"), - _ => None, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn blocks_private_ips() { - assert!(is_blocked_ip("127.0.0.1".parse().unwrap())); - assert!(is_blocked_ip("10.0.0.1".parse().unwrap())); - assert!(is_blocked_ip("169.254.169.254".parse().unwrap())); - assert!(is_blocked_ip("::1".parse().unwrap())); - assert!(is_blocked_ip("::ffff:127.0.0.1".parse().unwrap())); - assert!(is_blocked_ip("2002:7f00:0001::1".parse().unwrap())); - assert!(is_blocked_ip("2002:c0a8:0001::1".parse().unwrap())); - assert!(is_blocked_ip( - "2001:0000:4136:e378:8000:63bf:807f:fffe".parse().unwrap() - )); - assert!(!is_blocked_ip("8.8.8.8".parse().unwrap())); - assert!(!is_blocked_ip("2002:0808:0808::1".parse().unwrap())); - } - - #[test] - fn builds_https_client_with_embedded_roots() { - let url = Url::parse("https://example.com/").unwrap(); - let addrs = ["93.184.216.34:443".parse().unwrap()]; - build_pinned_client(&url, &addrs, Duration::from_secs(1)).unwrap(); - } - - #[test] - fn inspects_png_dimensions_without_decode() { - let png = base64_simd::STANDARD - .decode_to_vec(b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jfJ8AAAAASUVORK5CYII=") - .unwrap(); - let inspected = inspect_image_for_proxy_inner( - &png, - ImageInspectionOptions { - max_width: None, - max_height: None, - max_pixels: None, - }, - ) - .unwrap(); - assert_eq!(inspected.mime_type, "image/png"); - assert_eq!(inspected.width, 1); - assert_eq!(inspected.height, 1); - } - - #[test] - fn rejects_oversized_dimensions() { - let png = [ - b"\x89PNG\r\n\x1a\n".as_slice(), - &[0, 0, 0, 13], - b"IHDR".as_slice(), - &100_000u32.to_be_bytes(), - &100_000u32.to_be_bytes(), - &[8, 6, 0, 0, 0], - ] - .concat(); - assert!( - inspect_image_for_proxy_inner( - &png, - ImageInspectionOptions { - max_width: None, - max_height: None, - max_pixels: None, - } - ) - .is_err() - ); - } +fn invalid_arg(error: impl ToString) -> Error { + Error::new(Status::InvalidArg, error.to_string()) } diff --git a/packages/backend/server/src/__tests__/auth/auth.e2e.ts b/packages/backend/server/src/__tests__/auth/auth.e2e.ts index d72c7ae050..d05207cf17 100644 --- a/packages/backend/server/src/__tests__/auth/auth.e2e.ts +++ b/packages/backend/server/src/__tests__/auth/auth.e2e.ts @@ -38,7 +38,7 @@ test('change email', async t => { const jwt = signedIn?.token.token; t.truthy(jwt); - await sendChangeEmail(app, u1Email, '/email-change'); + await sendChangeEmail(app, '/email-change'); const changeMail = app.mails.last('ChangeEmail'); @@ -157,12 +157,11 @@ test('should forbid graphql callbackUrl to external origin', async t => { .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ query: ` - mutation($email: String!, $callbackUrl: String!) { - sendChangeEmail(email: $email, callbackUrl: $callbackUrl) + mutation($callbackUrl: String!) { + sendChangeEmail(callbackUrl: $callbackUrl) } `, variables: { - email: u1Email, callbackUrl: 'https://evil.example', }, }) diff --git a/packages/backend/server/src/__tests__/copilot/byok.spec.ts b/packages/backend/server/src/__tests__/copilot/byok.spec.ts index 67148e6f3a..bf30b2f5e1 100644 --- a/packages/backend/server/src/__tests__/copilot/byok.spec.ts +++ b/packages/backend/server/src/__tests__/copilot/byok.spec.ts @@ -787,7 +787,7 @@ test('test key failure disables a saved key and success restores it', async t => apiKey: 'sk-test-primary', }); - const fetch = Sinon.stub(globalThis, 'fetch'); + const fetch = Sinon.stub(t.context.byok as any, 'probeFetch'); fetch .onFirstCall() .resolves( @@ -858,7 +858,7 @@ test('local key test does not mutate saved server config', async t => { apiKey: 'sk-server', }); - const fetch = Sinon.stub(globalThis, 'fetch').resolves( + const fetch = Sinon.stub(t.context.byok as any, 'probeFetch').resolves( new Response('{"error":"invalid sk-local"}', { status: 401 }) ); t.teardown(() => fetch.restore()); @@ -889,7 +889,7 @@ test('Gemini key test sends key in header and returns safe failure message', asy const { user, workspace } = await createUserWorkspace(t); await grantUserPlan(t, user.id); - const fetch = Sinon.stub(globalThis, 'fetch').resolves( + const fetch = Sinon.stub(t.context.byok as any, 'probeFetch').resolves( new Response( 'failed https://generativelanguage.googleapis.com/v1beta/models?key=gemini-secret', { status: 401 } @@ -916,6 +916,7 @@ test('Gemini key test sends key in header and returns safe failure message', asy ], 'gemini-secret' ); + t.deepEqual(fetch.firstCall.args[2]?.allowedHeaders, ['x-goog-api-key']); t.false(result.message?.includes('gemini-secret')); t.is(result.message, 'Provider rejected the BYOK key.'); }); @@ -924,7 +925,7 @@ test('FAL key test uses read-only platform API probe endpoint', async t => { const { user, workspace } = await createUserWorkspace(t); await grantUserPlan(t, user.id); - const fetch = Sinon.stub(globalThis, 'fetch').resolves( + const fetch = Sinon.stub(t.context.byok as any, 'probeFetch').resolves( new Response('{}', { status: 200 }) ); t.teardown(() => fetch.restore()); @@ -943,6 +944,7 @@ test('FAL key test uses read-only platform API probe endpoint', async t => { (fetch.firstCall.args[1]!.headers as Record).Authorization, 'Key fal-secret' ); + t.deepEqual(fetch.firstCall.args[2]?.allowedHeaders, ['Authorization']); }); test('provider test failures do not return raw provider response body', async t => { @@ -970,7 +972,7 @@ test('provider test failures do not return raw provider response body', async t message: 'Provider service is unavailable.', }, ]; - const fetch = Sinon.stub(globalThis, 'fetch'); + const fetch = Sinon.stub(t.context.byok as any, 'probeFetch'); for (const [index, matrixCase] of cases.entries()) { fetch .onCall(index) diff --git a/packages/backend/server/src/__tests__/copilot/copilot.spec.ts b/packages/backend/server/src/__tests__/copilot/copilot.spec.ts index 1edd3b5773..55f1431a1f 100644 --- a/packages/backend/server/src/__tests__/copilot/copilot.spec.ts +++ b/packages/backend/server/src/__tests__/copilot/copilot.spec.ts @@ -1673,6 +1673,7 @@ test('should be able to manage context', async t => { 'workspace.file.embed.finished', { contextId: session.id, + workspaceId: session.workspaceId, fileId: file.id, chunkSize: 1, }, diff --git a/packages/backend/server/src/__tests__/models/feature-user.spec.ts b/packages/backend/server/src/__tests__/models/feature-user.spec.ts index 76f546934a..0273a6c77b 100644 --- a/packages/backend/server/src/__tests__/models/feature-user.spec.ts +++ b/packages/backend/server/src/__tests__/models/feature-user.spec.ts @@ -53,7 +53,7 @@ test('admin feature resolver rejects commercial projection features', async t => test('should get null if user feature not found', async t => { const { model, u1 } = t.context; - const userFeature = await model.get(u1.id, 'ai_early_access'); + const userFeature = await model.get(u1.id, 'administrator'); t.is(userFeature, null); }); @@ -93,7 +93,7 @@ test('should directly test user feature existence', async t => { await model.add(u1.id, 'free_plan_v1', 'legacy projection'); t.true(await model.has(u1.id, 'free_plan_v1')); - t.false(await model.has(u1.id, 'ai_early_access')); + t.false(await model.has(u1.id, 'administrator')); }); test('should add user feature', async t => { diff --git a/packages/backend/server/src/__tests__/oauth/controller.spec.ts b/packages/backend/server/src/__tests__/oauth/controller.spec.ts index 28cf546946..5497740dd7 100644 --- a/packages/backend/server/src/__tests__/oauth/controller.spec.ts +++ b/packages/backend/server/src/__tests__/oauth/controller.spec.ts @@ -924,7 +924,7 @@ test('oidc should not fall back to default email claim when custom claim is conf test('oidc discovery should remove oauth feature on failure and restore it after backoff retry succeeds', async t => { const { provider, factory, server } = createOidcRegistrationHarness(); - const fetchStub = Sinon.stub(globalThis, 'fetch'); + const fetchStub = Sinon.stub(provider as any, 'oidcFetch'); const scheduledRetries: Array<() => void> = []; const retryDelays: number[] = []; const setTimeoutStub = Sinon.stub(globalThis, 'setTimeout').callsFake((( diff --git a/packages/backend/server/src/__tests__/payment/__snapshots__/service.spec.ts.md b/packages/backend/server/src/__tests__/payment/__snapshots__/service.spec.ts.md deleted file mode 100644 index fd46a6d8cb..0000000000 --- a/packages/backend/server/src/__tests__/payment/__snapshots__/service.spec.ts.md +++ /dev/null @@ -1,121 +0,0 @@ -# Snapshot report for `src/__tests__/payment/service.spec.ts` - -The actual snapshot is saved in `service.spec.ts.snap`. - -Generated by [AVA](https://avajs.dev). - -## should list normal price for unauthenticated user - -> Snapshot 1 - - [ - 'pro_monthly', - 'pro_yearly', - 'pro_lifetime', - 'ai_yearly', - 'team_monthly', - 'team_yearly', - ] - -## should list normal prices for authenticated user - -> Snapshot 1 - - [ - 'pro_monthly', - 'pro_yearly', - 'pro_lifetime', - 'ai_yearly', - 'team_monthly', - 'team_yearly', - ] - -## should not show lifetime price if not enabled - -> Snapshot 1 - - [ - 'pro_monthly', - 'pro_yearly', - 'ai_yearly', - 'team_monthly', - 'team_yearly', - ] - -## should list early access prices for pro ea user - -> Snapshot 1 - - [ - 'pro_monthly', - 'pro_lifetime', - 'pro_yearly_earlyaccess', - 'ai_yearly', - 'team_monthly', - 'team_yearly', - ] - -## should list normal prices for pro ea user with old subscriptions - -> Snapshot 1 - - [ - 'pro_monthly', - 'pro_yearly', - 'pro_lifetime', - 'ai_yearly', - 'team_monthly', - 'team_yearly', - ] - -## should list early access prices for ai ea user - -> Snapshot 1 - - [ - 'pro_monthly', - 'pro_yearly', - 'pro_lifetime', - 'ai_yearly_earlyaccess', - 'team_monthly', - 'team_yearly', - ] - -## should list early access prices for pro and ai ea user - -> Snapshot 1 - - [ - 'pro_monthly', - 'pro_lifetime', - 'pro_yearly_earlyaccess', - 'ai_yearly_earlyaccess', - 'team_monthly', - 'team_yearly', - ] - -## should list normal prices for ai ea user with old subscriptions - -> Snapshot 1 - - [ - 'pro_monthly', - 'pro_yearly', - 'pro_lifetime', - 'ai_yearly', - 'team_monthly', - 'team_yearly', - ] - -## should be able to list prices for team - -> Snapshot 1 - - [ - 'pro_monthly', - 'pro_yearly', - 'pro_lifetime', - 'ai_yearly', - 'team_monthly', - 'team_yearly', - ] diff --git a/packages/backend/server/src/__tests__/payment/__snapshots__/service.spec.ts.snap b/packages/backend/server/src/__tests__/payment/__snapshots__/service.spec.ts.snap deleted file mode 100644 index 165ed37186952cad5b8f7265f9c80af731fcecde..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 527 zcmV+q0`UDoRzVvt{h+ zb__qumLH1<00000000B+)v<2VKpY0}@8TqFL!>AQbiS!uDJ)D#qzpVmS*XtTMLu!4 zv+m9cVL&kU0lIYw6MF|9fCqqu2jBr%cmPE0#EF$QPLw7RLp)fPKK*s}Pv2i2^?a#= zxAx*Qcfv{j?x~G*B10;LbyR87XOhN71f2G?p^0LNi@0(kV2+10vD^r&SZpKh?59x} zGRMyTc-TXFk$tk)1@H=h58wlUD*#^s`~=V;^){)U8pDQ!bq)Yvm$2oMeRLVIvKPoS9ix$+KpfNzvm@^m(3od3wOa++cM&=s4@oA6c8e zPnM2HbG|2w_-y94(<{|Gp6DsbRb+rwG63Ed>a@MJE4``?Q22v@hY`sVvSbjc3WuW`gV_| zc6Sx_9*)meU2mqsGSAnfv24x11Ni+vuKCt*Uo810i`~Yrj(Ie}^f^uc!PM!oYXLN! R+L_&q{sT}ZoOWCb008V-_{snP diff --git a/packages/backend/server/src/__tests__/payment/event.spec.ts b/packages/backend/server/src/__tests__/payment/event.spec.ts index 742a2dcdde..88f3c08534 100644 --- a/packages/backend/server/src/__tests__/payment/event.spec.ts +++ b/packages/backend/server/src/__tests__/payment/event.spec.ts @@ -7,7 +7,7 @@ import { WorkspacePolicyService } from '../../core/permission'; import { QuotaStateService } from '../../core/quota/state'; import { WorkspaceService } from '../../core/workspaces'; import { Models } from '../../models'; -import { LicenseService } from '../../plugins/license/service'; +import { licenseClient, LicenseService } from '../../plugins/license/service'; import { PaymentEventHandlers } from '../../plugins/payment/event'; import { SubscriptionPlan, @@ -19,6 +19,12 @@ type Context = Record; const test = ava as TestFn; +const originalActivateLicense = licenseClient.activate; + +test.afterEach.always(() => { + licenseClient.activate = originalActivateLicense; +}); + test('workspace subscription activation only sends upgrade notification', async t => { const events: Array<{ name: string; payload: unknown }> = []; let reconciled = false; @@ -120,7 +126,6 @@ test('onetime selfhost license seat allocation ignores projected license quantit test('recurring selfhost license activation returns activation projection without remote health recheck', async t => { const events: Array<{ name: string; payload: unknown }> = []; - const affineProRequests: string[] = []; const upserts: unknown[] = []; const entitlements: unknown[] = []; const expiresAt = Date.now() + 30 * 24 * 60 * 60 * 1000; @@ -154,28 +159,17 @@ test('recurring selfhost license activation returns activation projection withou {} as unknown as QuotaStateService ); - ( - service as unknown as { - fetchAffinePro: (path: string) => Promise<{ - plan: SubscriptionPlan; - recurring: SubscriptionRecurring; - quantity: number; - endAt: number; - res: Response; - }>; - } - ).fetchAffinePro = async (path: string) => { - affineProRequests.push(path); + let activatedLicenseKey: string | undefined; + licenseClient.activate = async ({ licenseKey }) => { + activatedLicenseKey = licenseKey; return { - plan: SubscriptionPlan.SelfHostedTeam, - recurring: SubscriptionRecurring.Monthly, - quantity: 3, - endAt: expiresAt, - res: new Response(null, { - headers: { - 'x-next-validate-key': 'next-validate-key', - }, - }), + license: { + plan: SubscriptionPlan.SelfHostedTeam, + recurring: SubscriptionRecurring.Monthly, + quantity: 3, + expiresAt, + validateKey: 'next-validate-key', + }, }; }; @@ -189,7 +183,7 @@ test('recurring selfhost license activation returns activation projection withou }); t.is(entitlements.length, 1); t.is(upserts.length, 1); - t.deepEqual(affineProRequests, ['/api/team/licenses/license-key/activate']); + t.is(activatedLicenseKey, 'license-key'); t.deepEqual(events, [ { name: 'workspace.subscription.activated', diff --git a/packages/backend/server/src/__tests__/payment/service.spec.ts b/packages/backend/server/src/__tests__/payment/service.spec.ts index db798bbffb..1e7b2f9e2c 100644 --- a/packages/backend/server/src/__tests__/payment/service.spec.ts +++ b/packages/backend/server/src/__tests__/payment/service.spec.ts @@ -10,17 +10,14 @@ import { EventBus } from '../../base'; import { ConfigFactory, ConfigModule } from '../../base/config'; import { CurrentUser } from '../../core/auth'; import { AuthService } from '../../core/auth/service'; -import { EarlyAccessType, FeatureService } from '../../core/features'; import { SubscriptionCronJobs } from '../../plugins/payment/cron'; import { SubscriptionService } from '../../plugins/payment/service'; import { StripeFactory } from '../../plugins/payment/stripe'; import { - CouponType, encodeLookupKey, SubscriptionPlan, SubscriptionRecurring, SubscriptionStatus, - SubscriptionVariant, } from '../../plugins/payment/types'; import { createTestingApp, type TestingApp } from '../utils'; @@ -31,15 +28,25 @@ const unixNow = () => { const PRO_MONTHLY = `${SubscriptionPlan.Pro}_${SubscriptionRecurring.Monthly}`; const PRO_YEARLY = `${SubscriptionPlan.Pro}_${SubscriptionRecurring.Yearly}`; const PRO_LIFETIME = `${SubscriptionPlan.Pro}_${SubscriptionRecurring.Lifetime}`; -const PRO_EA_YEARLY = `${SubscriptionPlan.Pro}_${SubscriptionRecurring.Yearly}_${SubscriptionVariant.EA}`; const AI_YEARLY = `${SubscriptionPlan.AI}_${SubscriptionRecurring.Yearly}`; -const AI_YEARLY_EA = `${SubscriptionPlan.AI}_${SubscriptionRecurring.Yearly}_${SubscriptionVariant.EA}`; const TEAM_MONTHLY = `${SubscriptionPlan.Team}_${SubscriptionRecurring.Monthly}`; const TEAM_YEARLY = `${SubscriptionPlan.Team}_${SubscriptionRecurring.Yearly}`; -// prices for code redeeming -const PRO_MONTHLY_CODE = `${SubscriptionPlan.Pro}_${SubscriptionRecurring.Monthly}_${SubscriptionVariant.Onetime}`; -const PRO_YEARLY_CODE = `${SubscriptionPlan.Pro}_${SubscriptionRecurring.Yearly}_${SubscriptionVariant.Onetime}`; -const AI_YEARLY_CODE = `${SubscriptionPlan.AI}_${SubscriptionRecurring.Yearly}_${SubscriptionVariant.Onetime}`; +const NORMAL_USER_PRICES = [ + PRO_MONTHLY, + PRO_YEARLY, + PRO_LIFETIME, + AI_YEARLY, + TEAM_MONTHLY, + TEAM_YEARLY, +]; + +const NORMAL_USER_PRICES_WITHOUT_LIFETIME = [ + PRO_MONTHLY, + PRO_YEARLY, + AI_YEARLY, + TEAM_MONTHLY, + TEAM_YEARLY, +]; const PRICES = { [PRO_MONTHLY]: { @@ -66,15 +73,6 @@ const PRICES = { id: PRO_LIFETIME, lookup_key: PRO_LIFETIME, }, - [PRO_EA_YEARLY]: { - recurring: { - interval: 'year', - }, - unit_amount: 5000, - currency: 'usd', - id: PRO_EA_YEARLY, - lookup_key: PRO_EA_YEARLY, - }, [AI_YEARLY]: { recurring: { interval: 'year', @@ -84,33 +82,6 @@ const PRICES = { id: AI_YEARLY, lookup_key: AI_YEARLY, }, - [AI_YEARLY_EA]: { - recurring: { - interval: 'year', - }, - unit_amount: 9999, - currency: 'usd', - id: AI_YEARLY_EA, - lookup_key: AI_YEARLY_EA, - }, - [PRO_MONTHLY_CODE]: { - unit_amount: 799, - currency: 'usd', - id: PRO_MONTHLY_CODE, - lookup_key: PRO_MONTHLY_CODE, - }, - [PRO_YEARLY_CODE]: { - unit_amount: 8100, - currency: 'usd', - id: PRO_YEARLY_CODE, - lookup_key: PRO_YEARLY_CODE, - }, - [AI_YEARLY_CODE]: { - unit_amount: 10680, - currency: 'usd', - id: AI_YEARLY_CODE, - lookup_key: AI_YEARLY_CODE, - }, [TEAM_MONTHLY]: { unit_amount: 1500, currency: 'usd', @@ -164,7 +135,6 @@ const test = ava as TestFn<{ app: TestingApp; service: SubscriptionService; event: Sinon.SinonStubbedInstance; - feature: Sinon.SinonStubbedInstance; stripe: { customers: Sinon.SinonStubbedInstance; prices: Sinon.SinonStubbedInstance; @@ -203,16 +173,12 @@ test.before(async t => { AppModule, ], tapModule: m => { - m.overrideProvider(FeatureService).useValue( - Sinon.createStubInstance(FeatureService) - ); m.overrideProvider(EventBus).useValue(Sinon.createStubInstance(EventBus)); }, }); t.context.event = app.get(EventBus); t.context.service = app.get(SubscriptionService); - t.context.feature = app.get(FeatureService); t.context.db = app.get(PrismaClient); t.context.app = app; @@ -286,18 +252,21 @@ test('should list normal price for unauthenticated user', async t => { const prices = await service.listPrices(); - t.snapshot(prices.map(p => encodeLookupKey(p.lookupKey))); + t.deepEqual( + prices.map(p => encodeLookupKey(p.lookupKey)), + NORMAL_USER_PRICES + ); }); test('should list normal prices for authenticated user', async t => { - const { feature, service, u1 } = t.context; - - feature.isEarlyAccessUser.withArgs(u1.id).resolves(false); - feature.isEarlyAccessUser.withArgs(u1.id, EarlyAccessType.AI).resolves(false); + const { service, u1 } = t.context; const prices = await service.listPrices(u1); - t.snapshot(prices.map(p => encodeLookupKey(p.lookupKey))); + t.deepEqual( + prices.map(p => encodeLookupKey(p.lookupKey)), + NORMAL_USER_PRICES + ); }); test('should not show lifetime price if not enabled', async t => { @@ -311,25 +280,14 @@ test('should not show lifetime price if not enabled', async t => { const prices = await service.listPrices(t.context.u1); - t.snapshot(prices.map(p => encodeLookupKey(p.lookupKey))); + t.deepEqual( + prices.map(p => encodeLookupKey(p.lookupKey)), + NORMAL_USER_PRICES_WITHOUT_LIFETIME + ); }); -test('should list early access prices for pro ea user', async t => { - const { feature, service, u1 } = t.context; - - feature.isEarlyAccessUser.withArgs(u1.id).resolves(true); - feature.isEarlyAccessUser.withArgs(u1.id, EarlyAccessType.AI).resolves(false); - - const prices = await service.listPrices(u1); - - t.snapshot(prices.map(p => encodeLookupKey(p.lookupKey))); -}); - -test('should list normal prices for pro ea user with old subscriptions', async t => { - const { feature, service, u1, stripe } = t.context; - - feature.isEarlyAccessUser.withArgs(u1.id).resolves(true); - feature.isEarlyAccessUser.withArgs(u1.id, EarlyAccessType.AI).resolves(false); +test('should list normal prices for user with old pro subscriptions', async t => { + const { service, u1, stripe } = t.context; stripe.subscriptions.list.resolves({ data: [ @@ -352,35 +310,14 @@ test('should list normal prices for pro ea user with old subscriptions', async t const prices = await service.listPrices(u1); - t.snapshot(prices.map(p => encodeLookupKey(p.lookupKey))); + t.deepEqual( + prices.map(p => encodeLookupKey(p.lookupKey)), + NORMAL_USER_PRICES + ); }); -test('should list early access prices for ai ea user', async t => { - const { feature, service, u1 } = t.context; - - feature.isEarlyAccessUser.withArgs(u1.id).resolves(false); - feature.isEarlyAccessUser.withArgs(u1.id, EarlyAccessType.AI).resolves(true); - - const prices = await service.listPrices(u1); - - t.snapshot(prices.map(p => encodeLookupKey(p.lookupKey))); -}); - -test('should list early access prices for pro and ai ea user', async t => { - const { feature, service, u1 } = t.context; - - feature.isEarlyAccessUser.withArgs(u1.id).resolves(true); - - const prices = await service.listPrices(u1); - - t.snapshot(prices.map(p => encodeLookupKey(p.lookupKey))); -}); - -test('should list normal prices for ai ea user with old subscriptions', async t => { - const { feature, service, u1, stripe } = t.context; - - feature.isEarlyAccessUser.withArgs(u1.id).resolves(false); - feature.isEarlyAccessUser.withArgs(u1.id, EarlyAccessType.AI).resolves(true); +test('should list normal prices for user with old ai subscriptions', async t => { + const { service, u1, stripe } = t.context; stripe.subscriptions.list.resolves({ data: [ @@ -403,7 +340,10 @@ test('should list normal prices for ai ea user with old subscriptions', async t const prices = await service.listPrices(u1); - t.snapshot(prices.map(p => encodeLookupKey(p.lookupKey))); + t.deepEqual( + prices.map(p => encodeLookupKey(p.lookupKey)), + NORMAL_USER_PRICES + ); }); // ============= end prices ================ @@ -470,11 +410,9 @@ test('should allow checkout after local subscription period ended', async t => { }); test('should get correct pro plan price for checking out', async t => { - const { app, service, u1, stripe, feature } = t.context; - // non-ea user + const { app, service, u1, stripe } = t.context; + // monthly { - feature.isEarlyAccessUser.resolves(false); - await service.checkout( { plan: SubscriptionPlan.Pro, @@ -490,27 +428,8 @@ test('should get correct pro plan price for checking out', async t => { }); } - // ea user, but monthly + // yearly { - feature.isEarlyAccessUser.resolves(true); - await service.checkout( - { - plan: SubscriptionPlan.Pro, - recurring: SubscriptionRecurring.Monthly, - successCallbackLink: '', - }, - { user: u1 } - ); - - t.deepEqual(getLastCheckoutPrice(stripe.checkout.sessions.create), { - price: PRO_MONTHLY, - coupon: undefined, - }); - } - - // ea user, yearly - { - feature.isEarlyAccessUser.resolves(true); await service.checkout( { plan: SubscriptionPlan.Pro, @@ -521,14 +440,13 @@ test('should get correct pro plan price for checking out', async t => { ); t.deepEqual(getLastCheckoutPrice(stripe.checkout.sessions.create), { - price: PRO_EA_YEARLY, - coupon: CouponType.ProEarlyAccessOneYearFree, + price: PRO_YEARLY, + coupon: undefined, }); } - // ea user, yearly recurring, but has old subscription + // yearly recurring, but has old subscription { - feature.isEarlyAccessUser.resolves(true); stripe.subscriptions.list.resolves({ data: [ { @@ -561,27 +479,10 @@ test('should get correct pro plan price for checking out', async t => { price: PRO_YEARLY, coupon: undefined, }); - - await t.throwsAsync( - () => - service.checkout( - { - plan: SubscriptionPlan.Pro, - recurring: SubscriptionRecurring.Yearly, - variant: SubscriptionVariant.EA, - successCallbackLink: '', - }, - { user: u1 } - ), - { - message: 'You are trying to access a unknown subscription plan.', - } - ); } // any user, lifetime recurring { - feature.isEarlyAccessUser.resolves(false); app.get(ConfigFactory).override({ payment: { showLifetimePrice: true, @@ -605,12 +506,10 @@ test('should get correct pro plan price for checking out', async t => { }); test('should get correct ai plan price for checking out', async t => { - const { service, u1, stripe, feature } = t.context; + const { service, u1, stripe } = t.context; - // non-ea user + // user { - feature.isEarlyAccessUser.resolves(false); - await service.checkout( { plan: SubscriptionPlan.AI, @@ -626,111 +525,8 @@ test('should get correct ai plan price for checking out', async t => { }); } - // ea user + // user with old subscription { - feature.isEarlyAccessUser.resolves(true); - - await service.checkout( - { - plan: SubscriptionPlan.AI, - recurring: SubscriptionRecurring.Yearly, - successCallbackLink: '', - }, - { user: u1 } - ); - - t.deepEqual(getLastCheckoutPrice(stripe.checkout.sessions.create), { - price: AI_YEARLY_EA, - coupon: CouponType.AIEarlyAccessOneYearFree, - }); - } - - // ea user, but has old subscription - { - feature.isEarlyAccessUser.withArgs(u1.id).resolves(false); - feature.isEarlyAccessUser - .withArgs(u1.id, EarlyAccessType.AI) - .resolves(true); - stripe.subscriptions.list.resolves({ - data: [ - { - id: 'sub_1', - status: 'canceled', - items: { - data: [ - { - // @ts-expect-error stub - price: { - lookup_key: AI_YEARLY, - }, - }, - ], - }, - }, - ], - }); - - await service.checkout( - { - plan: SubscriptionPlan.AI, - recurring: SubscriptionRecurring.Yearly, - successCallbackLink: '', - }, - { user: u1 } - ); - - t.deepEqual(getLastCheckoutPrice(stripe.checkout.sessions.create), { - price: AI_YEARLY, - coupon: undefined, - }); - - await t.throwsAsync( - () => - service.checkout( - { - plan: SubscriptionPlan.AI, - recurring: SubscriptionRecurring.Yearly, - variant: SubscriptionVariant.EA, - successCallbackLink: '', - }, - { user: u1 } - ), - { - message: 'You are trying to access a unknown subscription plan.', - } - ); - } - - // pro ea user - { - feature.isEarlyAccessUser.withArgs(u1.id).resolves(true); - feature.isEarlyAccessUser - .withArgs(u1.id, EarlyAccessType.AI) - .resolves(false); - // @ts-expect-error stub - stripe.subscriptions.list.resolves({ data: [] }); - - await service.checkout( - { - plan: SubscriptionPlan.AI, - recurring: SubscriptionRecurring.Yearly, - successCallbackLink: '', - }, - { user: u1 } - ); - - t.deepEqual(getLastCheckoutPrice(stripe.checkout.sessions.create), { - price: AI_YEARLY, - coupon: CouponType.ProEarlyAccessAIOneYearFree, - }); - } - - // pro ea user, but has old subscription - { - feature.isEarlyAccessUser.withArgs(u1.id).resolves(true); - feature.isEarlyAccessUser - .withArgs(u1.id, EarlyAccessType.AI) - .resolves(false); stripe.subscriptions.list.resolves({ data: [ { @@ -1218,44 +1014,6 @@ const lifetimeInvoice: Stripe.Invoice = { }, }; -const onetimeMonthlyInvoice: Stripe.Invoice = { - id: 'in_2', - object: 'invoice', - amount_paid: 799, - total: 799, - customer: 'cus_1', - customer_email: 'u1@affine.pro', - currency: 'usd', - status: 'paid', - lines: { - data: [ - // @ts-expect-error stub - { - price: PRICES[PRO_MONTHLY_CODE], - }, - ], - }, -}; - -const onetimeYearlyInvoice: Stripe.Invoice = { - id: 'in_3', - object: 'invoice', - amount_paid: 8100, - total: 8100, - customer: 'cus_1', - customer_email: 'u1@affine.pro', - currency: 'usd', - status: 'paid', - lines: { - data: [ - // @ts-expect-error stub - { - price: PRICES[PRO_YEARLY_CODE], - }, - ], - }, -}; - test('should not be able to checkout for lifetime recurring if not enabled', async t => { const { service, u1, app } = t.context; app.get(ConfigFactory).override({ @@ -1337,32 +1095,6 @@ test('should not be able to checkout for lifetime recurring if already subscribe ), { message: 'You have already subscribed to the pro plan.' } ); - - await db.subscription.updateMany({ - where: { targetId: u1.id }, - data: { - stripeSubscriptionId: null, - recurring: SubscriptionRecurring.Monthly, - variant: SubscriptionVariant.Onetime, - end: new Date(Date.now() + 100000), - }, - }); - - await t.throwsAsync( - () => - service.checkout( - { - plan: SubscriptionPlan.Pro, - recurring: SubscriptionRecurring.Lifetime, - variant: null, - successCallbackLink: '', - }, - { - user: u1, - } - ), - { message: 'You have already subscribed to the pro plan.' } - ); }); test('should be able to subscribe to lifetime recurring', async t => { @@ -1474,226 +1206,16 @@ test('should not be able to update lifetime recurring', async t => { ); }); -// ============== Onetime Subscription =============== -test('should be able to checkout for onetime payment', async t => { - const { service, u1, stripe } = t.context; - - await service.checkout( - { - plan: SubscriptionPlan.Pro, - recurring: SubscriptionRecurring.Monthly, - variant: SubscriptionVariant.Onetime, - successCallbackLink: '', - }, - { - user: u1, - } - ); - - t.true(stripe.checkout.sessions.create.calledOnce); - const arg = stripe.checkout.sessions.create.firstCall - .args[0] as Stripe.Checkout.SessionCreateParams; - t.is(arg.mode, 'payment'); - t.deepEqual(getLastCheckoutPrice(stripe.checkout.sessions.create), { - price: PRO_MONTHLY_CODE, - coupon: undefined, - }); -}); - -test('should be able to checkout onetime payment if previous subscription is onetime', async t => { - const { service, u1, stripe, db } = t.context; - - await db.subscription.create({ - data: { - targetId: u1.id, - stripeSubscriptionId: 'sub_1', - plan: SubscriptionPlan.Pro, - recurring: SubscriptionRecurring.Monthly, - variant: SubscriptionVariant.Onetime, - status: SubscriptionStatus.Active, - start: new Date(), - end: new Date(Date.now() + 100000), - }, - }); - - await service.checkout( - { - plan: SubscriptionPlan.Pro, - recurring: SubscriptionRecurring.Monthly, - variant: SubscriptionVariant.Onetime, - successCallbackLink: '', - }, - { - user: u1, - } - ); - - t.true(stripe.checkout.sessions.create.calledOnce); - const arg = stripe.checkout.sessions.create.firstCall - .args[0] as Stripe.Checkout.SessionCreateParams; - t.is(arg.mode, 'payment'); - t.deepEqual(getLastCheckoutPrice(stripe.checkout.sessions.create), { - price: PRO_MONTHLY_CODE, - coupon: undefined, - }); -}); - -test('should not be able to checkout out onetime payment if previous subscription is not onetime', async t => { - const { service, u1, db } = t.context; - - await db.subscription.create({ - data: { - targetId: u1.id, - stripeSubscriptionId: 'sub_1', - plan: SubscriptionPlan.Pro, - recurring: SubscriptionRecurring.Monthly, - status: SubscriptionStatus.Active, - start: new Date(), - end: new Date(Date.now() + 100000), - }, - }); - - await t.throwsAsync( - () => - service.checkout( - { - plan: SubscriptionPlan.Pro, - recurring: SubscriptionRecurring.Monthly, - variant: SubscriptionVariant.Onetime, - successCallbackLink: '', - }, - { - user: u1, - } - ), - { message: 'You have already subscribed to the pro plan.' } - ); - - await db.subscription.updateMany({ - where: { targetId: u1.id }, - data: { - stripeSubscriptionId: null, - recurring: SubscriptionRecurring.Lifetime, - }, - }); - - await t.throwsAsync( - () => - service.checkout( - { - plan: SubscriptionPlan.Pro, - recurring: SubscriptionRecurring.Monthly, - variant: SubscriptionVariant.Onetime, - successCallbackLink: '', - }, - { - user: u1, - } - ), - { message: 'You have already subscribed to the pro plan.' } - ); -}); - -test('should be able to subscribe onetime payment subscription', async t => { - const { service, db, u1, event } = t.context; - - await service.saveStripeInvoice(onetimeMonthlyInvoice); - - const subInDB = await db.subscription.findFirst({ - where: { targetId: u1.id }, - }); - - t.true( - event.emit.calledOnceWith('user.subscription.activated', { - userId: u1.id, - plan: SubscriptionPlan.Pro, - recurring: SubscriptionRecurring.Monthly, - }) - ); - t.is(subInDB?.plan, SubscriptionPlan.Pro); - t.is(subInDB?.recurring, SubscriptionRecurring.Monthly); - t.is(subInDB?.status, SubscriptionStatus.Active); - t.is(subInDB?.stripeSubscriptionId, null); - t.is( - subInDB?.end?.toDateString(), - new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toDateString() - ); -}); - -test('should be able to accumulate onetime payment subscription period', async t => { - const { service, db, u1 } = t.context; - - await service.saveStripeInvoice(onetimeMonthlyInvoice); - - let subInDB = await db.subscription.findFirst({ - where: { targetId: u1.id }, - }); - - t.truthy(subInDB); - - let end = subInDB!.end!; - await service.saveStripeInvoice(onetimeYearlyInvoice); - subInDB = await db.subscription.findFirst({ - where: { targetId: u1.id }, - }); - - // add 365 days - t.is(subInDB!.end!.getTime(), end.getTime() + 365 * 24 * 60 * 60 * 1000); -}); - -test('should be able to recalculate onetime payment subscription period after expiration', async t => { - const { service, db, u1 } = t.context; - - await service.saveStripeInvoice(onetimeMonthlyInvoice); - - let subInDB = await db.subscription.findFirst({ - where: { targetId: u1.id }, - }); - - // make subscription expired - await db.subscription.update({ - where: { id: subInDB!.id }, - data: { - end: new Date(Date.now() - 1000), - }, - }); - await service.saveStripeInvoice(onetimeYearlyInvoice); - subInDB = await db.subscription.findFirst({ - where: { targetId: u1.id }, - }); - - // add 365 days from now - t.is( - subInDB?.end?.toDateString(), - new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toDateString() - ); -}); - -test('should not accumulate onetime payment subscription period for redeemed invoices', async t => { - const { service, db, u1 } = t.context; - - // save invoices received more than once, should only redeem them once. - await service.saveStripeInvoice(onetimeYearlyInvoice); - await service.saveStripeInvoice(onetimeYearlyInvoice); - await service.saveStripeInvoice(onetimeYearlyInvoice); - - const subInDB = await db.subscription.findFirst({ - where: { targetId: u1.id }, - }); - - t.is( - subInDB?.end?.toDateString(), - new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toDateString() - ); -}); - // TEAM test('should be able to list prices for team', async t => { const { service } = t.context; const prices = await service.listPrices(undefined); - t.snapshot(prices.map(p => encodeLookupKey(p.lookupKey))); + t.deepEqual( + prices.map(p => encodeLookupKey(p.lookupKey)), + NORMAL_USER_PRICES + ); }); test('should be able to checkout for team', async t => { diff --git a/packages/backend/server/src/__tests__/utils/invite.ts b/packages/backend/server/src/__tests__/utils/invite.ts index f20d6f1299..2d44795403 100644 --- a/packages/backend/server/src/__tests__/utils/invite.ts +++ b/packages/backend/server/src/__tests__/utils/invite.ts @@ -21,7 +21,7 @@ export async function inviteUsers( app: TestingApp, workspaceId: string, emails: string[] -): Promise> { +): Promise> { const res = await app.gql( ` mutation inviteMembers($workspaceId: String!, $emails: [String!]!) { @@ -31,7 +31,6 @@ export async function inviteUsers( ) { email inviteId - sentSuccess } } `, diff --git a/packages/backend/server/src/__tests__/utils/notification.ts b/packages/backend/server/src/__tests__/utils/notification.ts index 19de0c6335..d6012e0f5a 100644 --- a/packages/backend/server/src/__tests__/utils/notification.ts +++ b/packages/backend/server/src/__tests__/utils/notification.ts @@ -42,19 +42,6 @@ export async function listNotifications( return res.currentUser.notifications; } -export async function getNotificationCount(app: TestingApp): Promise { - const res = await app.gql( - ` - query notificationCount { - currentUser { - notificationCount - } - } - ` - ); - return res.currentUser.notificationCount; -} - export async function mentionUser( app: TestingApp, input: MentionInput diff --git a/packages/backend/server/src/__tests__/utils/user.ts b/packages/backend/server/src/__tests__/utils/user.ts index bb06d1dc04..9f85f511cd 100644 --- a/packages/backend/server/src/__tests__/utils/user.ts +++ b/packages/backend/server/src/__tests__/utils/user.ts @@ -34,12 +34,11 @@ export async function getPublicUserById( export async function sendChangeEmail( app: TestingApp, - email: string, callbackUrl: string ): Promise { const res = await app.gql(` mutation { - sendChangeEmail(email: "${email}", callbackUrl: "${callbackUrl}") + sendChangeEmail(callbackUrl: "${callbackUrl}") } `); diff --git a/packages/backend/server/src/app.module.ts b/packages/backend/server/src/app.module.ts index 48d29f683e..3f055264a2 100644 --- a/packages/backend/server/src/app.module.ts +++ b/packages/backend/server/src/app.module.ts @@ -56,7 +56,6 @@ import { ModelsModule } from './models'; import { CalendarModule } from './plugins/calendar'; import { CaptchaModule } from './plugins/captcha'; import { CopilotModule, CopilotRealtimeModule } from './plugins/copilot'; -import { CustomerIoModule } from './plugins/customerio'; import { GCloudModule } from './plugins/gcloud'; import { IndexerModule } from './plugins/indexer'; import { LicenseModule } from './plugins/license'; @@ -205,7 +204,6 @@ export function buildAppModule(env: Env) { CaptchaModule, OAuthModule, CalendarModule, - CustomerIoModule, TelemetryModule, CommentModule, AccessTokenModule, diff --git a/packages/backend/server/src/base/utils/ssrf.ts b/packages/backend/server/src/base/utils/ssrf.ts index 66aaf11f4d..ffc83b5624 100644 --- a/packages/backend/server/src/base/utils/ssrf.ts +++ b/packages/backend/server/src/base/utils/ssrf.ts @@ -10,6 +10,7 @@ export type SSRFBlockReason = | 'disallowed_protocol' | 'url_has_credentials' | 'blocked_hostname' + | 'host_not_allowed' | 'unresolvable_hostname' | 'blocked_ip' | 'too_many_redirects'; @@ -19,6 +20,7 @@ const SSRF_REASONS = new Set([ 'disallowed_protocol', 'url_has_credentials', 'blocked_hostname', + 'host_not_allowed', 'unresolvable_hostname', 'blocked_ip', 'too_many_redirects', @@ -47,6 +49,12 @@ export interface SafeFetchOptions { timeoutMs?: number; maxRedirects?: number; maxBytes?: number; + allowedHeaders?: string[]; + allowedHosts?: string[]; + allowHttp?: boolean; + allowPrivateTargetOrigin?: boolean; + enableEch?: boolean; + echConfigList?: Buffer; } export async function assertSsrFSafeUrl(rawUrl: string | URL): Promise { @@ -72,20 +80,25 @@ export async function safeFetch( ): Promise { const url = rawUrl.toString(); const method = String(init.method ?? 'GET').toUpperCase(); - if (method !== 'GET' && method !== 'HEAD') { + if (!['GET', 'HEAD', 'POST', 'PUT', 'PROPFIND', 'REPORT'].includes(method)) { throw new Error(`Unsupported safeFetch method: ${method}`); } try { const response = await safeFetchFromNative({ url, - method: (method === 'HEAD' ? 'head' : 'get') as NonNullable< - SafeFetchRequest['method'] - >, + method: method.toLowerCase() as NonNullable, headers: normalizeHeaders(init.headers), + body: normalizeBody(init.body), timeoutMs: options.timeoutMs, maxRedirects: options.maxRedirects, maxBytes: options.maxBytes, + allowedHeaders: options.allowedHeaders, + allowedHosts: options.allowedHosts, + allowHttp: options.allowHttp, + allowPrivateTargetOrigin: options.allowPrivateTargetOrigin, + enableEch: options.enableEch, + echConfigList: options.echConfigList, }); const body = method === 'HEAD' || [204, 205, 304].includes(response.status) @@ -117,6 +130,22 @@ function normalizeHeaders(headers: RequestInit['headers'] | undefined) { ); } +function normalizeBody(body: RequestInit['body'] | null | undefined) { + if (body === null || body === undefined) { + return undefined; + } + if (typeof body === 'string') { + return Buffer.from(body); + } + if (body instanceof ArrayBuffer) { + return Buffer.from(body); + } + if (ArrayBuffer.isView(body)) { + return Buffer.from(body.buffer, body.byteOffset, body.byteLength); + } + throw new Error('Unsupported safeFetch body type.'); +} + export function bufferToArrayBuffer(buffer: Buffer): ArrayBuffer { const copy = new Uint8Array(buffer.byteLength); copy.set(buffer); diff --git a/packages/backend/server/src/core/auth/resolver.ts b/packages/backend/server/src/core/auth/resolver.ts index 76eb6c6101..68b0e07d13 100644 --- a/packages/backend/server/src/core/auth/resolver.ts +++ b/packages/backend/server/src/core/auth/resolver.ts @@ -187,9 +187,7 @@ export class AuthResolver { @Mutation(() => Boolean) async sendChangeEmail( @CurrentUser() user: CurrentUser, - @Args('callbackUrl') callbackUrl: string, - // @deprecated - @Args('email', { nullable: true }) _email?: string + @Args('callbackUrl') callbackUrl: string ) { if (!user.emailVerified) { throw new EmailVerificationRequired(); diff --git a/packages/backend/server/src/core/comment/resolver.ts b/packages/backend/server/src/core/comment/resolver.ts index 1bb0f35313..7f33c0a8cd 100644 --- a/packages/backend/server/src/core/comment/resolver.ts +++ b/packages/backend/server/src/core/comment/resolver.ts @@ -299,18 +299,13 @@ export class CommentResolver { @CurrentUser() me: UserType, @Parent() workspace: WorkspaceType, @Args('docId') docId: string, - @Args({ - name: 'pagination', - }) + @Args({ name: 'pagination' }) pagination: PaginationInput ): Promise { // DEPRECATED-0.26-COMPAT(realtime): remove after server no longer supports 0.26.x clients. await this.assertPermission( me, - { - workspaceId: workspace.id, - docId, - }, + { workspaceId: workspace.id, docId }, 'Doc.Comments.Read' ); diff --git a/packages/backend/server/src/core/doc/reader.ts b/packages/backend/server/src/core/doc/reader.ts index d64912c137..768df9445b 100644 --- a/packages/backend/server/src/core/doc/reader.ts +++ b/packages/backend/server/src/core/doc/reader.ts @@ -7,6 +7,7 @@ import { Config, CryptoHelper, getOrGenRequestId, + safeFetch, UserFriendlyError, } from '../../base'; import { Models } from '../../models'; @@ -303,7 +304,17 @@ export class RpcDocReader extends DatabaseDocReader { if (body) { requestInit.body = body; } - const res = await fetch(url, requestInit); + const res = await safeFetch(url, requestInit, { + timeoutMs: 10_000, + maxRedirects: 0, + maxBytes: 50 * 1024 * 1024, + allowedHeaders: [ + 'content-type', + 'x-access-token', + 'x-cloud-trace-context', + ], + allowPrivateTargetOrigin: true, + }); if (!res.ok) { if (res.status === 404) { return null; diff --git a/packages/backend/server/src/core/entitlement/service.ts b/packages/backend/server/src/core/entitlement/service.ts index 383d142281..df2f342e25 100644 --- a/packages/backend/server/src/core/entitlement/service.ts +++ b/packages/backend/server/src/core/entitlement/service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { Entitlement, Prisma, PrismaClient } from '@prisma/client'; import { BadRequest, CryptoHelper, EventBus } from '../../base'; -import { resolveEntitlementV1 } from '../../native'; +import { checkLicenseHealth, resolveEntitlementV1 } from '../../native'; import { SubscriptionPlan, SubscriptionRecurring, @@ -47,15 +47,7 @@ export interface SelfhostLicenseEntitlementInput { license?: Buffer | null; } -interface RemoteSelfhostLicense { - plan: string; - recurring: string; - quantity: number; - endAt: number; -} - const REMOTE_SELFHOST_LICENSE_REVALIDATE_INTERVAL = 1000 * 60 * 10; -const REMOTE_SELFHOST_LICENSE_HEALTH_TIMEOUT = 10_000; declare global { interface Events { @@ -844,44 +836,25 @@ export class EntitlementService { return cached.entitlement; } - const endpoint = - process.env.AFFINE_PRO_SERVER_ENDPOINT ?? 'https://app.affine.pro'; - const signal = AbortSignal.timeout(REMOTE_SELFHOST_LICENSE_HEALTH_TIMEOUT); try { - const res = await fetch( - `${endpoint}/api/team/licenses/${entitlement.subjectId}/health`, - { - signal, - headers: { - 'Content-Type': 'application/json', - 'x-validate-key': metadata.validateKey, - }, - } - ); - if (!res.ok) { - if (res.status >= 500) { + const res = await checkLicenseHealth({ + licenseKey: entitlement.subjectId, + validateKey: metadata.validateKey, + }); + if (res.error) { + if (res.error.status >= 500) { return this.remoteSelfhostFallbackEntitlement(entitlement); } await this.markRemoteSelfhostLicenseNeedsReupload( entitlement, - `Remote license health check failed: ${res.status}` + `Remote license health check failed: ${res.error.status}` ); return null; } - const payload = (await res - .json() - .catch(() => null)) as RemoteSelfhostLicense | null; - if (!payload) { - return this.remoteSelfhostFallbackEntitlement(entitlement); - } - const expiresAt = this.remoteSelfhostLicenseExpiresAt(payload.endAt); - if ( - payload.plan !== SubscriptionPlan.SelfHostedTeam || - payload.quantity < 1 || - !expiresAt - ) { + const license = res.license; + if (!license || license.plan !== SubscriptionPlan.SelfHostedTeam) { await this.markRemoteSelfhostLicenseNeedsReupload( entitlement, 'Remote license health payload is invalid.' @@ -889,17 +862,17 @@ export class EntitlementService { return null; } - const validateKey = - res.headers.get('x-next-validate-key') ?? metadata.validateKey; + const expiresAt = new Date(license.expiresAt); + const validateKey = license.validateKey || metadata.validateKey; const [updated] = await Promise.all([ this.db.entitlement.update({ where: { id: entitlement.id }, data: { status: 'active', - quantity: this.normalizedQuantity(payload.quantity), + quantity: this.normalizedQuantity(license.quantity), metadata: { ...metadata, - recurring: payload.recurring, + recurring: license.recurring, validateKey, remoteValidated: true, errorCode: null, @@ -913,8 +886,8 @@ export class EntitlementService { .updateMany({ where: { key: entitlement.subjectId }, data: { - quantity: this.normalizedQuantity(payload.quantity), - recurring: payload.recurring, + quantity: this.normalizedQuantity(license.quantity), + recurring: license.recurring, validateKey, validatedAt: new Date(), expiredAt: expiresAt, @@ -950,14 +923,6 @@ export class EntitlementService { return cached.entitlement; } - private remoteSelfhostLicenseExpiresAt(endAt: unknown) { - const expiresAt = new Date(endAt as string | number | Date); - if (!Number.isFinite(expiresAt.getTime()) || expiresAt <= new Date()) { - return null; - } - return expiresAt; - } - private async markRemoteSelfhostLicenseNeedsReupload( entitlement: Entitlement, reason: string diff --git a/packages/backend/server/src/core/features/index.ts b/packages/backend/server/src/core/features/index.ts index cb9ff5da91..dac70533b9 100644 --- a/packages/backend/server/src/core/features/index.ts +++ b/packages/backend/server/src/core/features/index.ts @@ -5,7 +5,7 @@ import { AdminFeatureManagementResolver, UserFeatureResolver, } from './resolver'; -import { EarlyAccessType, FeatureService } from './service'; +import { FeatureService } from './service'; @Module({ imports: [EntitlementModule], @@ -18,5 +18,5 @@ import { EarlyAccessType, FeatureService } from './service'; }) export class FeatureModule {} -export { EarlyAccessType, FeatureService }; +export { FeatureService }; export { AvailableUserFeatureConfig } from './types'; diff --git a/packages/backend/server/src/core/features/service.ts b/packages/backend/server/src/core/features/service.ts index 97bebb0b3d..ad7e9634af 100644 --- a/packages/backend/server/src/core/features/service.ts +++ b/packages/backend/server/src/core/features/service.ts @@ -4,11 +4,6 @@ import { Models } from '../../models'; const STAFF = ['@toeverything.info', '@affine.pro']; -export enum EarlyAccessType { - App = 'app', - AI = 'ai', -} - @Injectable() export class FeatureService { protected logger = new Logger(FeatureService.name); @@ -32,15 +27,4 @@ export class FeatureService { addAdmin(userId: string) { return this.models.userFeature.add(userId, 'administrator', 'Admin user'); } - - // ======== Early Access ======== - async isEarlyAccessUser( - userId: string, - type: EarlyAccessType = EarlyAccessType.App - ) { - return await this.models.userFeature.has( - userId, - type === EarlyAccessType.App ? 'early_access' : 'ai_early_access' - ); - } } diff --git a/packages/backend/server/src/core/features/types.ts b/packages/backend/server/src/core/features/types.ts index 5b1cb0286c..b183a94fb7 100644 --- a/packages/backend/server/src/core/features/types.ts +++ b/packages/backend/server/src/core/features/types.ts @@ -5,14 +5,10 @@ import { Feature, UserFeatureName } from '../../models'; @Injectable() export class AvailableUserFeatureConfig { availableUserFeatures(): Set { - return new Set([Feature.Admin, Feature.EarlyAccess, Feature.AIEarlyAccess]); + return new Set([Feature.Admin]); } configurableUserFeatures(): Set { - return new Set( - env.selfhosted - ? [Feature.Admin] - : [Feature.EarlyAccess, Feature.AIEarlyAccess, Feature.Admin] - ); + return new Set([Feature.Admin]); } } diff --git a/packages/backend/server/src/core/notification/resolver.ts b/packages/backend/server/src/core/notification/resolver.ts index 36114c17a9..fb1ae7503f 100644 --- a/packages/backend/server/src/core/notification/resolver.ts +++ b/packages/backend/server/src/core/notification/resolver.ts @@ -1,11 +1,4 @@ -import { - Args, - ID, - Int, - Mutation, - ResolveField, - Resolver, -} from '@nestjs/graphql'; +import { Args, ID, Mutation, ResolveField, Resolver } from '@nestjs/graphql'; import { MentionUserDocAccessDenied, @@ -45,16 +38,6 @@ export class UserNotificationResolver { return paginate(notifications, 'createdAt', pagination, totalCount); } - @ResolveField(() => Int, { - description: 'Get user notification count', - deprecationReason: - 'Use realtime subscription "notification.count.changed" instead.', - }) - async notificationCount(@CurrentUser() me: UserType): Promise { - // DEPRECATED-0.26-COMPAT(realtime): remove after server no longer supports 0.26.x clients. - return await this.service.countByUserId(me.id); - } - @Mutation(() => ID, { description: 'mention user in a doc', }) diff --git a/packages/backend/server/src/core/quota/__tests__/state.spec.ts b/packages/backend/server/src/core/quota/__tests__/state.spec.ts index 476bc1539f..425e87e219 100644 --- a/packages/backend/server/src/core/quota/__tests__/state.spec.ts +++ b/packages/backend/server/src/core/quota/__tests__/state.spec.ts @@ -155,7 +155,6 @@ test('quota service exposes history period in seconds', async t => { usedStorageQuota: 0, memberCount: 1, overcapacityMemberCount: 0, - usedSize: 0, }).historyPeriod, '30 days' ); diff --git a/packages/backend/server/src/core/quota/service.ts b/packages/backend/server/src/core/quota/service.ts index 974dae1dcb..27e6cfca76 100644 --- a/packages/backend/server/src/core/quota/service.ts +++ b/packages/backend/server/src/core/quota/service.ts @@ -76,7 +76,6 @@ export class QuotaService { usedStorageQuota: Number(state.usedStorageQuota), memberCount: state.memberCount, overcapacityMemberCount: state.overcapacityMemberCount, - usedSize: Number(state.usedStorageQuota), }; } diff --git a/packages/backend/server/src/core/quota/types.ts b/packages/backend/server/src/core/quota/types.ts index 31da198edf..d4b7c91f50 100644 --- a/packages/backend/server/src/core/quota/types.ts +++ b/packages/backend/server/src/core/quota/types.ts @@ -118,12 +118,4 @@ export class WorkspaceQuotaType implements Partial { @Field() humanReadable!: WorkspaceQuotaHumanReadableType; - - /** - * @deprecated - */ - @Field(() => SafeIntResolver, { - deprecationReason: 'use `usedStorageQuota` instead', - }) - usedSize!: number; } diff --git a/packages/backend/server/src/core/workspaces/types.ts b/packages/backend/server/src/core/workspaces/types.ts index 0145a4249a..6719e53302 100644 --- a/packages/backend/server/src/core/workspaces/types.ts +++ b/packages/backend/server/src/core/workspaces/types.ts @@ -166,16 +166,6 @@ export class InviteResult { }) inviteId?: string; - /** - * @deprecated - */ - @Field(() => Boolean, { - description: 'Invite email sent success', - deprecationReason: 'Notification will be sent asynchronously', - defaultValue: true, - }) - sentSuccess?: boolean; - @Field(() => GraphQLJSONObject, { nullable: true, description: 'Invite error', diff --git a/packages/backend/server/src/models/common/feature.ts b/packages/backend/server/src/models/common/feature.ts index ca22e21637..2a9cb61d17 100644 --- a/packages/backend/server/src/models/common/feature.ts +++ b/packages/backend/server/src/models/common/feature.ts @@ -43,8 +43,6 @@ export enum FeatureType { export enum Feature { // user Admin = 'administrator', - EarlyAccess = 'early_access', - AIEarlyAccess = 'ai_early_access', UnlimitedCopilot = 'unlimited_copilot', FreePlan = 'free_plan_v1', ProPlan = 'pro_plan_v1', @@ -58,10 +56,8 @@ export enum Feature { // TODO(@forehalo): may merge `FeatureShapes` and `FeatureConfigs`? export const FeaturesShapes = { - early_access: z.object({ whitelist: z.array(z.string()).readonly() }), unlimited_workspace: EMPTY_CONFIG, unlimited_copilot: EMPTY_CONFIG, - ai_early_access: EMPTY_CONFIG, administrator: EMPTY_CONFIG, free_plan_v1: UserPlanQuotaConfig, pro_plan_v1: UserPlanQuotaConfig, @@ -72,8 +68,6 @@ export const FeaturesShapes = { export type UserFeatureName = keyof Pick< typeof FeaturesShapes, - | 'early_access' - | 'ai_early_access' | 'unlimited_copilot' | 'administrator' | 'free_plan_v1' @@ -142,11 +136,6 @@ const TeamFeature = { }, } as const; -const WhitelistFeature = { - type: FeatureType.Feature, - configs: { whitelist: [] }, -} as const; - const EmptyFeature = { type: FeatureType.Feature, configs: {}, @@ -164,10 +153,8 @@ export const FeatureConfigs: { pro_plan_v1: ProFeature, lifetime_pro_plan_v1: LifetimeProFeature, team_plan_v1: TeamFeature, - early_access: WhitelistFeature, unlimited_workspace: EmptyFeature, quota_exceeded_readonly_workspace_v1: EmptyFeature, unlimited_copilot: EmptyFeature, - ai_early_access: EmptyFeature, administrator: EmptyFeature, }; diff --git a/packages/backend/server/src/native.ts b/packages/backend/server/src/native.ts index 5045c8413d..9a012f62d5 100644 --- a/packages/backend/server/src/native.ts +++ b/packages/backend/server/src/native.ts @@ -9,8 +9,16 @@ import serverNativeModule, { type CanonicalStructuredRequestContract, type CapabilityAttachmentContract, type CapabilityModelCapability, + type CommandResponse, type ImageInspection, type ImageInspectionOptions, + type LicenseError, + type LicenseHealthRequest, + type LicenseInfo, + type LicenseKeyRequest, + type LicenseRecurringRequest, + type LicenseResponse, + type LicenseSeatsRequest, type LlmCoreMessage, type LlmEmbeddingRequestContract, type LlmImageRequestContract, @@ -20,6 +28,7 @@ import serverNativeModule, { type ModelConditionsContract, type ModelRegistryMatchResponse, type ModelRegistryResolveResponse, + type PortalResponse, type PromptMessageContract, type PromptMetadataContract, type PromptMetadataResult, @@ -45,9 +54,18 @@ export type { AssertSafeUrlRequest, CapabilityAttachmentContract, CapabilityModelCapability, + CommandResponse, ImageInspection, ImageInspectionOptions, + LicenseError, + LicenseHealthRequest, + LicenseInfo, + LicenseKeyRequest, + LicenseRecurringRequest, + LicenseResponse, + LicenseSeatsRequest, ModelConditionsContract, + PortalResponse, PromptMessageContract, PromptStructuredResponseContract, RemoteAttachmentFetchRequest, @@ -143,6 +161,13 @@ export const fetchRemoteAttachment = serverNativeModule.fetchRemoteAttachment; export const inferRemoteMimeType = serverNativeModule.inferRemoteMimeType; export const assertSafeUrl = serverNativeModule.assertSafeUrl; export const safeFetch = serverNativeModule.safeFetch; +export const activateLicense = serverNativeModule.activateLicense; +export const checkLicenseHealth = serverNativeModule.checkLicenseHealth; +export const createCustomerPortal = + serverNativeModule.createLicenseCustomerPortal; +export const deactivateLicense = serverNativeModule.deactivateLicense; +export const updateLicenseRecurring = serverNativeModule.updateLicenseRecurring; +export const updateLicenseSeats = serverNativeModule.updateLicenseSeats; export const parseDoc = serverNativeModule.parseDoc; export const htmlSanitize = serverNativeModule.htmlSanitize; export const processImage = serverNativeModule.processImage; diff --git a/packages/backend/server/src/plugins/calendar/providers/caldav.ts b/packages/backend/server/src/plugins/calendar/providers/caldav.ts index 1a31a1b447..5d90c5bf01 100644 --- a/packages/backend/server/src/plugins/calendar/providers/caldav.ts +++ b/packages/backend/server/src/plugins/calendar/providers/caldav.ts @@ -5,9 +5,9 @@ import { XMLParser } from 'fast-xml-parser'; import { escape } from 'lodash-es'; import { - assertSsrFSafeUrl, CalendarProviderRequestError, GraphqlBadRequest, + safeFetch, SsrfBlockedError, } from '../../../base'; import type { @@ -35,6 +35,8 @@ const XML_PARSER = new XMLParser({ const DEFAULT_REQUEST_TIMEOUT_MS = 10_000; const DEFAULT_MAX_REDIRECTS = 5; +const DEFAULT_MAX_RESPONSE_BYTES = 50 * 1024 * 1024; +const CALDAV_SAFE_FETCH_HEADERS = ['authorization', 'content-type', 'depth']; type CalDAVCredentials = { username: string; @@ -106,9 +108,6 @@ const formatUtcForIcal = (iso: string) => { ].join(''); }; -const isRedirectStatus = (status: number) => - [301, 302, 303, 307, 308].includes(status); - const splitHeaderTokens = (value: string) => value .split(/,(?=(?:[^"]*"[^"]*")*[^"]*$)/) @@ -536,36 +535,24 @@ class CalDAVRequestPolicy { return this.config.calendar.caldav.maxRedirects ?? DEFAULT_MAX_REDIRECTS; } - async fetch( - url: string, - init: RequestInit, - redirects = 0 - ): Promise { + async fetch(url: string, init: RequestInit): Promise { await this.assertAllowedUrl(url); - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), this.timeoutMs); - const response = await fetch(url, { - ...init, - redirect: 'manual', - signal: controller.signal, - }).finally(() => clearTimeout(timer)); - - if (isRedirectStatus(response.status)) { - const location = response.headers.get('location'); - if (location) { - if (redirects >= this.maxRedirects) { - throw new GraphqlBadRequest({ - code: 'caldav_max_redirects', - message: 'CalDAV request exceeded redirect limit.', - }); - } - const nextUrl = resolveHref(location, url); - return this.fetch(nextUrl, init, redirects + 1); - } + try { + return await safeFetch(url, init, { + timeoutMs: this.timeoutMs, + maxRedirects: this.maxRedirects, + maxBytes: DEFAULT_MAX_RESPONSE_BYTES, + allowedHeaders: CALDAV_SAFE_FETCH_HEADERS, + allowedHosts: this.allowedHosts, + allowHttp: this.allowInsecureHttp, + allowPrivateTargetOrigin: !this.blockPrivateNetwork, + }); + } catch (error) { + const ssrfError = this.toGraphqlSsrfError(error); + if (ssrfError) throw ssrfError; + throw error; } - - return response; } private async assertAllowedUrl(urlValue: string) { @@ -599,52 +586,62 @@ class CalDAVRequestPolicy { message: 'CalDAV host is not allowed.', }); } + } - if (!this.blockPrivateNetwork) { - return; + private toGraphqlSsrfError(error: unknown) { + if (!(error instanceof SsrfBlockedError)) { + return null; } - try { - await assertSsrFSafeUrl(url); - } catch (error) { - if (error instanceof SsrfBlockedError) { - const reason = String(error.data?.reason ?? ''); + const reason = String(error.data?.reason ?? ''); - if (reason === 'blocked_ip') { - throw new GraphqlBadRequest({ - code: 'caldav_private_network', - message: 'CalDAV host is in a private network.', - }); - } - - if (reason === 'unresolvable_hostname') { - throw new GraphqlBadRequest({ - code: 'caldav_dns_failed', - message: 'Unable to resolve CalDAV host.', - }); - } - - if (reason === 'disallowed_protocol') { - throw new GraphqlBadRequest({ - code: 'caldav_insecure_url', - message: 'CalDAV URL must use https.', - }); - } - - if ( - reason === 'invalid_url' || - reason === 'blocked_hostname' || - reason === 'url_has_credentials' - ) { - throw new GraphqlBadRequest({ - code: 'caldav_invalid_url', - message: 'CalDAV URL is invalid.', - }); - } - } - - throw error; + if (reason === 'blocked_ip') { + return new GraphqlBadRequest({ + code: 'caldav_private_network', + message: 'CalDAV host is in a private network.', + }); } + + if (reason === 'unresolvable_hostname') { + return new GraphqlBadRequest({ + code: 'caldav_dns_failed', + message: 'Unable to resolve CalDAV host.', + }); + } + + if (reason === 'disallowed_protocol') { + return new GraphqlBadRequest({ + code: 'caldav_insecure_url', + message: 'CalDAV URL must use https.', + }); + } + + if (reason === 'too_many_redirects') { + return new GraphqlBadRequest({ + code: 'caldav_max_redirects', + message: 'CalDAV request exceeded redirect limit.', + }); + } + + if (reason === 'host_not_allowed') { + return new GraphqlBadRequest({ + code: 'caldav_host_blocked', + message: 'CalDAV host is not allowed.', + }); + } + + if ( + reason === 'invalid_url' || + reason === 'blocked_hostname' || + reason === 'url_has_credentials' + ) { + return new GraphqlBadRequest({ + code: 'caldav_invalid_url', + message: 'CalDAV URL is invalid.', + }); + } + + return null; } } diff --git a/packages/backend/server/src/plugins/copilot/byok/service.ts b/packages/backend/server/src/plugins/copilot/byok/service.ts index 3ca900ac6a..a694bd1ddc 100644 --- a/packages/backend/server/src/plugins/copilot/byok/service.ts +++ b/packages/backend/server/src/plugins/copilot/byok/service.ts @@ -2,7 +2,13 @@ import { createHash, createHmac, randomUUID } from 'node:crypto'; import { BadRequestException, Injectable } from '@nestjs/common'; -import { BadRequest, Cache, CryptoHelper, metrics } from '../../../base'; +import { + BadRequest, + Cache, + CryptoHelper, + metrics, + safeFetch, +} from '../../../base'; import { Models } from '../../../models'; import type { CopilotProviderProfile } from '../config'; import { ByokEntitlementPolicy } from './policy'; @@ -103,6 +109,8 @@ type ByokProfileMeta = { @Injectable() export class ByokService { + private readonly probeFetch = safeFetch; + constructor( private readonly models: Models, private readonly crypto: CryptoHelper, @@ -755,22 +763,24 @@ export class ByokService { apiKey: string, endpoint: string | null ) { - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), TEST_TIMEOUT_MS); - try { - const request = this.buildProbeRequest(provider, apiKey, endpoint); - const response = await fetch(request.url, { + const request = this.buildProbeRequest(provider, apiKey, endpoint); + const response = await this.probeFetch( + request.url, + { method: request.method, headers: request.headers as unknown as Record, - signal: controller.signal, - }); - if (!response.ok) { - throw new BadRequestException( - this.providerProbeFailureMessage(response.status) - ); + }, + { + timeoutMs: TEST_TIMEOUT_MS, + maxRedirects: 3, + maxBytes: 1024, + allowedHeaders: Object.keys(request.headers), } - } finally { - clearTimeout(timeout); + ); + if (!response.ok) { + throw new BadRequestException( + this.providerProbeFailureMessage(response.status) + ); } } diff --git a/packages/backend/server/src/plugins/copilot/context/realtime.ts b/packages/backend/server/src/plugins/copilot/context/realtime.ts index 84a7529208..001efa03c0 100644 --- a/packages/backend/server/src/plugins/copilot/context/realtime.ts +++ b/packages/backend/server/src/plugins/copilot/context/realtime.ts @@ -66,12 +66,12 @@ export class CopilotEmbeddingRealtimeProvider implements OnModuleInit { @OnEvent('workspace.file.embed.finished', { suppressError: true }) async onFileEmbedFinished(payload: Events['workspace.file.embed.finished']) { - await this.publishContext(payload.contextId, 'finished'); + await this.publishEmbeddingProgress(payload, 'finished'); } @OnEvent('workspace.file.embed.failed', { suppressError: true }) async onFileEmbedFailed(payload: Events['workspace.file.embed.failed']) { - await this.publishContext(payload.contextId, 'failed'); + await this.publishEmbeddingProgress(payload, 'failed'); } @OnEvent('workspace.blob.embed.finished', { suppressError: true }) @@ -90,11 +90,29 @@ export class CopilotEmbeddingRealtimeProvider implements OnModuleInit { ) { if (!this.publisher) return; const context = await this.context.get(contextId); + this.publishWorkspace(context.workspaceId, reason); + } + + private async publishEmbeddingProgress( + payload: + | Events['workspace.file.embed.finished'] + | Events['workspace.file.embed.failed'], + reason: 'finished' | 'failed' + ) { + if (!this.publisher) return; + if (payload.contextId) { + await this.publishContext(payload.contextId, reason); + return; + } + this.publishWorkspace(payload.workspaceId, reason); + } + + private publishWorkspace(workspaceId: string, reason: 'finished' | 'failed') { this.publisher.publish( 'workspace.embedding.progress.changed', - { workspaceId: context.workspaceId }, + { workspaceId }, { reason }, - { room: workspaceEmbeddingRoom(context.workspaceId) } + { room: workspaceEmbeddingRoom(workspaceId) } ); } diff --git a/packages/backend/server/src/plugins/copilot/context/service.ts b/packages/backend/server/src/plugins/copilot/context/service.ts index 237948105e..ee9d03af52 100644 --- a/packages/backend/server/src/plugins/copilot/context/service.ts +++ b/packages/backend/server/src/plugins/copilot/context/service.ts @@ -354,6 +354,7 @@ export class CopilotContextService implements OnApplicationBootstrap { fileId, chunkSize, }: Events['workspace.file.embed.finished']) { + if (!contextId) return; const context = await this.get(contextId); await context.saveFileRecord(fileId, file => ({ ...(file as ContextFile), @@ -368,6 +369,7 @@ export class CopilotContextService implements OnApplicationBootstrap { fileId, error, }: Events['workspace.file.embed.failed']) { + if (!contextId) return; const context = await this.get(contextId); await context.saveFileRecord(fileId, file => ({ ...(file as ContextFile), diff --git a/packages/backend/server/src/plugins/copilot/embedding/job.ts b/packages/backend/server/src/plugins/copilot/embedding/job.ts index 4c9e2c74a3..ee6635e07e 100644 --- a/packages/backend/server/src/plugins/copilot/embedding/job.ts +++ b/packages/backend/server/src/plugins/copilot/embedding/job.ts @@ -300,21 +300,19 @@ export class CopilotEmbeddingJob { } } - if (contextId) { - this.event.emit('workspace.file.embed.finished', { - contextId, - fileId, - chunkSize: total, - }); - } + this.event.emit('workspace.file.embed.finished', { + contextId, + workspaceId, + fileId, + chunkSize: total, + }); } catch (error: any) { - if (contextId) { - this.event.emit('workspace.file.embed.failed', { - contextId, - fileId, - error: mapAnyError(error).message, - }); - } + this.event.emit('workspace.file.embed.failed', { + contextId, + workspaceId, + fileId, + error: mapAnyError(error).message, + }); // passthrough error to job queue throw error; diff --git a/packages/backend/server/src/plugins/copilot/embedding/types.ts b/packages/backend/server/src/plugins/copilot/embedding/types.ts index 3b5e581e2c..73b8cffd4e 100644 --- a/packages/backend/server/src/plugins/copilot/embedding/types.ts +++ b/packages/backend/server/src/plugins/copilot/embedding/types.ts @@ -43,13 +43,15 @@ declare global { }; 'workspace.file.embed.finished': { - contextId: string; + contextId?: string; + workspaceId: string; fileId: string; chunkSize: number; }; 'workspace.file.embed.failed': { - contextId: string; + contextId?: string; + workspaceId: string; fileId: string; error: string; }; diff --git a/packages/backend/server/src/plugins/customerio/config.ts b/packages/backend/server/src/plugins/customerio/config.ts deleted file mode 100644 index c243f2a05c..0000000000 --- a/packages/backend/server/src/plugins/customerio/config.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { defineModuleConfig } from '../../base'; - -declare global { - interface AppConfigSchema { - customerIo: { - enabled: boolean; - token: string; - }; - } -} - -defineModuleConfig('customerIo', { - enabled: { - desc: 'Enable customer.io integration', - default: false, - }, - token: { - desc: 'Customer.io token', - default: '', - schema: { type: 'string' }, - }, -}); diff --git a/packages/backend/server/src/plugins/customerio/index.ts b/packages/backend/server/src/plugins/customerio/index.ts deleted file mode 100644 index 3a7fd4f507..0000000000 --- a/packages/backend/server/src/plugins/customerio/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import './config'; - -import { Module } from '@nestjs/common'; - -import { CustomerIoService } from './service'; - -@Module({ - providers: [CustomerIoService], -}) -export class CustomerIoModule {} diff --git a/packages/backend/server/src/plugins/customerio/service.ts b/packages/backend/server/src/plugins/customerio/service.ts deleted file mode 100644 index 4b1a1d0245..0000000000 --- a/packages/backend/server/src/plugins/customerio/service.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { Config, OnEvent } from '../../base'; - -@Injectable() -export class CustomerIoService { - #fetch: ((url: string, options?: RequestInit) => Promise) | null = - null; - constructor(private readonly config: Config) {} - - @OnEvent('config.init') - setup() { - const { enabled, token } = this.config.customerIo; - if (enabled && token) { - this.#fetch = (url, options) => { - return fetch(url, { - ...options, - headers: { - Authorization: `Basic ${token}`, - 'Content-Type': 'application/json', - ...options?.headers, - }, - }); - }; - } else { - this.#fetch = null; - } - } - - @OnEvent('config.changed') - onConfigChanged(event: Events['config.changed']) { - if (event.updates.customerIo) { - this.setup(); - } - } - - @OnEvent('user.created') - @OnEvent('user.updated') - async onUserUpdated(user: Events['user.updated'] | Events['user.created']) { - await this.#fetch?.( - `https://track.customer.io/api/v1/customers/${user.id}`, - { - method: 'PUT', - body: JSON.stringify({ - name: user.name, - email: user.email, - created_at: Number(user.createdAt) / 1000, - }), - } - ); - } - - @OnEvent('user.deleted') - async onUserDeleted(user: Events['user.deleted']) { - if (user.emailVerifiedAt) { - // suppress email if email is verified - await this.#fetch?.( - `https://track.customer.io/api/v1/customers/${user.email}/suppress`, - { - method: 'POST', - } - ); - } - - await this.#fetch?.( - `https://track.customer.io/api/v1/customers/${user.id}`, - { - method: 'DELETE', - } - ); - } -} diff --git a/packages/backend/server/src/plugins/indexer/__tests__/providers/manticoresearch.spec.ts b/packages/backend/server/src/plugins/indexer/__tests__/providers/manticoresearch.spec.ts index 395082edb9..83b0a0d421 100644 --- a/packages/backend/server/src/plugins/indexer/__tests__/providers/manticoresearch.spec.ts +++ b/packages/backend/server/src/plugins/indexer/__tests__/providers/manticoresearch.spec.ts @@ -1803,6 +1803,41 @@ test('should parse es query with custom term mapping field work', async t => { t.snapshot(result3); }); +test('should parse es query with parent and nested must_not work', async t => { + const nestedMust = { + must: [ + { term: { workspace_id: 'workspaceId1' } }, + { bool: { must_not: { term: { doc_id: 'docId1' } } } }, + ], + }; + const parentMustNot = { term: { doc_id: 'docId2' } }; + const expectedMust = [{ equals: { workspace_id: 'workspaceId1' } }]; + const expectedMustNot = ['docId1', 'docId2']; + + const queryWithParentMustNotFirst = { + bool: { must_not: parentMustNot, must: nestedMust.must }, + }; + const queryWithParentMustNotLast = { + bool: { must: nestedMust.must, must_not: parentMustNot }, + }; + + // @ts-expect-error use private method + const result = searchProvider.parseESQuery(queryWithParentMustNotFirst); + // @ts-expect-error use private method + const result2 = searchProvider.parseESQuery(queryWithParentMustNotLast); + + t.deepEqual(result.bool.must, expectedMust); + t.deepEqual(result2.bool.must, expectedMust); + t.deepEqual( + result.bool.must_not.map((clause: any) => clause.equals.doc_id).sort(), + expectedMustNot + ); + t.deepEqual( + result2.bool.must_not.map((clause: any) => clause.equals.doc_id).sort(), + expectedMustNot + ); +}); + test('should parse es query exists work', async t => { const query = { exists: { diff --git a/packages/backend/server/src/plugins/indexer/providers/elasticsearch.ts b/packages/backend/server/src/plugins/indexer/providers/elasticsearch.ts index 3c7d920ebb..18970b7d22 100644 --- a/packages/backend/server/src/plugins/indexer/providers/elasticsearch.ts +++ b/packages/backend/server/src/plugins/indexer/providers/elasticsearch.ts @@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common'; import { InternalServerError, InvalidSearchProviderRequest, + safeFetch, } from '../../../base'; import { SearchProviderType } from '../config'; import { DateFieldNames, SearchTable, SearchTableUniqueId } from '../tables'; @@ -61,6 +62,14 @@ interface ESAggregateResponse extends ESSearchResponse { }; } +const INDEXER_FETCH_OPTIONS = { + timeoutMs: 30_000, + maxRedirects: 0, + maxBytes: 50 * 1024 * 1024, + allowedHeaders: ['authorization', 'content-type'], + allowPrivateTargetOrigin: true, +}; + @Injectable() export class ElasticsearchProvider extends SearchProvider { type = SearchProviderType.Elasticsearch; @@ -278,11 +287,11 @@ export class ElasticsearchProvider extends SearchProvider { } else if (this.config.provider.password) { headers.Authorization = `Basic ${Buffer.from(`${this.config.provider.username}:${this.config.provider.password}`).toString('base64')}`; } - const response = await fetch(url, { - method, - body, - headers, - }); + const response = await safeFetch( + url, + { method, body, headers }, + INDEXER_FETCH_OPTIONS + ); const data = await response.json(); if (ignoreErrorStatus?.includes(response.status)) { return data; diff --git a/packages/backend/server/src/plugins/indexer/providers/manticoresearch.ts b/packages/backend/server/src/plugins/indexer/providers/manticoresearch.ts index d928b76623..ad999c2ff2 100644 --- a/packages/backend/server/src/plugins/indexer/providers/manticoresearch.ts +++ b/packages/backend/server/src/plugins/indexer/providers/manticoresearch.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { omit } from 'lodash-es'; -import { InternalServerError } from '../../../base'; +import { InternalServerError, safeFetch } from '../../../base'; import { SearchProviderType } from '../config'; import { SearchTable } from '../tables'; import { @@ -32,6 +32,14 @@ interface MSSearchResponse { scroll: string; } +const INDEXER_FETCH_OPTIONS = { + timeoutMs: 30_000, + maxRedirects: 0, + maxBytes: 50 * 1024 * 1024, + allowedHeaders: ['authorization', 'content-type'], + allowPrivateTargetOrigin: true, +}; + const SupportIndexedAttributes = [ 'flavour', 'parent_flavour', @@ -58,6 +66,11 @@ const ConvertEmptyStringToNullValueFields = new Set([ 'parent_flavour', ]); +function boolClauses(value: unknown) { + if (value === undefined) return []; + return Array.isArray(value) ? value : [value]; +} + @Injectable() export class ManticoresearchProvider extends ElasticsearchProvider { override type = SearchProviderType.Manticoresearch; @@ -282,7 +295,9 @@ export class ManticoresearchProvider extends ElasticsearchProvider { for (const occur in query.bool) { const conditions = query.bool[occur]; if (Array.isArray(conditions)) { - node.bool[occur] = []; + const parsedConditions: Record[] = []; + const existing = node.bool[occur]; + node.bool[occur] = parsedConditions; // { must: [ { term: [Object] }, { bool: [Object] } ] } // { // must: [ { term: [Object] }, { term: [Object] }, { bool: [Object] } ] @@ -290,16 +305,41 @@ export class ManticoresearchProvider extends ElasticsearchProvider { for (const item of conditions) { this.parseESQuery(item, { ...options, - parentNodes: node.bool[occur], + parentNodes: parsedConditions, }); } + if (existing !== undefined) { + node.bool[occur] = [...boolClauses(existing), ...parsedConditions]; + } + if (occur === 'must') { + for (let i = node.bool.must.length - 1; i >= 0; i--) { + const child = node.bool.must[i]; + const childBool = child.bool; + if ( + childBool && + Object.keys(childBool).length === 1 && + childBool.must_not !== undefined + ) { + node.bool.must.splice(i, 1); + node.bool.must_not = [ + ...boolClauses(node.bool.must_not), + ...boolClauses(childBool.must_not), + ]; + } + } + } } else { // { // must_not: { term: { doc_id: 'docId' } } // } - node.bool[occur] = this.parseESQuery(conditions, { + const parsed = this.parseESQuery(conditions, { termMappingField: options?.termMappingField, }); + if (node.bool[occur] !== undefined) { + node.bool[occur] = [...boolClauses(node.bool[occur]), parsed]; + } else { + node.bool[occur] = parsed; + } } } } else if (query.term) { @@ -460,11 +500,11 @@ export class ManticoresearchProvider extends ElasticsearchProvider { headers.Authorization = `Basic ${Buffer.from(`${this.config.provider.username}:${this.config.provider.password}`).toString('base64')}`; } - const response = await fetch(url, { - method: 'POST', - body: sql, - headers, - }); + const response = await safeFetch( + url, + { method: 'POST', body: sql, headers }, + INDEXER_FETCH_OPTIONS + ); const text = (await response.text()).trim(); if (!response.ok) { this.logger.error(`failed to execute SQL "${sql}", response: ${text}`); diff --git a/packages/backend/server/src/plugins/license/service.ts b/packages/backend/server/src/plugins/license/service.ts index 3a95214430..5d92438b4f 100644 --- a/packages/backend/server/src/plugins/license/service.ts +++ b/packages/backend/server/src/plugins/license/service.ts @@ -17,21 +17,34 @@ import { EntitlementService } from '../../core/entitlement'; import { WorkspacePolicyService } from '../../core/permission'; import { QuotaStateService } from '../../core/quota/state'; import { Models } from '../../models'; -import { ResolvedEntitlement, resolveEntitlementV1 } from '../../native'; +import { + activateLicense, + checkLicenseHealth, + type CommandResponse, + createCustomerPortal, + deactivateLicense, + type LicenseError, + type LicenseResponse, + type PortalResponse, + type ResolvedEntitlement, + resolveEntitlementV1, + updateLicenseRecurring, + updateLicenseSeats, +} from '../../native'; import { SubscriptionPlan, SubscriptionRecurring, SubscriptionVariant, } from '../payment/types'; -interface License { - plan: SubscriptionPlan; - recurring: SubscriptionRecurring; - quantity: number; - endAt: number; -} - -const AFFINE_PRO_REQUEST_TIMEOUT = 10_000; +export const licenseClient = { + activate: activateLicense, + checkHealth: checkLicenseHealth, + createCustomerPortal, + deactivate: deactivateLicense, + updateRecurring: updateLicenseRecurring, + updateSeats: updateLicenseSeats, +}; export interface LicensePreview { id: string; @@ -157,31 +170,27 @@ export class LicenseService { throw new WorkspaceLicenseAlreadyExists(); } - const data = await this.fetchAffinePro( - `/api/team/licenses/${licenseKey}/activate`, - { - method: 'POST', - } + const data = this.remoteLicense( + await licenseClient.activate({ licenseKey }) ); const validatedAt = new Date(); - const expiresAt = this.remoteLicenseExpiresAt(data); - const validateKey = data.res.headers.get('x-next-validate-key') ?? ''; + const expiresAt = new Date(data.expiresAt); this.event.emit('workspace.subscription.activated', { workspaceId, - plan: data.plan, - recurring: data.recurring, + plan: data.plan as SubscriptionPlan, + recurring: data.recurring as SubscriptionRecurring, quantity: data.quantity, }); await this.entitlement.upsertFromValidatedSelfhostLicense({ workspaceId, licenseKey, - recurring: data.recurring, + recurring: data.recurring as SubscriptionRecurring, quantity: data.quantity, expiresAt, validatedAt, - validateKey, + validateKey: data.validateKey, }); return this.db.installedLicense.upsert({ @@ -189,9 +198,9 @@ export class LicenseService { update: { key: licenseKey, quantity: data.quantity, - recurring: data.recurring, + recurring: data.recurring as SubscriptionRecurring, variant: null, - validateKey, + validateKey: data.validateKey, validatedAt, expiredAt: expiresAt, license: null, @@ -200,9 +209,9 @@ export class LicenseService { workspaceId, key: licenseKey, quantity: data.quantity, - recurring: data.recurring, + recurring: data.recurring as SubscriptionRecurring, variant: null, - validateKey, + validateKey: data.validateKey, validatedAt, expiredAt: expiresAt, license: null, @@ -242,18 +251,18 @@ export class LicenseService { } async deactivateTeamLicense(license: InstalledLicense) { - await this.fetchAffinePro(`/api/team/licenses/${license.key}/deactivate`, { - method: 'POST', - }); + this.remoteCommand( + await licenseClient.deactivate({ licenseKey: license.key }) + ); } async updateTeamRecurring(key: string, recurring: SubscriptionRecurring) { - await this.fetchAffinePro(`/api/team/licenses/${key}/recurring`, { - method: 'POST', - body: JSON.stringify({ + this.remoteCommand( + await licenseClient.updateRecurring({ + licenseKey: key, recurring, - }), - }); + }) + ); } async createCustomerPortal(workspaceId: string) { @@ -267,12 +276,12 @@ export class LicenseService { throw new LicenseNotFound(); } - return this.fetchAffinePro<{ url: string }>( - `/api/team/licenses/${license.key}/create-customer-portal`, - { - method: 'POST', - } + const portal = this.remotePortal( + await licenseClient.createCustomerPortal({ + licenseKey: license.key, + }) ); + return { url: portal.url }; } @OnEvent('workspace.members.updated') @@ -301,12 +310,12 @@ export class LicenseService { } const count = await this.models.workspaceUser.chargedCount(workspaceId); - await this.fetchAffinePro(`/api/team/licenses/${license.key}/seats`, { - method: 'POST', - body: JSON.stringify({ + this.remoteCommand( + await licenseClient.updateSeats({ + licenseKey: license.key, seats: count, - }), - }); + }) + ); // stripe payment is async, we can't directly the charge result in update calling await this.waitUntilLicenseUpdated(license, count); @@ -356,30 +365,28 @@ export class LicenseService { private async revalidateRecurringLicense(license: InstalledLicense) { try { - const res = await this.fetchAffinePro( - `/api/team/licenses/${license.key}/health`, - { - headers: { - 'x-validate-key': license.validateKey, - }, - } + const res = this.remoteLicense( + await licenseClient.checkHealth({ + licenseKey: license.key, + validateKey: license.validateKey, + }) ); const validatedAt = new Date(); - const expiresAt = this.remoteLicenseExpiresAt(res); - const validateKey = res.res.headers.get('x-next-validate-key') ?? ''; + const expiresAt = new Date(res.expiresAt); + const validateKey = res.validateKey || license.validateKey; this.event.emit('workspace.subscription.activated', { workspaceId: license.workspaceId, - plan: res.plan, - recurring: res.recurring, + plan: res.plan as SubscriptionPlan, + recurring: res.recurring as SubscriptionRecurring, quantity: res.quantity, }); await this.db.installedLicense.update({ where: { key: license.key }, data: { quantity: res.quantity, - recurring: res.recurring, + recurring: res.recurring as SubscriptionRecurring, validateKey, validatedAt, expiredAt: expiresAt, @@ -390,7 +397,7 @@ export class LicenseService { await this.entitlement.upsertFromSelfhostLicense({ workspaceId: license.workspaceId, licenseKey: license.key, - recurring: res.recurring, + recurring: res.recurring as SubscriptionRecurring, quantity: res.quantity, expiresAt, validatedAt, @@ -401,7 +408,7 @@ export class LicenseService { await this.entitlement.upsertFromValidatedSelfhostLicense({ workspaceId: license.workspaceId, licenseKey: license.key, - recurring: res.recurring, + recurring: res.recurring as SubscriptionRecurring, quantity: res.quantity, expiresAt, validatedAt, @@ -430,55 +437,38 @@ export class LicenseService { } } - private remoteLicenseExpiresAt(license: Pick) { - const expiresAt = new Date(license.endAt); - if (!Number.isFinite(expiresAt.getTime()) || expiresAt <= new Date()) { - throw new LicenseExpired(); + private remoteLicense(response: LicenseResponse) { + this.throwRemoteLicenseError(response.error); + if (!response.license) { + throw new InternalServerError('Invalid AFFiNE Pro license response.'); } - return expiresAt; + return response.license; } - private async fetchAffinePro( - path: string, - init?: RequestInit - ): Promise { - const endpoint = - process.env.AFFINE_PRO_SERVER_ENDPOINT ?? 'https://app.affine.pro'; + private remoteCommand(response: CommandResponse) { + this.throwRemoteLicenseError(response.error); + } + private remotePortal(response: PortalResponse) { + this.throwRemoteLicenseError(response.error); + if (!response.url) { + throw new InternalServerError('Invalid AFFiNE Pro portal response.'); + } + return { url: response.url }; + } + + private throwRemoteLicenseError( + error?: LicenseError | null + ): asserts error is null | undefined { + if (!error) return; try { - const signal = - init?.signal ?? - (AbortSignal.timeout - ? AbortSignal.timeout(AFFINE_PRO_REQUEST_TIMEOUT) - : undefined); - const res = await fetch(endpoint + path, { - ...init, - signal, - headers: { - 'Content-Type': 'application/json', - ...init?.headers, - }, - }); - - if (!res.ok) { - const body = (await res.json()) as UserFriendlyError; - throw UserFriendlyError.fromUserFriendlyErrorJSON(body); - } - - const data = (await res.json()) as T; - return { - ...data, - res, - }; + throw UserFriendlyError.fromUserFriendlyErrorJSON(JSON.parse(error.body)); } catch (e) { if (e instanceof UserFriendlyError) { throw e; } - throw new InternalServerError( - e instanceof Error - ? e.message - : 'Failed to contact with https://app.affine.pro' + 'Failed to contact with https://app.affine.pro' ); } } diff --git a/packages/backend/server/src/plugins/oauth/providers/def.ts b/packages/backend/server/src/plugins/oauth/providers/def.ts index 695eea7a07..ccfba7797f 100644 --- a/packages/backend/server/src/plugins/oauth/providers/def.ts +++ b/packages/backend/server/src/plugins/oauth/providers/def.ts @@ -5,6 +5,7 @@ import { InvalidOauthCallbackCode, InvalidOauthResponse, OnEvent, + safeFetch, } from '../../../base'; import { OAuthProviderName } from '../config'; import { OAuthProviderFactory } from '../factory'; @@ -83,10 +84,16 @@ export abstract class OAuthProvider { init?: RequestInit, options?: { treatServerErrorAsInvalid?: boolean } ) { - const response = await fetch(url, { - ...init, - headers: { ...init?.headers, Accept: 'application/json' }, - }); + const response = await safeFetch( + url, + { ...init, headers: { ...init?.headers, Accept: 'application/json' } }, + { + timeoutMs: 10_000, + maxRedirects: 3, + maxBytes: 1024 * 1024, + allowedHeaders: ['authorization', 'content-type', 'accept'], + } + ); const body = await response.text(); if (!response.ok) { diff --git a/packages/backend/server/src/plugins/oauth/providers/oidc.ts b/packages/backend/server/src/plugins/oauth/providers/oidc.ts index f4859afa5e..455cb6e773 100644 --- a/packages/backend/server/src/plugins/oauth/providers/oidc.ts +++ b/packages/backend/server/src/plugins/oauth/providers/oidc.ts @@ -1,5 +1,10 @@ import { Injectable, OnModuleDestroy } from '@nestjs/common'; -import { createRemoteJWKSet, type JWTPayload, jwtVerify } from 'jose'; +import { + createRemoteJWKSet, + customFetch, + type JWTPayload, + jwtVerify, +} from 'jose'; import { omit } from 'lodash-es'; import { z } from 'zod'; @@ -7,6 +12,7 @@ import { ExponentialBackoffScheduler, InvalidAuthState, InvalidOauthResponse, + safeFetch, URLHelper, } from '../../../base'; import { OAuthOIDCProviderConfig, OAuthProviderName } from '../config'; @@ -59,12 +65,19 @@ type OIDCConfiguration = z.infer; const OIDC_DISCOVERY_INITIAL_RETRY_DELAY = 1000; const OIDC_DISCOVERY_MAX_RETRY_DELAY = 60_000; +const OIDC_FETCH_OPTIONS = { + timeoutMs: 10_000, + maxRedirects: 3, + maxBytes: 1024 * 1024, + allowedHeaders: ['accept'], +}; @Injectable() export class OIDCProvider extends OAuthProvider implements OnModuleDestroy { override provider = OAuthProviderName.OIDC; #endpoints: OIDCConfiguration | null = null; #jwks: ReturnType | null = null; + private readonly oidcFetch = safeFetch; readonly #retryScheduler = new ExponentialBackoffScheduler({ baseDelayMs: OIDC_DISCOVERY_INITIAL_RETRY_DELAY, maxDelayMs: OIDC_DISCOVERY_MAX_RETRY_DELAY, @@ -132,12 +145,10 @@ export class OIDCProvider extends OAuthProvider implements OnModuleDestroy { } try { - const res = await fetch( + const res = await this.oidcFetch( `${config.issuer}/.well-known/openid-configuration`, - { - method: 'GET', - headers: { Accept: 'application/json' }, - } + { method: 'GET', headers: { Accept: 'application/json' } }, + OIDC_FETCH_OPTIONS ); if (generation !== this.#validationGeneration) { @@ -163,7 +174,10 @@ export class OIDCProvider extends OAuthProvider implements OnModuleDestroy { } this.#endpoints = configuration; - this.#jwks = createRemoteJWKSet(new URL(configuration.jwks_uri)); + this.#jwks = createRemoteJWKSet(new URL(configuration.jwks_uri), { + [customFetch]: (url, init) => + this.oidcFetch(url, init, OIDC_FETCH_OPTIONS), + }); this.#retryScheduler.reset(); super.setup(); } catch (e) { diff --git a/packages/backend/server/src/plugins/payment/config.ts b/packages/backend/server/src/plugins/payment/config.ts index fdc8c70b08..1b5c855e97 100644 --- a/packages/backend/server/src/plugins/payment/config.ts +++ b/packages/backend/server/src/plugins/payment/config.ts @@ -27,14 +27,6 @@ declare global { payment: { enabled: boolean; showLifetimePrice: boolean; - /** - * @deprecated use payment.stripe.apiKey - */ - apiKey: string; - /** - * @deprecated use payment.stripe.webhookKey - */ - webhookKey: string; stripe: ConfigItem< { /** Preferred place for Stripe API key */ @@ -70,16 +62,6 @@ defineModuleConfig('payment', { desc: 'Whether enable lifetime price and allow user to pay for it.', default: true, }, - apiKey: { - desc: '[Deprecated] Stripe API key. Use payment.stripe.apiKey instead.', - default: '', - env: 'STRIPE_API_KEY', - }, - webhookKey: { - desc: '[Deprecated] Stripe webhook key. Use payment.stripe.webhookKey instead.', - default: '', - env: 'STRIPE_WEBHOOK_KEY', - }, stripe: { desc: 'Stripe sdk options and credentials', default: { diff --git a/packages/backend/server/src/plugins/payment/controller.ts b/packages/backend/server/src/plugins/payment/controller.ts index 605502b0d5..bf231a15b9 100644 --- a/packages/backend/server/src/plugins/payment/controller.ts +++ b/packages/backend/server/src/plugins/payment/controller.ts @@ -19,9 +19,7 @@ export class StripeWebhookController { @Public() @Post('/webhook') async handleWebhook(@Req() req: RawBodyRequest) { - const nestedWebhookKey = this.config.payment.stripe?.webhookKey; - const legacyWebhookKey = this.config.payment.webhookKey; - const webhookKey = nestedWebhookKey || legacyWebhookKey || ''; + const webhookKey = this.config.payment.stripe?.webhookKey || ''; // Retrieve the event by verifying the signature using the raw body and secret. const signature = req.headers['stripe-signature']; try { diff --git a/packages/backend/server/src/plugins/payment/manager/common.ts b/packages/backend/server/src/plugins/payment/manager/common.ts index dd3dcc785d..74d12e177a 100644 --- a/packages/backend/server/src/plugins/payment/manager/common.ts +++ b/packages/backend/server/src/plugins/payment/manager/common.ts @@ -256,19 +256,21 @@ export abstract class SubscriptionManager { } async getPrice(lookupKey: LookupKey): Promise { + let key: string; + try { + key = encodeLookupKey(lookupKey); + } catch { + return null; + } + const prices = await this.stripe.prices.list({ - lookup_keys: [encodeLookupKey(lookupKey)], + lookup_keys: [key], limit: 1, }); const price = prices.data[0]; - return price - ? { - lookupKey, - price, - } - : null; + return price ? { lookupKey, price } : null; } protected async getCouponFromPromotionCode( diff --git a/packages/backend/server/src/plugins/payment/manager/user.ts b/packages/backend/server/src/plugins/payment/manager/user.ts index 4aa50ce8f3..004f269591 100644 --- a/packages/backend/server/src/plugins/payment/manager/user.ts +++ b/packages/backend/server/src/plugins/payment/manager/user.ts @@ -7,23 +7,18 @@ import { z } from 'zod'; import { Config, EventBus, - InternalServerError, InvalidCheckoutParameters, ManagedByAppStoreOrPlay, Mutex, - OneMonth, OnEvent, - OneYear, SubscriptionAlreadyExists, SubscriptionPlanNotFound, TooManyRequest, URLHelper, } from '../../../base'; import { EntitlementService } from '../../../core/entitlement'; -import { EarlyAccessType, FeatureService } from '../../../core/features'; import { StripeFactory } from '../stripe'; import { - CouponType, KnownStripeInvoice, KnownStripePrice, KnownStripeSubscription, @@ -32,7 +27,6 @@ import { SubscriptionPlan, SubscriptionRecurring, SubscriptionStatus, - SubscriptionVariant, } from '../types'; import { activeSubscriptionWhere, @@ -42,11 +36,8 @@ import { } from './common'; interface PriceStrategyStatus { - proEarlyAccess: boolean; - aiEarlyAccess: boolean; proSubscribed: boolean; aiSubscribed: boolean; - onetime: boolean; } export const UserSubscriptionIdentity = z.object({ @@ -67,7 +58,6 @@ export class UserSubscriptionManager extends SubscriptionManager { stripeProvider: StripeFactory, db: PrismaClient, private readonly config: Config, - private readonly feature: FeatureService, private readonly event: EventBus, private readonly url: URLHelper, private readonly mutex: Mutex, @@ -78,22 +68,12 @@ export class UserSubscriptionManager extends SubscriptionManager { async filterPrices( prices: KnownStripePrice[], - customer?: UserStripeCustomer + _customer?: UserStripeCustomer ) { - const strategyStatus = customer - ? await this.strategyStatus(customer) - : { - proEarlyAccess: false, - aiEarlyAccess: false, - proSubscribed: false, - aiSubscribed: false, - onetime: false, - }; - const availablePrices: KnownStripePrice[] = []; for (const price of prices) { - if (await this.isPriceAvailable(price, strategyStatus)) { + if (await this.isPriceAvailable(price)) { availablePrices.push(price); } } @@ -107,8 +87,9 @@ export class UserSubscriptionManager extends SubscriptionManager { { user }: z.infer ) { if ( - lookupKey.plan !== SubscriptionPlan.Pro && - lookupKey.plan !== SubscriptionPlan.AI + (lookupKey.plan !== SubscriptionPlan.Pro && + lookupKey.plan !== SubscriptionPlan.AI) || + lookupKey.variant !== null ) { throw new InvalidCheckoutParameters(); } @@ -125,15 +106,8 @@ export class UserSubscriptionManager extends SubscriptionManager { active && // do not allow to re-subscribe unless !( - /* current subscription is a onetime subscription and so as the one that's checking out */ - ( - (active.variant === SubscriptionVariant.Onetime && - lookupKey.variant === SubscriptionVariant.Onetime) || - /* current subscription is normal subscription and is checking-out a lifetime subscription */ - (active.recurring !== SubscriptionRecurring.Lifetime && - active.variant !== SubscriptionVariant.Onetime && - lookupKey.recurring === SubscriptionRecurring.Lifetime) - ) + active.recurring !== SubscriptionRecurring.Lifetime && + lookupKey.recurring === SubscriptionRecurring.Lifetime ) ) { throw new SubscriptionAlreadyExists({ plan: lookupKey.plan }); @@ -141,12 +115,9 @@ export class UserSubscriptionManager extends SubscriptionManager { const customer = await this.getOrCreateCustomer(user.id); const strategy = await this.strategyStatus(customer); - const price = await this.autoPrice(lookupKey, strategy); + const price = await this.getPrice(lookupKey); - if ( - !price || - !(await this.isPriceAvailable(price, { ...strategy, onetime: true })) - ) { + if (!price || !(await this.isPriceAvailable(price))) { throw new SubscriptionPlanNotFound({ plan: lookupKey.plan, recurring: lookupKey.recurring, @@ -154,10 +125,7 @@ export class UserSubscriptionManager extends SubscriptionManager { } const discounts = await (async () => { - const coupon = await this.getBuildInCoupon(customer, price); - if (coupon) { - return { discounts: [{ coupon }] }; - } else if (params.coupon) { + if (params.coupon) { const couponId = await this.getCouponFromPromotionCode( params.coupon, customer @@ -179,10 +147,9 @@ export class UserSubscriptionManager extends SubscriptionManager { return undefined; })(); - // mode: 'subscription' or 'payment' for lifetime and onetime payment + // mode: 'subscription' or 'payment' for lifetime payment const mode = - lookupKey.recurring === SubscriptionRecurring.Lifetime || - lookupKey.variant === SubscriptionVariant.Onetime + lookupKey.recurring === SubscriptionRecurring.Lifetime ? { mode: 'payment' as const, invoice_creation: { @@ -339,37 +306,6 @@ export class UserSubscriptionManager extends SubscriptionManager { }); } - private async getBuildInCoupon( - customer: UserStripeCustomer, - price: KnownStripePrice - ) { - const strategyStatus = await this.strategyStatus(customer); - - // onetime price is allowed for checkout - strategyStatus.onetime = true; - - if (!(await this.isPriceAvailable(price, strategyStatus))) { - return null; - } - - let coupon: CouponType | undefined; - - if (price.lookupKey.variant === SubscriptionVariant.EA) { - if (price.lookupKey.plan === SubscriptionPlan.Pro) { - coupon = CouponType.ProEarlyAccessOneYearFree; - } else if (price.lookupKey.plan === SubscriptionPlan.AI) { - coupon = CouponType.AIEarlyAccessOneYearFree; - } - } else if (price.lookupKey.plan === SubscriptionPlan.AI) { - const { proEarlyAccess, aiSubscribed } = strategyStatus; - if (proEarlyAccess && !aiSubscribed) { - coupon = CouponType.ProEarlyAccessAIOneYearFree; - } - } - - return coupon; - } - async saveInvoice(knownInvoice: KnownStripeInvoice) { const { userId, lookupKey, stripeInvoice } = knownInvoice; this.assertUserIdExists(userId); @@ -387,11 +323,11 @@ export class UserSubscriptionManager extends SubscriptionManager { }, }); - // onetime and lifetime subscription is a special "subscription" that doesn't get involved with stripe subscription system - // we track the deals by invoice only. + // Lifetime subscription does not get involved with the Stripe subscription system. + // We track the deal by invoice only. if (stripeInvoice.status === 'paid') { await using lock = await this.mutex.acquire( - `redeem-onetime-subscription:${stripeInvoice.id}` + `redeem-lifetime-subscription:${stripeInvoice.id}` ); if (!lock) { @@ -400,8 +336,6 @@ export class UserSubscriptionManager extends SubscriptionManager { if (lookupKey.recurring === SubscriptionRecurring.Lifetime) { await this.saveLifetimeSubscription(knownInvoice); - } else if (lookupKey.variant === SubscriptionVariant.Onetime) { - await this.saveOnetimePaymentSubscription(knownInvoice); } } @@ -470,106 +404,7 @@ export class UserSubscriptionManager extends SubscriptionManager { }); } - async saveOnetimePaymentSubscription(knownInvoice: KnownStripeInvoice) { - this.assertUserIdExists(knownInvoice.userId); - const { userId, lookupKey, stripeInvoice } = knownInvoice; - - const invoice = await this.db.invoice.findUnique({ - where: { - stripeInvoiceId: stripeInvoice.id, - }, - }); - - if (!invoice) { - // never happens - throw new InternalServerError('Invoice not found'); - } - - if (invoice.onetimeSubscriptionRedeemed) { - return; - } - - await this.db.invoice.update({ - select: { - onetimeSubscriptionRedeemed: true, - }, - where: { - stripeInvoiceId: stripeInvoice.id, - }, - data: { onetimeSubscriptionRedeemed: true }, - }); - - const existingSubscription = await this.db.subscription.findUnique({ - where: { - targetId_plan: { - targetId: userId, - plan: lookupKey.plan, - }, - }, - }); - - const subscriptionTime = - lookupKey.recurring === SubscriptionRecurring.Monthly - ? OneMonth - : OneYear; - - let subscription: Subscription; - - // extends the subscription time if exists - if (existingSubscription) { - if (!existingSubscription.end) { - throw new InternalServerError( - 'Unexpected onetime subscription with no end date' - ); - } - - const period = - // expired, reset the period - existingSubscription.end <= new Date() - ? { - start: new Date(), - end: new Date(Date.now() + subscriptionTime), - } - : { - end: new Date( - existingSubscription.end.getTime() + subscriptionTime - ), - }; - - subscription = await this.db.subscription.update({ - where: { - id: existingSubscription.id, - }, - data: period, - }); - } else { - subscription = await this.db.subscription.create({ - data: { - targetId: userId, - stripeSubscriptionId: null, - ...lookupKey, - start: new Date(), - end: new Date(Date.now() + subscriptionTime), - status: SubscriptionStatus.Active, - nextBillAt: null, - }, - }); - } - - this.event.emit('user.subscription.activated', { - userId, - plan: lookupKey.plan, - recurring: lookupKey.recurring, - }); - await this.entitlement.upsertFromCloudSubscription({ - ...subscription, - targetId: userId, - }); - - return subscription; - } - - async revokeOnetimeOrLifetime(knownInvoice: KnownStripeInvoice) { + async revokeLifetime(knownInvoice: KnownStripeInvoice) { this.assertUserIdExists(knownInvoice.userId); const { userId, lookupKey } = knownInvoice; @@ -609,7 +444,7 @@ export class UserSubscriptionManager extends SubscriptionManager { }); } - async restoreOnetimeOrLifetime(knownInvoice: KnownStripeInvoice) { + async restoreLifetime(knownInvoice: KnownStripeInvoice) { this.assertUserIdExists(knownInvoice.userId); const { userId, lookupKey, stripeInvoice } = knownInvoice; @@ -627,18 +462,6 @@ export class UserSubscriptionManager extends SubscriptionManager { ? stripeInvoice.created : Date.now() / 1000); - let end: Date | null = null; - - if (lookupKey.recurring === SubscriptionRecurring.Lifetime) { - end = null; - } else if (lookupKey.variant === SubscriptionVariant.Onetime) { - const isMonthly = lookupKey.recurring === SubscriptionRecurring.Monthly; - const duration = isMonthly ? OneMonth : OneYear; - end = subscription?.end ?? new Date(start * 1000 + duration); - } else { - end = subscription?.end ?? null; - } - if (subscription) { const saved = await this.db.subscription.update({ where: { id: subscription.id }, @@ -647,7 +470,7 @@ export class UserSubscriptionManager extends SubscriptionManager { canceledAt: null, nextBillAt: null, start: subscription.start ?? new Date(start * 1000), - end, + end: null, }, }); await this.entitlement.upsertFromCloudSubscription(saved); @@ -658,7 +481,7 @@ export class UserSubscriptionManager extends SubscriptionManager { stripeSubscriptionId: null, ...lookupKey, start: new Date(start * 1000), - end, + end: null, status: SubscriptionStatus.Active, nextBillAt: null, }, @@ -673,111 +496,43 @@ export class UserSubscriptionManager extends SubscriptionManager { }); } - private async autoPrice(lookupKey: LookupKey, strategy: PriceStrategyStatus) { - // auto select ea variant when available if not specified - let variant: SubscriptionVariant | null = lookupKey.variant; - - if (!variant) { - // make the if conditions separated, more readable - // pro early access - if ( - lookupKey.plan === SubscriptionPlan.Pro && - lookupKey.recurring === SubscriptionRecurring.Yearly && - strategy.proEarlyAccess && - !strategy.proSubscribed - ) { - variant = SubscriptionVariant.EA; - } - - // ai early access - if ( - lookupKey.plan === SubscriptionPlan.AI && - lookupKey.recurring === SubscriptionRecurring.Yearly && - strategy.aiEarlyAccess && - !strategy.aiSubscribed - ) { - variant = SubscriptionVariant.EA; - } - } - - return this.getPrice({ - plan: lookupKey.plan, - recurring: lookupKey.recurring, - variant, - }); - } - - private async isPriceAvailable( - price: KnownStripePrice, - strategy: PriceStrategyStatus - ) { + private async isPriceAvailable(price: KnownStripePrice) { if (price.lookupKey.plan === SubscriptionPlan.Pro) { - return this.isProPriceAvailable(price, strategy); + return this.isProPriceAvailable(price); } if (price.lookupKey.plan === SubscriptionPlan.AI) { - return this.isAIPriceAvailable(price, strategy); + return this.isAIPriceAvailable(price); } return false; } - private async isProPriceAvailable( - { lookupKey }: KnownStripePrice, - { proEarlyAccess, proSubscribed, onetime }: PriceStrategyStatus - ) { + private async isProPriceAvailable({ lookupKey }: KnownStripePrice) { if (lookupKey.recurring === SubscriptionRecurring.Lifetime) { return this.config.payment.showLifetimePrice; } - if (lookupKey.variant === SubscriptionVariant.Onetime) { - return onetime; - } - // no special price for monthly plan if (lookupKey.recurring === SubscriptionRecurring.Monthly) { return true; } - // show EA price instead of normal price if early access is available - return proEarlyAccess && !proSubscribed - ? lookupKey.variant === SubscriptionVariant.EA - : lookupKey.variant !== SubscriptionVariant.EA; + return lookupKey.variant === null; } - private async isAIPriceAvailable( - { lookupKey }: KnownStripePrice, - { aiEarlyAccess, aiSubscribed, onetime }: PriceStrategyStatus - ) { + private async isAIPriceAvailable({ lookupKey }: KnownStripePrice) { // no lifetime price for AI if (lookupKey.recurring === SubscriptionRecurring.Lifetime) { return false; } - // never show onetime prices - if (lookupKey.variant === SubscriptionVariant.Onetime) { - return onetime; - } - - // show EA price instead of normal price if early access is available - return aiEarlyAccess && !aiSubscribed - ? lookupKey.variant === SubscriptionVariant.EA - : lookupKey.variant !== SubscriptionVariant.EA; + return lookupKey.variant === null; } private async strategyStatus( customer: UserStripeCustomer ): Promise { - const proEarlyAccess = await this.feature.isEarlyAccessUser( - customer.userId, - EarlyAccessType.App - ); - - const aiEarlyAccess = await this.feature.isEarlyAccessUser( - customer.userId, - EarlyAccessType.AI - ); - let proSubscribed = false; let aiSubscribed = false; @@ -786,8 +541,6 @@ export class UserSubscriptionManager extends SubscriptionManager { status: 'all', }); - // if the early access user had early access subscription in the past, but it got canceled or past due, - // the user will lose the early access privilege for (const sub of subscriptions.data) { const lookupKey = retriveLookupKeyFromStripeSubscription(sub); if (!lookupKey) { @@ -803,13 +556,7 @@ export class UserSubscriptionManager extends SubscriptionManager { } } - return { - proEarlyAccess, - aiEarlyAccess, - proSubscribed, - aiSubscribed, - onetime: false, - }; + return { proSubscribed, aiSubscribed }; } private assertUserIdExists( diff --git a/packages/backend/server/src/plugins/payment/resolver.ts b/packages/backend/server/src/plugins/payment/resolver.ts index acf0c0ab0e..f3f1f157fe 100644 --- a/packages/backend/server/src/plugins/payment/resolver.ts +++ b/packages/backend/server/src/plugins/payment/resolver.ts @@ -166,14 +166,6 @@ export class InvoiceType implements Partial { @Field(() => Date) updatedAt!: Date; - // deprecated fields - @Field(() => String, { - name: 'id', - nullable: true, - deprecationReason: 'removed', - }) - stripeInvoiceId?: string; - @Field(() => SubscriptionPlan, { nullable: true, deprecationReason: 'removed', @@ -475,12 +467,7 @@ export class UserSubscriptionResolver { ) {} private normalizeSubscription(s: Subscription) { - if ( - s.variant && - ![SubscriptionVariant.EA, SubscriptionVariant.Onetime].includes( - s.variant as SubscriptionVariant - ) - ) { + if (s.variant && s.variant !== SubscriptionVariant.Onetime) { s.variant = null; } return s; diff --git a/packages/backend/server/src/plugins/payment/service.ts b/packages/backend/server/src/plugins/payment/service.ts index 3206a88f42..7e38b33a26 100644 --- a/packages/backend/server/src/plugins/payment/service.ts +++ b/packages/backend/server/src/plugins/payment/service.ts @@ -55,7 +55,6 @@ import { SubscriptionPlan, SubscriptionRecurring, SubscriptionStatus, - SubscriptionVariant, } from './types'; export const CheckoutExtraArgs = z.union([ @@ -536,9 +535,8 @@ export class SubscriptionService { reason === 'dispute_open' || reason === 'dispute_lost'; const restore = reason === 'dispute_won'; - const isOneTimeOrLifetime = - knownInvoice.lookupKey.recurring === SubscriptionRecurring.Lifetime || - knownInvoice.lookupKey.variant === SubscriptionVariant.Onetime; + const isLifetime = + knownInvoice.lookupKey.recurring === SubscriptionRecurring.Lifetime; if (restore) { if (invoice.subscription) { @@ -564,11 +562,11 @@ export class SubscriptionService { } if ( - isOneTimeOrLifetime && + isLifetime && (knownInvoice.lookupKey.plan === SubscriptionPlan.Pro || knownInvoice.lookupKey.plan === SubscriptionPlan.AI) ) { - await this.userManager.restoreOnetimeOrLifetime(knownInvoice); + await this.userManager.restoreLifetime(knownInvoice); } return; @@ -615,11 +613,11 @@ export class SubscriptionService { } if ( - isOneTimeOrLifetime && + isLifetime && (knownInvoice.lookupKey.plan === SubscriptionPlan.Pro || knownInvoice.lookupKey.plan === SubscriptionPlan.AI) ) { - await this.userManager.revokeOnetimeOrLifetime(knownInvoice); + await this.userManager.revokeLifetime(knownInvoice); return; } diff --git a/packages/backend/server/src/plugins/payment/stripe.ts b/packages/backend/server/src/plugins/payment/stripe.ts index 59a811a733..180b4fa78a 100644 --- a/packages/backend/server/src/plugins/payment/stripe.ts +++ b/packages/backend/server/src/plugins/payment/stripe.ts @@ -7,7 +7,6 @@ import { decodeLookupKey, DEFAULT_PRICES, SubscriptionRecurring, - SubscriptionVariant, } from './types'; @Injectable() @@ -39,20 +38,18 @@ export class StripeFactory { } setup() { - // Prefer new keys under payment.stripe.*, fallback to legacy root keys for backward compatibility const { - apiKey: nestedApiKey, + apiKey, webhookKey: _, ...config } = this.config.payment.stripe || {}; // NOTE: // we always fake a key if not set because `new Stripe` will complain if it's empty string // this will make code cleaner than providing `Stripe` instance as optional one. - const apiKey = - nestedApiKey || this.config.payment.apiKey || 'stripe-api-key'; + const stripeApiKey = apiKey || 'stripe-api-key'; // TODO@(@darkskygit): use per-requests api key injection - this.#stripe = new Stripe(apiKey, config); + this.#stripe = new Stripe(stripeApiKey, config); if (this.config.payment.enabled) { this.server.enableFeature(ServerFeature.Payment); } else { @@ -107,8 +104,7 @@ export class StripeFactory { lookup_key: key, tax_behavior: 'inclusive', recurring: - lookupKey.recurring === SubscriptionRecurring.Lifetime || - lookupKey.variant === SubscriptionVariant.Onetime + lookupKey.recurring === SubscriptionRecurring.Lifetime ? undefined : { interval: diff --git a/packages/backend/server/src/plugins/payment/types.ts b/packages/backend/server/src/plugins/payment/types.ts index 2f26b38fe6..de10de87c4 100644 --- a/packages/backend/server/src/plugins/payment/types.ts +++ b/packages/backend/server/src/plugins/payment/types.ts @@ -20,7 +20,6 @@ export enum SubscriptionPlan { } export enum SubscriptionVariant { - EA = 'earlyaccess', Onetime = 'onetime', } @@ -44,12 +43,6 @@ export enum InvoiceStatus { Uncollectible = 'uncollectible', } -export enum CouponType { - ProEarlyAccessOneYearFree = 'pro_ea_one_year_free', - AIEarlyAccessOneYearFree = 'ai_ea_one_year_free', - ProEarlyAccessAIOneYearFree = 'ai_pro_ea_one_year_free', -} - declare global { interface Events { 'user.subscription.activated': { @@ -199,11 +192,6 @@ export const DEFAULT_PRICES = new Map([ price: 8100, }, ], - // only EA for yearly pro - [ - `${SubscriptionPlan.Pro}_${SubscriptionRecurring.Yearly}_${SubscriptionVariant.EA}`, - { product: 'AFFiNE Pro', price: 5000 }, - ], [ `${SubscriptionPlan.Pro}_${SubscriptionRecurring.Lifetime}`, { @@ -211,29 +199,11 @@ export const DEFAULT_PRICES = new Map([ price: 49900, }, ], - [ - `${SubscriptionPlan.Pro}_${SubscriptionRecurring.Monthly}_${SubscriptionVariant.Onetime}`, - { product: 'AFFiNE Pro - One Month', price: 799 }, - ], - [ - `${SubscriptionPlan.Pro}_${SubscriptionRecurring.Yearly}_${SubscriptionVariant.Onetime}`, - { product: 'AFFiNE Pro - One Year', price: 8100 }, - ], - // ai [ `${SubscriptionPlan.AI}_${SubscriptionRecurring.Yearly}`, { product: 'AFFiNE AI', price: 10680 }, ], - // only EA for yearly AI - [ - `${SubscriptionPlan.AI}_${SubscriptionRecurring.Yearly}_${SubscriptionVariant.EA}`, - { product: 'AFFiNE AI', price: 9900 }, - ], - [ - `${SubscriptionPlan.AI}_${SubscriptionRecurring.Yearly}_${SubscriptionVariant.Onetime}`, - { product: 'AFFiNE AI - One Year', price: 10680 }, - ], // team [ @@ -284,7 +254,7 @@ export function decodeLookupKey(key: string): LookupKey { return { plan: plan as SubscriptionPlan, recurring: recurring as SubscriptionRecurring, - variant: variant as SubscriptionVariant, + variant: (variant as SubscriptionVariant) ?? null, }; } diff --git a/packages/backend/server/src/plugins/worker/controller.ts b/packages/backend/server/src/plugins/worker/controller.ts index 88c396825b..fcffb52285 100644 --- a/packages/backend/server/src/plugins/worker/controller.ts +++ b/packages/backend/server/src/plugins/worker/controller.ts @@ -60,6 +60,8 @@ function toBadRequestReason(reason: SSRFBlockReason) { return 'Failed to resolve hostname'; case 'too_many_redirects': return 'Too many redirects'; + default: + return 'Blocked by SSRF protection'; } } diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index f52ee6e77a..3c92daca88 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -1113,9 +1113,7 @@ type ExpectToUpdateDocUserRoleDataType { } enum FeatureType { - AIEarlyAccess Admin - EarlyAccess FreePlan LifetimeProPlan ProPlan @@ -1340,9 +1338,6 @@ type InviteResult { """Invite id, null if invite record create failed""" inviteId: String - - """Invite email sent success""" - sentSuccess: Boolean! @deprecated(reason: "Notification will be sent asynchronously") } type InviteUserType { @@ -1393,7 +1388,6 @@ type InvoiceType { amount: Int! createdAt: DateTime! currency: String! - id: String @deprecated(reason: "removed") lastPaymentError: String link: String plan: SubscriptionPlan @deprecated(reason: "removed") @@ -1695,7 +1689,7 @@ type Mutation { revokeMember(userId: String!, workspaceId: String!): Boolean! revokePublicDoc(docId: String!, workspaceId: String!): DocType! revokeUserAccessToken(id: String!): Boolean! - sendChangeEmail(callbackUrl: String!, email: String): Boolean! + sendChangeEmail(callbackUrl: String!): Boolean! sendChangePasswordEmail(callbackUrl: String!, email: String @deprecated(reason: "fetched from signed in user")): Boolean! sendSetPasswordEmail(callbackUrl: String!, email: String @deprecated(reason: "fetched from signed in user")): Boolean! sendTestEmail(config: JSONObject!): Boolean! @@ -2399,7 +2393,6 @@ type SubscriptionType { } enum SubscriptionVariant { - EA Onetime } @@ -2667,9 +2660,6 @@ type UserType { """User name""" name: String! - """Get user notification count""" - notificationCount: Int! @deprecated(reason: "Use realtime subscription \"notification.count.changed\" instead.") - """Get current user notifications""" notifications(pagination: PaginationInput!): PaginatedNotificationObjectType! quota: UserQuotaType! @@ -2835,7 +2825,6 @@ type WorkspaceQuotaType { name: String! overcapacityMemberCount: Int! storageQuota: SafeInt! - usedSize: SafeInt! @deprecated(reason: "use `usedStorageQuota` instead") usedStorageQuota: SafeInt! } diff --git a/packages/common/graphql/src/graphql/index.ts b/packages/common/graphql/src/graphql/index.ts index d3ecb5c530..a0e0a4e231 100644 --- a/packages/common/graphql/src/graphql/index.ts +++ b/packages/common/graphql/src/graphql/index.ts @@ -2348,7 +2348,6 @@ export const invoicesQuery = { currentUser { invoiceCount invoices(take: $take, skip: $skip) { - id status currency amount @@ -2359,7 +2358,6 @@ export const invoicesQuery = { } } }`, - deprecations: ["'id' is deprecated: removed"], }; export const leaveWorkspaceMutation = { @@ -2983,10 +2981,8 @@ export const inviteByEmailsMutation = { inviteMembers(workspaceId: $workspaceId, emails: $emails) { email inviteId - sentSuccess } }`, - deprecations: ["'sentSuccess' is deprecated: Notification will be sent asynchronously"], }; export const acceptInviteByInviteIdMutation = { @@ -3023,7 +3019,6 @@ export const workspaceInvoicesQuery = { workspace(id: $workspaceId) { invoiceCount invoices(take: $take, skip: $skip) { - id status currency amount @@ -3034,7 +3029,6 @@ export const workspaceInvoicesQuery = { } } }`, - deprecations: ["'id' is deprecated: removed"], }; export const getWorkspaceRolePermissionsQuery = { diff --git a/packages/common/graphql/src/graphql/invoices.gql b/packages/common/graphql/src/graphql/invoices.gql index 6b9517de5f..594d351b25 100644 --- a/packages/common/graphql/src/graphql/invoices.gql +++ b/packages/common/graphql/src/graphql/invoices.gql @@ -2,7 +2,6 @@ query invoices($take: Int!, $skip: Int!) { currentUser { invoiceCount invoices(take: $take, skip: $skip) { - id status currency amount diff --git a/packages/common/graphql/src/graphql/workspace-intive-by-emails.gql b/packages/common/graphql/src/graphql/workspace-intive-by-emails.gql index e0b525ed17..8856376608 100644 --- a/packages/common/graphql/src/graphql/workspace-intive-by-emails.gql +++ b/packages/common/graphql/src/graphql/workspace-intive-by-emails.gql @@ -2,6 +2,5 @@ mutation inviteByEmails($workspaceId: String!, $emails: [String!]!) { inviteMembers(workspaceId: $workspaceId, emails: $emails) { email inviteId - sentSuccess } } diff --git a/packages/common/graphql/src/graphql/workspace-invoices.gql b/packages/common/graphql/src/graphql/workspace-invoices.gql index 43e1bd6d41..2bb302ccb5 100644 --- a/packages/common/graphql/src/graphql/workspace-invoices.gql +++ b/packages/common/graphql/src/graphql/workspace-invoices.gql @@ -2,7 +2,6 @@ query workspaceInvoices($take: Int!, $skip: Int!, $workspaceId: String!) { workspace(id: $workspaceId) { invoiceCount invoices(take: $take, skip: $skip) { - id status currency amount diff --git a/packages/common/graphql/src/schema.ts b/packages/common/graphql/src/schema.ts index a0e2e81b5e..4e666d23ba 100644 --- a/packages/common/graphql/src/schema.ts +++ b/packages/common/graphql/src/schema.ts @@ -1338,9 +1338,7 @@ export interface ExpectToUpdateDocUserRoleDataType { } export enum FeatureType { - AIEarlyAccess = 'AIEarlyAccess', Admin = 'Admin', - EarlyAccess = 'EarlyAccess', FreePlan = 'FreePlan', LifetimeProPlan = 'LifetimeProPlan', ProPlan = 'ProPlan', @@ -1564,11 +1562,6 @@ export interface InviteResult { error: Maybe; /** Invite id, null if invite record create failed */ inviteId: Maybe; - /** - * Invite email sent success - * @deprecated Notification will be sent asynchronously - */ - sentSuccess: Scalars['Boolean']['output']; } export interface InviteUserType { @@ -1617,8 +1610,6 @@ export interface InvoiceType { amount: Scalars['Int']['output']; createdAt: Scalars['DateTime']['output']; currency: Scalars['String']['output']; - /** @deprecated removed */ - id: Maybe; lastPaymentError: Maybe; link: Maybe; /** @deprecated removed */ @@ -2273,7 +2264,6 @@ export interface MutationRevokeUserAccessTokenArgs { export interface MutationSendChangeEmailArgs { callbackUrl: Scalars['String']['input']; - email?: InputMaybe; } export interface MutationSendChangePasswordEmailArgs { @@ -3124,7 +3114,6 @@ export interface SubscriptionType { } export enum SubscriptionVariant { - EA = 'EA', Onetime = 'Onetime', } @@ -3396,11 +3385,6 @@ export interface UserType { invoices: Array; /** User name */ name: Scalars['String']['output']; - /** - * Get user notification count - * @deprecated Use realtime subscription "notification.count.changed" instead. - */ - notificationCount: Scalars['Int']['output']; /** Get current user notifications */ notifications: PaginatedNotificationObjectType; quota: UserQuotaType; @@ -3597,8 +3581,6 @@ export interface WorkspaceQuotaType { name: Scalars['String']['output']; overcapacityMemberCount: Scalars['Int']['output']; storageQuota: Scalars['SafeInt']['output']; - /** @deprecated use `usedStorageQuota` instead */ - usedSize: Scalars['SafeInt']['output']; usedStorageQuota: Scalars['SafeInt']['output']; } @@ -6874,7 +6856,6 @@ export type InvoicesQuery = { invoiceCount: number; invoices: Array<{ __typename?: 'InvoiceType'; - id: string | null; status: InvoiceStatus; currency: string; amount: number; @@ -7598,7 +7579,6 @@ export type InviteByEmailsMutation = { __typename?: 'InviteResult'; email: string; inviteId: string | null; - sentSuccess: boolean; }>; }; @@ -7648,7 +7628,6 @@ export type WorkspaceInvoicesQuery = { invoiceCount: number; invoices: Array<{ __typename?: 'InvoiceType'; - id: string | null; status: InvoiceStatus; currency: string; amount: number; diff --git a/packages/frontend/admin/src/config.json b/packages/frontend/admin/src/config.json index 8f61424ec0..b792db7b98 100644 --- a/packages/frontend/admin/src/config.json +++ b/packages/frontend/admin/src/config.json @@ -429,16 +429,6 @@ "desc": "Number of workspaces automatically indexed per batch" } }, - "customerIo": { - "enabled": { - "type": "Boolean", - "desc": "Enable customer.io integration" - }, - "token": { - "type": "String", - "desc": "Customer.io token" - } - }, "oauth": { "providers.google": { "type": "Object", @@ -470,16 +460,6 @@ "type": "Boolean", "desc": "Whether enable lifetime price and allow user to pay for it." }, - "apiKey": { - "type": "String", - "desc": "[Deprecated] Stripe API key. Use payment.stripe.apiKey instead.", - "env": "STRIPE_API_KEY" - }, - "webhookKey": { - "type": "String", - "desc": "[Deprecated] Stripe webhook key. Use payment.stripe.webhookKey instead.", - "env": "STRIPE_WEBHOOK_KEY" - }, "stripe": { "type": "Object", "desc": "Stripe sdk options and credentials", diff --git a/packages/frontend/admin/src/modules/settings/config.ts b/packages/frontend/admin/src/modules/settings/config.ts index e47e18cc0d..03e2f6a5bc 100644 --- a/packages/frontend/admin/src/modules/settings/config.ts +++ b/packages/frontend/admin/src/modules/settings/config.ts @@ -37,13 +37,7 @@ type ConfigGroup = { const IGNORED_MODULES: (keyof AppConfig)[] = []; if (environment.isSelfHosted) { - IGNORED_MODULES.push( - 'payment', - 'customerIo', - 'captcha', - 'telemetry', - 'metrics' - ); + IGNORED_MODULES.push('payment', 'captcha', 'telemetry', 'metrics'); } const ALL_CONFIGURABLE_MODULES = Object.keys(CONFIG_DESCRIPTORS).filter( diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/AdminUpdateWorkspaceMutation.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/AdminUpdateWorkspaceMutation.graphql.swift index ddb460d840..888361e25e 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/AdminUpdateWorkspaceMutation.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/AdminUpdateWorkspaceMutation.graphql.swift @@ -30,7 +30,7 @@ public class AdminUpdateWorkspaceMutation: GraphQLMutation { AdminUpdateWorkspaceMutation.Data.self ] } - /// Update workspace flags and features for admin + /// Update workspace flags for admin public var adminUpdateWorkspace: AdminUpdateWorkspace? { __data["adminUpdateWorkspace"] } /// AdminUpdateWorkspace diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/ClearWorkspaceByokConfigsMutation.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/ClearWorkspaceByokConfigsMutation.graphql.swift new file mode 100644 index 0000000000..2408bbc579 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/ClearWorkspaceByokConfigsMutation.graphql.swift @@ -0,0 +1,35 @@ +// @generated +// This file was automatically generated and should not be edited. + +@_exported import ApolloAPI + +public class ClearWorkspaceByokConfigsMutation: GraphQLMutation { + public static let operationName: String = "clearWorkspaceByokConfigs" + public static let operationDocument: ApolloAPI.OperationDocument = .init( + definition: .init( + #"mutation clearWorkspaceByokConfigs($workspaceId: String!) { clearWorkspaceByokConfigs(workspaceId: $workspaceId) }"# + )) + + public var workspaceId: String + + public init(workspaceId: String) { + self.workspaceId = workspaceId + } + + public var __variables: Variables? { ["workspaceId": workspaceId] } + + public struct Data: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Mutation } + public static var __selections: [ApolloAPI.Selection] { [ + .field("clearWorkspaceByokConfigs", Bool.self, arguments: ["workspaceId": .variable("workspaceId")]), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + ClearWorkspaceByokConfigsMutation.Data.self + ] } + + public var clearWorkspaceByokConfigs: Bool { __data["clearWorkspaceByokConfigs"] } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/CreateWorkspaceByokLocalLeaseMutation.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/CreateWorkspaceByokLocalLeaseMutation.graphql.swift new file mode 100644 index 0000000000..348d035b8a --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/CreateWorkspaceByokLocalLeaseMutation.graphql.swift @@ -0,0 +1,56 @@ +// @generated +// This file was automatically generated and should not be edited. + +@_exported import ApolloAPI + +public class CreateWorkspaceByokLocalLeaseMutation: GraphQLMutation { + public static let operationName: String = "createWorkspaceByokLocalLease" + public static let operationDocument: ApolloAPI.OperationDocument = .init( + definition: .init( + #"mutation createWorkspaceByokLocalLease($input: CreateWorkspaceByokLocalLeaseInput!) { createWorkspaceByokLocalLease(input: $input) { __typename leaseId expiresAt } }"# + )) + + public var input: CreateWorkspaceByokLocalLeaseInput + + public init(input: CreateWorkspaceByokLocalLeaseInput) { + self.input = input + } + + public var __variables: Variables? { ["input": input] } + + public struct Data: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Mutation } + public static var __selections: [ApolloAPI.Selection] { [ + .field("createWorkspaceByokLocalLease", CreateWorkspaceByokLocalLease.self, arguments: ["input": .variable("input")]), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + CreateWorkspaceByokLocalLeaseMutation.Data.self + ] } + + public var createWorkspaceByokLocalLease: CreateWorkspaceByokLocalLease { __data["createWorkspaceByokLocalLease"] } + + /// CreateWorkspaceByokLocalLease + /// + /// Parent Type: `CreateWorkspaceByokLocalLeaseResultType` + public struct CreateWorkspaceByokLocalLease: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.CreateWorkspaceByokLocalLeaseResultType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("leaseId", String.self), + .field("expiresAt", AffineGraphQL.DateTime.self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + CreateWorkspaceByokLocalLeaseMutation.Data.CreateWorkspaceByokLocalLease.self + ] } + + public var leaseId: String { __data["leaseId"] } + public var expiresAt: AffineGraphQL.DateTime { __data["expiresAt"] } + } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/DeleteWorkspaceByokConfigMutation.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/DeleteWorkspaceByokConfigMutation.graphql.swift new file mode 100644 index 0000000000..7b5e1b8573 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/DeleteWorkspaceByokConfigMutation.graphql.swift @@ -0,0 +1,46 @@ +// @generated +// This file was automatically generated and should not be edited. + +@_exported import ApolloAPI + +public class DeleteWorkspaceByokConfigMutation: GraphQLMutation { + public static let operationName: String = "deleteWorkspaceByokConfig" + public static let operationDocument: ApolloAPI.OperationDocument = .init( + definition: .init( + #"mutation deleteWorkspaceByokConfig($workspaceId: String!, $id: ID!) { deleteWorkspaceByokConfig(workspaceId: $workspaceId, id: $id) }"# + )) + + public var workspaceId: String + public var id: ID + + public init( + workspaceId: String, + id: ID + ) { + self.workspaceId = workspaceId + self.id = id + } + + public var __variables: Variables? { [ + "workspaceId": workspaceId, + "id": id + ] } + + public struct Data: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Mutation } + public static var __selections: [ApolloAPI.Selection] { [ + .field("deleteWorkspaceByokConfig", Bool.self, arguments: [ + "workspaceId": .variable("workspaceId"), + "id": .variable("id") + ]), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + DeleteWorkspaceByokConfigMutation.Data.self + ] } + + public var deleteWorkspaceByokConfig: Bool { __data["deleteWorkspaceByokConfig"] } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/InviteByEmailsMutation.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/InviteByEmailsMutation.graphql.swift index 3a58cd7361..3dc7a7d5ff 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/InviteByEmailsMutation.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/InviteByEmailsMutation.graphql.swift @@ -7,7 +7,7 @@ public class InviteByEmailsMutation: GraphQLMutation { public static let operationName: String = "inviteByEmails" public static let operationDocument: ApolloAPI.OperationDocument = .init( definition: .init( - #"mutation inviteByEmails($workspaceId: String!, $emails: [String!]!) { inviteMembers(workspaceId: $workspaceId, emails: $emails) { __typename email inviteId sentSuccess } }"# + #"mutation inviteByEmails($workspaceId: String!, $emails: [String!]!) { inviteMembers(workspaceId: $workspaceId, emails: $emails) { __typename email inviteId } }"# )) public var workspaceId: String @@ -55,7 +55,6 @@ public class InviteByEmailsMutation: GraphQLMutation { .field("__typename", String.self), .field("email", String.self), .field("inviteId", String?.self), - .field("sentSuccess", Bool.self), ] } public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ InviteByEmailsMutation.Data.InviteMember.self @@ -64,9 +63,6 @@ public class InviteByEmailsMutation: GraphQLMutation { public var email: String { __data["email"] } /// Invite id, null if invite record create failed public var inviteId: String? { __data["inviteId"] } - /// Invite email sent success - @available(*, deprecated, message: "Notification will be sent asynchronously") - public var sentSuccess: Bool { __data["sentSuccess"] } } } } diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/PreviewLicenseMutation.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/PreviewLicenseMutation.graphql.swift new file mode 100644 index 0000000000..d61645809f --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/PreviewLicenseMutation.graphql.swift @@ -0,0 +1,74 @@ +// @generated +// This file was automatically generated and should not be edited. + +@_exported import ApolloAPI + +public class PreviewLicenseMutation: GraphQLMutation { + public static let operationName: String = "previewLicense" + public static let operationDocument: ApolloAPI.OperationDocument = .init( + definition: .init( + #"mutation previewLicense($license: Upload!) { previewLicense(license: $license) { __typename id workspaceId plan recurring quantity issuedAt expiresAt endAt entity issuer valid } }"# + )) + + public var license: Upload + + public init(license: Upload) { + self.license = license + } + + public var __variables: Variables? { ["license": license] } + + public struct Data: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Mutation } + public static var __selections: [ApolloAPI.Selection] { [ + .field("previewLicense", PreviewLicense.self, arguments: ["license": .variable("license")]), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + PreviewLicenseMutation.Data.self + ] } + + public var previewLicense: PreviewLicense { __data["previewLicense"] } + + /// PreviewLicense + /// + /// Parent Type: `AdminLicensePreview` + public struct PreviewLicense: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.AdminLicensePreview } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("id", String.self), + .field("workspaceId", String.self), + .field("plan", GraphQLEnum.self), + .field("recurring", GraphQLEnum.self), + .field("quantity", Int.self), + .field("issuedAt", AffineGraphQL.DateTime.self), + .field("expiresAt", AffineGraphQL.DateTime.self), + .field("endAt", AffineGraphQL.DateTime.self), + .field("entity", String.self), + .field("issuer", String.self), + .field("valid", Bool.self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + PreviewLicenseMutation.Data.PreviewLicense.self + ] } + + public var id: String { __data["id"] } + public var workspaceId: String { __data["workspaceId"] } + public var plan: GraphQLEnum { __data["plan"] } + public var recurring: GraphQLEnum { __data["recurring"] } + public var quantity: Int { __data["quantity"] } + public var issuedAt: AffineGraphQL.DateTime { __data["issuedAt"] } + public var expiresAt: AffineGraphQL.DateTime { __data["expiresAt"] } + public var endAt: AffineGraphQL.DateTime { __data["endAt"] } + public var entity: String { __data["entity"] } + public var issuer: String { __data["issuer"] } + public var valid: Bool { __data["valid"] } + } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/ReorderWorkspaceByokConfigsMutation.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/ReorderWorkspaceByokConfigsMutation.graphql.swift new file mode 100644 index 0000000000..a37e504385 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/ReorderWorkspaceByokConfigsMutation.graphql.swift @@ -0,0 +1,56 @@ +// @generated +// This file was automatically generated and should not be edited. + +@_exported import ApolloAPI + +public class ReorderWorkspaceByokConfigsMutation: GraphQLMutation { + public static let operationName: String = "reorderWorkspaceByokConfigs" + public static let operationDocument: ApolloAPI.OperationDocument = .init( + definition: .init( + #"mutation reorderWorkspaceByokConfigs($input: ReorderWorkspaceByokConfigsInput!) { reorderWorkspaceByokConfigs(input: $input) { __typename id sortOrder } }"# + )) + + public var input: ReorderWorkspaceByokConfigsInput + + public init(input: ReorderWorkspaceByokConfigsInput) { + self.input = input + } + + public var __variables: Variables? { ["input": input] } + + public struct Data: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Mutation } + public static var __selections: [ApolloAPI.Selection] { [ + .field("reorderWorkspaceByokConfigs", [ReorderWorkspaceByokConfig].self, arguments: ["input": .variable("input")]), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + ReorderWorkspaceByokConfigsMutation.Data.self + ] } + + public var reorderWorkspaceByokConfigs: [ReorderWorkspaceByokConfig] { __data["reorderWorkspaceByokConfigs"] } + + /// ReorderWorkspaceByokConfig + /// + /// Parent Type: `WorkspaceByokKeyConfigType` + public struct ReorderWorkspaceByokConfig: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.WorkspaceByokKeyConfigType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("id", AffineGraphQL.ID.self), + .field("sortOrder", AffineGraphQL.SafeInt.self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + ReorderWorkspaceByokConfigsMutation.Data.ReorderWorkspaceByokConfig.self + ] } + + public var id: AffineGraphQL.ID { __data["id"] } + public var sortOrder: AffineGraphQL.SafeInt { __data["sortOrder"] } + } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/RetryTranscriptTaskMutation.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/RetryTranscriptTaskMutation.graphql.swift new file mode 100644 index 0000000000..d940a4f118 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/RetryTranscriptTaskMutation.graphql.swift @@ -0,0 +1,67 @@ +// @generated +// This file was automatically generated and should not be edited. + +@_exported import ApolloAPI + +public class RetryTranscriptTaskMutation: GraphQLMutation { + public static let operationName: String = "retryTranscriptTask" + public static let operationDocument: ApolloAPI.OperationDocument = .init( + definition: .init( + #"mutation retryTranscriptTask($workspaceId: String!, $taskId: String!) { retryTranscriptTask(workspaceId: $workspaceId, taskId: $taskId) { __typename id status } }"# + )) + + public var workspaceId: String + public var taskId: String + + public init( + workspaceId: String, + taskId: String + ) { + self.workspaceId = workspaceId + self.taskId = taskId + } + + public var __variables: Variables? { [ + "workspaceId": workspaceId, + "taskId": taskId + ] } + + public struct Data: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Mutation } + public static var __selections: [ApolloAPI.Selection] { [ + .field("retryTranscriptTask", RetryTranscriptTask?.self, arguments: [ + "workspaceId": .variable("workspaceId"), + "taskId": .variable("taskId") + ]), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + RetryTranscriptTaskMutation.Data.self + ] } + + public var retryTranscriptTask: RetryTranscriptTask? { __data["retryTranscriptTask"] } + + /// RetryTranscriptTask + /// + /// Parent Type: `TranscriptionResultType` + public struct RetryTranscriptTask: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.TranscriptionResultType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("id", AffineGraphQL.ID.self), + .field("status", GraphQLEnum.self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + RetryTranscriptTaskMutation.Data.RetryTranscriptTask.self + ] } + + public var id: AffineGraphQL.ID { __data["id"] } + public var status: GraphQLEnum { __data["status"] } + } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/SendTestEmailMutation.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/SendTestEmailMutation.graphql.swift index 8012289837..602c24fb4b 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/SendTestEmailMutation.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/SendTestEmailMutation.graphql.swift @@ -7,9 +7,10 @@ public class SendTestEmailMutation: GraphQLMutation { public static let operationName: String = "sendTestEmail" public static let operationDocument: ApolloAPI.OperationDocument = .init( definition: .init( - #"mutation sendTestEmail($host: String!, $port: Int!, $sender: String!, $username: String!, $password: String!, $ignoreTLS: Boolean!) { sendTestEmail( config: { host: $host port: $port sender: $sender username: $username password: $password ignoreTLS: $ignoreTLS } ) }"# + #"mutation sendTestEmail($name: String!, $host: String!, $port: Int!, $sender: String!, $username: String!, $password: String!, $ignoreTLS: Boolean!) { sendTestEmail( config: { name: $name host: $host port: $port sender: $sender username: $username password: $password ignoreTLS: $ignoreTLS } ) }"# )) + public var name: String public var host: String public var port: Int public var sender: String @@ -18,6 +19,7 @@ public class SendTestEmailMutation: GraphQLMutation { public var ignoreTLS: Bool public init( + name: String, host: String, port: Int, sender: String, @@ -25,6 +27,7 @@ public class SendTestEmailMutation: GraphQLMutation { password: String, ignoreTLS: Bool ) { + self.name = name self.host = host self.port = port self.sender = sender @@ -34,6 +37,7 @@ public class SendTestEmailMutation: GraphQLMutation { } public var __variables: Variables? { [ + "name": name, "host": host, "port": port, "sender": sender, @@ -49,6 +53,7 @@ public class SendTestEmailMutation: GraphQLMutation { public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Mutation } public static var __selections: [ApolloAPI.Selection] { [ .field("sendTestEmail", Bool.self, arguments: ["config": [ + "name": .variable("name"), "host": .variable("host"), "port": .variable("port"), "sender": .variable("sender"), diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/SettleTranscriptTaskMutation.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/SettleTranscriptTaskMutation.graphql.swift new file mode 100644 index 0000000000..a06babd915 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/SettleTranscriptTaskMutation.graphql.swift @@ -0,0 +1,274 @@ +// @generated +// This file was automatically generated and should not be edited. + +@_exported import ApolloAPI + +public class SettleTranscriptTaskMutation: GraphQLMutation { + public static let operationName: String = "settleTranscriptTask" + public static let operationDocument: ApolloAPI.OperationDocument = .init( + definition: .init( + #"mutation settleTranscriptTask($workspaceId: String!, $taskId: String!) { settleTranscriptTask(workspaceId: $workspaceId, taskId: $taskId) { __typename id status title summary actions sourceAudio { __typename blobId mimeType durationMs sampleRate channels } quality { __typename degraded overflowCount } sliceManifest { __typename index fileName mimeType startSec durationSec byteSize } normalizedSegments { __typename speaker startSec endSec start end text } normalizedTranscript summaryJson { __typename title durationMinutes attendees keyPoints actionItems { __typename description owner deadline } decisions openQuestions blockers } transcription { __typename speaker start end transcription } } }"# + )) + + public var workspaceId: String + public var taskId: String + + public init( + workspaceId: String, + taskId: String + ) { + self.workspaceId = workspaceId + self.taskId = taskId + } + + public var __variables: Variables? { [ + "workspaceId": workspaceId, + "taskId": taskId + ] } + + public struct Data: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Mutation } + public static var __selections: [ApolloAPI.Selection] { [ + .field("settleTranscriptTask", SettleTranscriptTask?.self, arguments: [ + "workspaceId": .variable("workspaceId"), + "taskId": .variable("taskId") + ]), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + SettleTranscriptTaskMutation.Data.self + ] } + + public var settleTranscriptTask: SettleTranscriptTask? { __data["settleTranscriptTask"] } + + /// SettleTranscriptTask + /// + /// Parent Type: `TranscriptionResultType` + public struct SettleTranscriptTask: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.TranscriptionResultType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("id", AffineGraphQL.ID.self), + .field("status", GraphQLEnum.self), + .field("title", String?.self), + .field("summary", String?.self), + .field("actions", String?.self), + .field("sourceAudio", SourceAudio?.self), + .field("quality", Quality?.self), + .field("sliceManifest", [SliceManifest]?.self), + .field("normalizedSegments", [NormalizedSegment]?.self), + .field("normalizedTranscript", String?.self), + .field("summaryJson", SummaryJson?.self), + .field("transcription", [Transcription]?.self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + SettleTranscriptTaskMutation.Data.SettleTranscriptTask.self + ] } + + public var id: AffineGraphQL.ID { __data["id"] } + public var status: GraphQLEnum { __data["status"] } + public var title: String? { __data["title"] } + public var summary: String? { __data["summary"] } + public var actions: String? { __data["actions"] } + public var sourceAudio: SourceAudio? { __data["sourceAudio"] } + public var quality: Quality? { __data["quality"] } + public var sliceManifest: [SliceManifest]? { __data["sliceManifest"] } + public var normalizedSegments: [NormalizedSegment]? { __data["normalizedSegments"] } + public var normalizedTranscript: String? { __data["normalizedTranscript"] } + public var summaryJson: SummaryJson? { __data["summaryJson"] } + public var transcription: [Transcription]? { __data["transcription"] } + + /// SettleTranscriptTask.SourceAudio + /// + /// Parent Type: `TranscriptionSourceAudioType` + public struct SourceAudio: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.TranscriptionSourceAudioType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("blobId", String?.self), + .field("mimeType", String?.self), + .field("durationMs", Int?.self), + .field("sampleRate", Int?.self), + .field("channels", Int?.self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + SettleTranscriptTaskMutation.Data.SettleTranscriptTask.SourceAudio.self + ] } + + public var blobId: String? { __data["blobId"] } + public var mimeType: String? { __data["mimeType"] } + public var durationMs: Int? { __data["durationMs"] } + public var sampleRate: Int? { __data["sampleRate"] } + public var channels: Int? { __data["channels"] } + } + + /// SettleTranscriptTask.Quality + /// + /// Parent Type: `TranscriptionQualityType` + public struct Quality: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.TranscriptionQualityType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("degraded", Bool?.self), + .field("overflowCount", Int?.self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + SettleTranscriptTaskMutation.Data.SettleTranscriptTask.Quality.self + ] } + + public var degraded: Bool? { __data["degraded"] } + public var overflowCount: Int? { __data["overflowCount"] } + } + + /// SettleTranscriptTask.SliceManifest + /// + /// Parent Type: `AudioSliceManifestItemType` + public struct SliceManifest: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.AudioSliceManifestItemType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("index", Int.self), + .field("fileName", String.self), + .field("mimeType", String.self), + .field("startSec", Double.self), + .field("durationSec", Double.self), + .field("byteSize", Int?.self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + SettleTranscriptTaskMutation.Data.SettleTranscriptTask.SliceManifest.self + ] } + + public var index: Int { __data["index"] } + public var fileName: String { __data["fileName"] } + public var mimeType: String { __data["mimeType"] } + public var startSec: Double { __data["startSec"] } + public var durationSec: Double { __data["durationSec"] } + public var byteSize: Int? { __data["byteSize"] } + } + + /// SettleTranscriptTask.NormalizedSegment + /// + /// Parent Type: `NormalizedTranscriptSegmentType` + public struct NormalizedSegment: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.NormalizedTranscriptSegmentType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("speaker", String.self), + .field("startSec", Double.self), + .field("endSec", Double.self), + .field("start", String.self), + .field("end", String.self), + .field("text", String.self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + SettleTranscriptTaskMutation.Data.SettleTranscriptTask.NormalizedSegment.self + ] } + + public var speaker: String { __data["speaker"] } + public var startSec: Double { __data["startSec"] } + public var endSec: Double { __data["endSec"] } + public var start: String { __data["start"] } + public var end: String { __data["end"] } + public var text: String { __data["text"] } + } + + /// SettleTranscriptTask.SummaryJson + /// + /// Parent Type: `MeetingSummaryV2Type` + public struct SummaryJson: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.MeetingSummaryV2Type } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("title", String.self), + .field("durationMinutes", Double.self), + .field("attendees", [String].self), + .field("keyPoints", [String].self), + .field("actionItems", [ActionItem].self), + .field("decisions", [String].self), + .field("openQuestions", [String].self), + .field("blockers", [String].self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + SettleTranscriptTaskMutation.Data.SettleTranscriptTask.SummaryJson.self + ] } + + public var title: String { __data["title"] } + public var durationMinutes: Double { __data["durationMinutes"] } + public var attendees: [String] { __data["attendees"] } + public var keyPoints: [String] { __data["keyPoints"] } + public var actionItems: [ActionItem] { __data["actionItems"] } + public var decisions: [String] { __data["decisions"] } + public var openQuestions: [String] { __data["openQuestions"] } + public var blockers: [String] { __data["blockers"] } + + /// SettleTranscriptTask.SummaryJson.ActionItem + /// + /// Parent Type: `MeetingActionItemType` + public struct ActionItem: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.MeetingActionItemType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("description", String.self), + .field("owner", String?.self), + .field("deadline", String?.self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + SettleTranscriptTaskMutation.Data.SettleTranscriptTask.SummaryJson.ActionItem.self + ] } + + public var description: String { __data["description"] } + public var owner: String? { __data["owner"] } + public var deadline: String? { __data["deadline"] } + } + } + + /// SettleTranscriptTask.Transcription + /// + /// Parent Type: `TranscriptionItemType` + public struct Transcription: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.TranscriptionItemType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("speaker", String.self), + .field("start", String.self), + .field("end", String.self), + .field("transcription", String.self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + SettleTranscriptTaskMutation.Data.SettleTranscriptTask.Transcription.self + ] } + + public var speaker: String { __data["speaker"] } + public var start: String { __data["start"] } + public var end: String { __data["end"] } + public var transcription: String { __data["transcription"] } + } + } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/SubmitTranscriptTaskMutation.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/SubmitTranscriptTaskMutation.graphql.swift new file mode 100644 index 0000000000..bb85e516b2 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/SubmitTranscriptTaskMutation.graphql.swift @@ -0,0 +1,82 @@ +// @generated +// This file was automatically generated and should not be edited. + +@_exported import ApolloAPI + +public class SubmitTranscriptTaskMutation: GraphQLMutation { + public static let operationName: String = "submitTranscriptTask" + public static let operationDocument: ApolloAPI.OperationDocument = .init( + definition: .init( + #"mutation submitTranscriptTask($workspaceId: String!, $blobId: String!, $blob: Upload, $blobs: [Upload!], $input: SubmitAudioTranscriptionInput) { submitTranscriptTask( blob: $blob blobs: $blobs blobId: $blobId workspaceId: $workspaceId input: $input ) { __typename id status } }"# + )) + + public var workspaceId: String + public var blobId: String + public var blob: GraphQLNullable + public var blobs: GraphQLNullable<[Upload]> + public var input: GraphQLNullable + + public init( + workspaceId: String, + blobId: String, + blob: GraphQLNullable, + blobs: GraphQLNullable<[Upload]>, + input: GraphQLNullable + ) { + self.workspaceId = workspaceId + self.blobId = blobId + self.blob = blob + self.blobs = blobs + self.input = input + } + + public var __variables: Variables? { [ + "workspaceId": workspaceId, + "blobId": blobId, + "blob": blob, + "blobs": blobs, + "input": input + ] } + + public struct Data: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Mutation } + public static var __selections: [ApolloAPI.Selection] { [ + .field("submitTranscriptTask", SubmitTranscriptTask?.self, arguments: [ + "blob": .variable("blob"), + "blobs": .variable("blobs"), + "blobId": .variable("blobId"), + "workspaceId": .variable("workspaceId"), + "input": .variable("input") + ]), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + SubmitTranscriptTaskMutation.Data.self + ] } + + public var submitTranscriptTask: SubmitTranscriptTask? { __data["submitTranscriptTask"] } + + /// SubmitTranscriptTask + /// + /// Parent Type: `TranscriptionResultType` + public struct SubmitTranscriptTask: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.TranscriptionResultType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("id", AffineGraphQL.ID.self), + .field("status", GraphQLEnum.self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + SubmitTranscriptTaskMutation.Data.SubmitTranscriptTask.self + ] } + + public var id: AffineGraphQL.ID { __data["id"] } + public var status: GraphQLEnum { __data["status"] } + } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/TestWorkspaceByokConfigMutation.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/TestWorkspaceByokConfigMutation.graphql.swift new file mode 100644 index 0000000000..857cc4cf7c --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/TestWorkspaceByokConfigMutation.graphql.swift @@ -0,0 +1,58 @@ +// @generated +// This file was automatically generated and should not be edited. + +@_exported import ApolloAPI + +public class TestWorkspaceByokConfigMutation: GraphQLMutation { + public static let operationName: String = "testWorkspaceByokConfig" + public static let operationDocument: ApolloAPI.OperationDocument = .init( + definition: .init( + #"mutation testWorkspaceByokConfig($input: TestWorkspaceByokConfigInput!) { testWorkspaceByokConfig(input: $input) { __typename ok status message } }"# + )) + + public var input: TestWorkspaceByokConfigInput + + public init(input: TestWorkspaceByokConfigInput) { + self.input = input + } + + public var __variables: Variables? { ["input": input] } + + public struct Data: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Mutation } + public static var __selections: [ApolloAPI.Selection] { [ + .field("testWorkspaceByokConfig", TestWorkspaceByokConfig.self, arguments: ["input": .variable("input")]), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + TestWorkspaceByokConfigMutation.Data.self + ] } + + public var testWorkspaceByokConfig: TestWorkspaceByokConfig { __data["testWorkspaceByokConfig"] } + + /// TestWorkspaceByokConfig + /// + /// Parent Type: `TestWorkspaceByokConfigResultType` + public struct TestWorkspaceByokConfig: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.TestWorkspaceByokConfigResultType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("ok", Bool.self), + .field("status", GraphQLEnum.self), + .field("message", String?.self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + TestWorkspaceByokConfigMutation.Data.TestWorkspaceByokConfig.self + ] } + + public var ok: Bool { __data["ok"] } + public var status: GraphQLEnum { __data["status"] } + public var message: String? { __data["message"] } + } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/UpsertWorkspaceByokConfigMutation.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/UpsertWorkspaceByokConfigMutation.graphql.swift new file mode 100644 index 0000000000..34be8b91a2 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/UpsertWorkspaceByokConfigMutation.graphql.swift @@ -0,0 +1,54 @@ +// @generated +// This file was automatically generated and should not be edited. + +@_exported import ApolloAPI + +public class UpsertWorkspaceByokConfigMutation: GraphQLMutation { + public static let operationName: String = "upsertWorkspaceByokConfig" + public static let operationDocument: ApolloAPI.OperationDocument = .init( + definition: .init( + #"mutation upsertWorkspaceByokConfig($input: UpsertWorkspaceByokConfigInput!) { upsertWorkspaceByokConfig(input: $input) { __typename id } }"# + )) + + public var input: UpsertWorkspaceByokConfigInput + + public init(input: UpsertWorkspaceByokConfigInput) { + self.input = input + } + + public var __variables: Variables? { ["input": input] } + + public struct Data: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Mutation } + public static var __selections: [ApolloAPI.Selection] { [ + .field("upsertWorkspaceByokConfig", UpsertWorkspaceByokConfig.self, arguments: ["input": .variable("input")]), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + UpsertWorkspaceByokConfigMutation.Data.self + ] } + + public var upsertWorkspaceByokConfig: UpsertWorkspaceByokConfig { __data["upsertWorkspaceByokConfig"] } + + /// UpsertWorkspaceByokConfig + /// + /// Parent Type: `WorkspaceByokKeyConfigType` + public struct UpsertWorkspaceByokConfig: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.WorkspaceByokKeyConfigType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("id", AffineGraphQL.ID.self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + UpsertWorkspaceByokConfigMutation.Data.UpsertWorkspaceByokConfig.self + ] } + + public var id: AffineGraphQL.ID { __data["id"] } + } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetTranscriptTaskQuery.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetTranscriptTaskQuery.graphql.swift new file mode 100644 index 0000000000..0a89b2a72b --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetTranscriptTaskQuery.graphql.swift @@ -0,0 +1,316 @@ +// @generated +// This file was automatically generated and should not be edited. + +@_exported import ApolloAPI + +public class GetTranscriptTaskQuery: GraphQLQuery { + public static let operationName: String = "getTranscriptTask" + public static let operationDocument: ApolloAPI.OperationDocument = .init( + definition: .init( + #"query getTranscriptTask($workspaceId: String!, $taskId: String, $blobId: String) { currentUser { __typename copilot(workspaceId: $workspaceId) { __typename transcriptTask(taskId: $taskId, blobId: $blobId) { __typename id status title summary sourceAudio { __typename blobId mimeType durationMs sampleRate channels } quality { __typename degraded overflowCount } sliceManifest { __typename index fileName mimeType startSec durationSec byteSize } normalizedSegments { __typename speaker startSec endSec start end text } normalizedTranscript summaryJson { __typename title durationMinutes attendees keyPoints actionItems { __typename description owner deadline } decisions openQuestions blockers } transcription { __typename speaker start end transcription } } } } }"# + )) + + public var workspaceId: String + public var taskId: GraphQLNullable + public var blobId: GraphQLNullable + + public init( + workspaceId: String, + taskId: GraphQLNullable, + blobId: GraphQLNullable + ) { + self.workspaceId = workspaceId + self.taskId = taskId + self.blobId = blobId + } + + public var __variables: Variables? { [ + "workspaceId": workspaceId, + "taskId": taskId, + "blobId": blobId + ] } + + public struct Data: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Query } + public static var __selections: [ApolloAPI.Selection] { [ + .field("currentUser", CurrentUser?.self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + GetTranscriptTaskQuery.Data.self + ] } + + /// Get current user + public var currentUser: CurrentUser? { __data["currentUser"] } + + /// CurrentUser + /// + /// Parent Type: `UserType` + public struct CurrentUser: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.UserType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("copilot", Copilot.self, arguments: ["workspaceId": .variable("workspaceId")]), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + GetTranscriptTaskQuery.Data.CurrentUser.self + ] } + + public var copilot: Copilot { __data["copilot"] } + + /// CurrentUser.Copilot + /// + /// Parent Type: `Copilot` + public struct Copilot: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Copilot } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("transcriptTask", TranscriptTask?.self, arguments: [ + "taskId": .variable("taskId"), + "blobId": .variable("blobId") + ]), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + GetTranscriptTaskQuery.Data.CurrentUser.Copilot.self + ] } + + @available(*, deprecated, message: "Use realtime subscription \"copilot.transcript.task.changed\" instead.") + public var transcriptTask: TranscriptTask? { __data["transcriptTask"] } + + /// CurrentUser.Copilot.TranscriptTask + /// + /// Parent Type: `TranscriptionResultType` + public struct TranscriptTask: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.TranscriptionResultType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("id", AffineGraphQL.ID.self), + .field("status", GraphQLEnum.self), + .field("title", String?.self), + .field("summary", String?.self), + .field("sourceAudio", SourceAudio?.self), + .field("quality", Quality?.self), + .field("sliceManifest", [SliceManifest]?.self), + .field("normalizedSegments", [NormalizedSegment]?.self), + .field("normalizedTranscript", String?.self), + .field("summaryJson", SummaryJson?.self), + .field("transcription", [Transcription]?.self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + GetTranscriptTaskQuery.Data.CurrentUser.Copilot.TranscriptTask.self + ] } + + public var id: AffineGraphQL.ID { __data["id"] } + public var status: GraphQLEnum { __data["status"] } + public var title: String? { __data["title"] } + public var summary: String? { __data["summary"] } + public var sourceAudio: SourceAudio? { __data["sourceAudio"] } + public var quality: Quality? { __data["quality"] } + public var sliceManifest: [SliceManifest]? { __data["sliceManifest"] } + public var normalizedSegments: [NormalizedSegment]? { __data["normalizedSegments"] } + public var normalizedTranscript: String? { __data["normalizedTranscript"] } + public var summaryJson: SummaryJson? { __data["summaryJson"] } + public var transcription: [Transcription]? { __data["transcription"] } + + /// CurrentUser.Copilot.TranscriptTask.SourceAudio + /// + /// Parent Type: `TranscriptionSourceAudioType` + public struct SourceAudio: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.TranscriptionSourceAudioType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("blobId", String?.self), + .field("mimeType", String?.self), + .field("durationMs", Int?.self), + .field("sampleRate", Int?.self), + .field("channels", Int?.self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + GetTranscriptTaskQuery.Data.CurrentUser.Copilot.TranscriptTask.SourceAudio.self + ] } + + public var blobId: String? { __data["blobId"] } + public var mimeType: String? { __data["mimeType"] } + public var durationMs: Int? { __data["durationMs"] } + public var sampleRate: Int? { __data["sampleRate"] } + public var channels: Int? { __data["channels"] } + } + + /// CurrentUser.Copilot.TranscriptTask.Quality + /// + /// Parent Type: `TranscriptionQualityType` + public struct Quality: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.TranscriptionQualityType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("degraded", Bool?.self), + .field("overflowCount", Int?.self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + GetTranscriptTaskQuery.Data.CurrentUser.Copilot.TranscriptTask.Quality.self + ] } + + public var degraded: Bool? { __data["degraded"] } + public var overflowCount: Int? { __data["overflowCount"] } + } + + /// CurrentUser.Copilot.TranscriptTask.SliceManifest + /// + /// Parent Type: `AudioSliceManifestItemType` + public struct SliceManifest: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.AudioSliceManifestItemType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("index", Int.self), + .field("fileName", String.self), + .field("mimeType", String.self), + .field("startSec", Double.self), + .field("durationSec", Double.self), + .field("byteSize", Int?.self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + GetTranscriptTaskQuery.Data.CurrentUser.Copilot.TranscriptTask.SliceManifest.self + ] } + + public var index: Int { __data["index"] } + public var fileName: String { __data["fileName"] } + public var mimeType: String { __data["mimeType"] } + public var startSec: Double { __data["startSec"] } + public var durationSec: Double { __data["durationSec"] } + public var byteSize: Int? { __data["byteSize"] } + } + + /// CurrentUser.Copilot.TranscriptTask.NormalizedSegment + /// + /// Parent Type: `NormalizedTranscriptSegmentType` + public struct NormalizedSegment: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.NormalizedTranscriptSegmentType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("speaker", String.self), + .field("startSec", Double.self), + .field("endSec", Double.self), + .field("start", String.self), + .field("end", String.self), + .field("text", String.self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + GetTranscriptTaskQuery.Data.CurrentUser.Copilot.TranscriptTask.NormalizedSegment.self + ] } + + public var speaker: String { __data["speaker"] } + public var startSec: Double { __data["startSec"] } + public var endSec: Double { __data["endSec"] } + public var start: String { __data["start"] } + public var end: String { __data["end"] } + public var text: String { __data["text"] } + } + + /// CurrentUser.Copilot.TranscriptTask.SummaryJson + /// + /// Parent Type: `MeetingSummaryV2Type` + public struct SummaryJson: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.MeetingSummaryV2Type } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("title", String.self), + .field("durationMinutes", Double.self), + .field("attendees", [String].self), + .field("keyPoints", [String].self), + .field("actionItems", [ActionItem].self), + .field("decisions", [String].self), + .field("openQuestions", [String].self), + .field("blockers", [String].self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + GetTranscriptTaskQuery.Data.CurrentUser.Copilot.TranscriptTask.SummaryJson.self + ] } + + public var title: String { __data["title"] } + public var durationMinutes: Double { __data["durationMinutes"] } + public var attendees: [String] { __data["attendees"] } + public var keyPoints: [String] { __data["keyPoints"] } + public var actionItems: [ActionItem] { __data["actionItems"] } + public var decisions: [String] { __data["decisions"] } + public var openQuestions: [String] { __data["openQuestions"] } + public var blockers: [String] { __data["blockers"] } + + /// CurrentUser.Copilot.TranscriptTask.SummaryJson.ActionItem + /// + /// Parent Type: `MeetingActionItemType` + public struct ActionItem: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.MeetingActionItemType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("description", String.self), + .field("owner", String?.self), + .field("deadline", String?.self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + GetTranscriptTaskQuery.Data.CurrentUser.Copilot.TranscriptTask.SummaryJson.ActionItem.self + ] } + + public var description: String { __data["description"] } + public var owner: String? { __data["owner"] } + public var deadline: String? { __data["deadline"] } + } + } + + /// CurrentUser.Copilot.TranscriptTask.Transcription + /// + /// Parent Type: `TranscriptionItemType` + public struct Transcription: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.TranscriptionItemType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("speaker", String.self), + .field("start", String.self), + .field("end", String.self), + .field("transcription", String.self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + GetTranscriptTaskQuery.Data.CurrentUser.Copilot.TranscriptTask.Transcription.self + ] } + + public var speaker: String { __data["speaker"] } + public var start: String { __data["start"] } + public var end: String { __data["end"] } + public var transcription: String { __data["transcription"] } + } + } + } + } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/InvoicesQuery.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/InvoicesQuery.graphql.swift index 1f4d74c5e0..64f83b1c1c 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/InvoicesQuery.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/InvoicesQuery.graphql.swift @@ -7,7 +7,7 @@ public class InvoicesQuery: GraphQLQuery { public static let operationName: String = "invoices" public static let operationDocument: ApolloAPI.OperationDocument = .init( definition: .init( - #"query invoices($take: Int!, $skip: Int!) { currentUser { __typename invoiceCount invoices(take: $take, skip: $skip) { __typename id status currency amount reason lastPaymentError link createdAt } } }"# + #"query invoices($take: Int!, $skip: Int!) { currentUser { __typename invoiceCount invoices(take: $take, skip: $skip) { __typename status currency amount reason lastPaymentError link createdAt } } }"# )) public var take: Int @@ -75,7 +75,6 @@ public class InvoicesQuery: GraphQLQuery { public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.InvoiceType } public static var __selections: [ApolloAPI.Selection] { [ .field("__typename", String.self), - .field("id", String?.self), .field("status", GraphQLEnum.self), .field("currency", String.self), .field("amount", Int.self), @@ -88,8 +87,6 @@ public class InvoicesQuery: GraphQLQuery { InvoicesQuery.Data.CurrentUser.Invoice.self ] } - @available(*, deprecated, message: "removed") - public var id: String? { __data["id"] } public var status: GraphQLEnum { __data["status"] } public var currency: String { __data["currency"] } public var amount: Int { __data["amount"] } diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/WorkspaceByokSettingsQuery.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/WorkspaceByokSettingsQuery.graphql.swift new file mode 100644 index 0000000000..f05ae847ab --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/WorkspaceByokSettingsQuery.graphql.swift @@ -0,0 +1,212 @@ +// @generated +// This file was automatically generated and should not be edited. + +@_exported import ApolloAPI + +public class WorkspaceByokSettingsQuery: GraphQLQuery { + public static let operationName: String = "workspaceByokSettings" + public static let operationDocument: ApolloAPI.OperationDocument = .init( + definition: .init( + #"query workspaceByokSettings($id: String!, $from: DateTime!, $to: DateTime!) { workspace(id: $id) { __typename id byokSettings { __typename workspaceId entitled serverEntitled localEntitled entitlementRequired allowedProviders localStorageSupported customEndpointSupported hasAiPlan keys { __typename id provider name description storage configured enabled endpoint endpointEditable sortOrder capabilities testStatus disabledReason lastTestedAt lastTestError lastUsedAt lastErrorAt lastError } warnings { __typename featureKind reason requiredProviders } } byokUsage(from: $from, to: $to) { __typename date featureKind totalTokens } } }"# + )) + + public var id: String + public var from: DateTime + public var to: DateTime + + public init( + id: String, + from: DateTime, + to: DateTime + ) { + self.id = id + self.from = from + self.to = to + } + + public var __variables: Variables? { [ + "id": id, + "from": from, + "to": to + ] } + + public struct Data: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Query } + public static var __selections: [ApolloAPI.Selection] { [ + .field("workspace", Workspace.self, arguments: ["id": .variable("id")]), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + WorkspaceByokSettingsQuery.Data.self + ] } + + /// Get workspace by id + public var workspace: Workspace { __data["workspace"] } + + /// Workspace + /// + /// Parent Type: `WorkspaceType` + public struct Workspace: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.WorkspaceType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("id", AffineGraphQL.ID.self), + .field("byokSettings", ByokSettings.self), + .field("byokUsage", [ByokUsage].self, arguments: [ + "from": .variable("from"), + "to": .variable("to") + ]), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + WorkspaceByokSettingsQuery.Data.Workspace.self + ] } + + public var id: AffineGraphQL.ID { __data["id"] } + public var byokSettings: ByokSettings { __data["byokSettings"] } + public var byokUsage: [ByokUsage] { __data["byokUsage"] } + + /// Workspace.ByokSettings + /// + /// Parent Type: `WorkspaceByokSettingsType` + public struct ByokSettings: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.WorkspaceByokSettingsType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("workspaceId", String.self), + .field("entitled", Bool.self), + .field("serverEntitled", Bool.self), + .field("localEntitled", Bool.self), + .field("entitlementRequired", [String].self), + .field("allowedProviders", [GraphQLEnum].self), + .field("localStorageSupported", Bool.self), + .field("customEndpointSupported", Bool.self), + .field("hasAiPlan", Bool.self), + .field("keys", [Key].self), + .field("warnings", [Warning].self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + WorkspaceByokSettingsQuery.Data.Workspace.ByokSettings.self + ] } + + public var workspaceId: String { __data["workspaceId"] } + public var entitled: Bool { __data["entitled"] } + public var serverEntitled: Bool { __data["serverEntitled"] } + public var localEntitled: Bool { __data["localEntitled"] } + public var entitlementRequired: [String] { __data["entitlementRequired"] } + public var allowedProviders: [GraphQLEnum] { __data["allowedProviders"] } + public var localStorageSupported: Bool { __data["localStorageSupported"] } + public var customEndpointSupported: Bool { __data["customEndpointSupported"] } + public var hasAiPlan: Bool { __data["hasAiPlan"] } + public var keys: [Key] { __data["keys"] } + public var warnings: [Warning] { __data["warnings"] } + + /// Workspace.ByokSettings.Key + /// + /// Parent Type: `WorkspaceByokKeyConfigType` + public struct Key: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.WorkspaceByokKeyConfigType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("id", AffineGraphQL.ID.self), + .field("provider", GraphQLEnum.self), + .field("name", String.self), + .field("description", String?.self), + .field("storage", GraphQLEnum.self), + .field("configured", Bool.self), + .field("enabled", Bool.self), + .field("endpoint", String?.self), + .field("endpointEditable", Bool.self), + .field("sortOrder", AffineGraphQL.SafeInt.self), + .field("capabilities", [String].self), + .field("testStatus", GraphQLEnum.self), + .field("disabledReason", String?.self), + .field("lastTestedAt", AffineGraphQL.DateTime?.self), + .field("lastTestError", String?.self), + .field("lastUsedAt", AffineGraphQL.DateTime?.self), + .field("lastErrorAt", AffineGraphQL.DateTime?.self), + .field("lastError", String?.self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + WorkspaceByokSettingsQuery.Data.Workspace.ByokSettings.Key.self + ] } + + public var id: AffineGraphQL.ID { __data["id"] } + public var provider: GraphQLEnum { __data["provider"] } + public var name: String { __data["name"] } + public var description: String? { __data["description"] } + public var storage: GraphQLEnum { __data["storage"] } + public var configured: Bool { __data["configured"] } + public var enabled: Bool { __data["enabled"] } + public var endpoint: String? { __data["endpoint"] } + public var endpointEditable: Bool { __data["endpointEditable"] } + public var sortOrder: AffineGraphQL.SafeInt { __data["sortOrder"] } + public var capabilities: [String] { __data["capabilities"] } + public var testStatus: GraphQLEnum { __data["testStatus"] } + public var disabledReason: String? { __data["disabledReason"] } + public var lastTestedAt: AffineGraphQL.DateTime? { __data["lastTestedAt"] } + public var lastTestError: String? { __data["lastTestError"] } + public var lastUsedAt: AffineGraphQL.DateTime? { __data["lastUsedAt"] } + public var lastErrorAt: AffineGraphQL.DateTime? { __data["lastErrorAt"] } + public var lastError: String? { __data["lastError"] } + } + + /// Workspace.ByokSettings.Warning + /// + /// Parent Type: `WorkspaceByokCapabilityWarningType` + public struct Warning: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.WorkspaceByokCapabilityWarningType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("featureKind", String.self), + .field("reason", String.self), + .field("requiredProviders", [GraphQLEnum].self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + WorkspaceByokSettingsQuery.Data.Workspace.ByokSettings.Warning.self + ] } + + public var featureKind: String { __data["featureKind"] } + public var reason: String { __data["reason"] } + public var requiredProviders: [GraphQLEnum] { __data["requiredProviders"] } + } + } + + /// Workspace.ByokUsage + /// + /// Parent Type: `WorkspaceByokUsagePointType` + public struct ByokUsage: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.WorkspaceByokUsagePointType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("date", AffineGraphQL.DateTime.self), + .field("featureKind", String.self), + .field("totalTokens", AffineGraphQL.SafeInt.self), + ] } + public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [ + WorkspaceByokSettingsQuery.Data.Workspace.ByokUsage.self + ] } + + public var date: AffineGraphQL.DateTime { __data["date"] } + public var featureKind: String { __data["featureKind"] } + public var totalTokens: AffineGraphQL.SafeInt { __data["totalTokens"] } + } + } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/WorkspaceInvoicesQuery.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/WorkspaceInvoicesQuery.graphql.swift index 8e5f36367a..f675b0783f 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/WorkspaceInvoicesQuery.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/WorkspaceInvoicesQuery.graphql.swift @@ -7,7 +7,7 @@ public class WorkspaceInvoicesQuery: GraphQLQuery { public static let operationName: String = "workspaceInvoices" public static let operationDocument: ApolloAPI.OperationDocument = .init( definition: .init( - #"query workspaceInvoices($take: Int!, $skip: Int!, $workspaceId: String!) { workspace(id: $workspaceId) { __typename invoiceCount invoices(take: $take, skip: $skip) { __typename id status currency amount reason lastPaymentError link createdAt } } }"# + #"query workspaceInvoices($take: Int!, $skip: Int!, $workspaceId: String!) { workspace(id: $workspaceId) { __typename invoiceCount invoices(take: $take, skip: $skip) { __typename status currency amount reason lastPaymentError link createdAt } } }"# )) public var take: Int @@ -79,7 +79,6 @@ public class WorkspaceInvoicesQuery: GraphQLQuery { public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.InvoiceType } public static var __selections: [ApolloAPI.Selection] { [ .field("__typename", String.self), - .field("id", String?.self), .field("status", GraphQLEnum.self), .field("currency", String.self), .field("amount", Int.self), @@ -92,8 +91,6 @@ public class WorkspaceInvoicesQuery: GraphQLQuery { WorkspaceInvoicesQuery.Data.Workspace.Invoice.self ] } - @available(*, deprecated, message: "removed") - public var id: String? { __data["id"] } public var status: GraphQLEnum { __data["status"] } public var currency: String { __data["currency"] } public var amount: Int { __data["amount"] } diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Enums/ByokKeyStorage.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Enums/ByokKeyStorage.graphql.swift new file mode 100644 index 0000000000..a67918aa74 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Enums/ByokKeyStorage.graphql.swift @@ -0,0 +1,9 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public enum ByokKeyStorage: String, EnumType { + case local = "local" + case server = "server" +} diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Enums/ByokKeyTestStatus.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Enums/ByokKeyTestStatus.graphql.swift new file mode 100644 index 0000000000..10e6ab6700 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Enums/ByokKeyTestStatus.graphql.swift @@ -0,0 +1,10 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public enum ByokKeyTestStatus: String, EnumType { + case failed = "failed" + case passed = "passed" + case untested = "untested" +} diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Enums/ByokProvider.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Enums/ByokProvider.graphql.swift new file mode 100644 index 0000000000..d52c750b73 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Enums/ByokProvider.graphql.swift @@ -0,0 +1,11 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public enum ByokProvider: String, EnumType { + case anthropic = "anthropic" + case fal = "fal" + case gemini = "gemini" + case openai = "openai" +} diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Enums/FeatureType.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Enums/FeatureType.graphql.swift index b606ebb6b6..68b7a58ee5 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Enums/FeatureType.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Enums/FeatureType.graphql.swift @@ -4,9 +4,7 @@ import ApolloAPI public enum FeatureType: String, EnumType { - case aIEarlyAccess = "AIEarlyAccess" case admin = "Admin" - case earlyAccess = "EarlyAccess" case freePlan = "FreePlan" case lifetimeProPlan = "LifetimeProPlan" case proPlan = "ProPlan" diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Enums/SubscriptionVariant.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Enums/SubscriptionVariant.graphql.swift index ca006c8394..90e68a6750 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Enums/SubscriptionVariant.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Enums/SubscriptionVariant.graphql.swift @@ -4,6 +4,5 @@ import ApolloAPI public enum SubscriptionVariant: String, EnumType { - case ea = "EA" case onetime = "Onetime" } diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/CreateChatMessageInput.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/CreateChatMessageInput.graphql.swift index 9d57fe5fdf..9d47155fd7 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/CreateChatMessageInput.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/CreateChatMessageInput.graphql.swift @@ -10,6 +10,23 @@ public struct CreateChatMessageInput: InputObject { __data = data } + public init( + blob: GraphQLNullable = nil, + blobs: GraphQLNullable<[Upload]> = nil, + content: GraphQLNullable = nil, + params: GraphQLNullable = nil, + sessionId: String + ) { + __data = InputDict([ + "blob": blob, + "blobs": blobs, + "content": content, + "params": params, + "sessionId": sessionId + ]) + } + + @available(*, deprecated, message: "Argument 'attachments' is deprecated.") public init( attachments: GraphQLNullable<[String]> = nil, blob: GraphQLNullable = nil, @@ -28,6 +45,7 @@ public struct CreateChatMessageInput: InputObject { ]) } + @available(*, deprecated, message: "use blobs") public var attachments: GraphQLNullable<[String]> { get { __data["attachments"] } set { __data["attachments"] = newValue } diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/CreateCheckoutSessionInput.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/CreateCheckoutSessionInput.graphql.swift index 3d8f65cecc..56c078bedf 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/CreateCheckoutSessionInput.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/CreateCheckoutSessionInput.graphql.swift @@ -10,6 +10,25 @@ public struct CreateCheckoutSessionInput: InputObject { __data = data } + public init( + args: GraphQLNullable = nil, + coupon: GraphQLNullable = nil, + plan: GraphQLNullable> = nil, + recurring: GraphQLNullable> = nil, + successCallbackLink: String, + variant: GraphQLNullable> = nil + ) { + __data = InputDict([ + "args": args, + "coupon": coupon, + "plan": plan, + "recurring": recurring, + "successCallbackLink": successCallbackLink, + "variant": variant + ]) + } + + @available(*, deprecated, message: "Argument 'idempotencyKey' is deprecated.") public init( args: GraphQLNullable = nil, coupon: GraphQLNullable = nil, @@ -40,6 +59,7 @@ public struct CreateCheckoutSessionInput: InputObject { set { __data["coupon"] = newValue } } + @available(*, deprecated, message: "not required anymore") public var idempotencyKey: GraphQLNullable { get { __data["idempotencyKey"] } set { __data["idempotencyKey"] = newValue } diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/CreateWorkspaceByokLocalLeaseInput.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/CreateWorkspaceByokLocalLeaseInput.graphql.swift new file mode 100644 index 0000000000..471de239f0 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/CreateWorkspaceByokLocalLeaseInput.graphql.swift @@ -0,0 +1,32 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public struct CreateWorkspaceByokLocalLeaseInput: InputObject { + public private(set) var __data: InputDict + + public init(_ data: InputDict) { + __data = data + } + + public init( + providers: [CreateWorkspaceByokLocalLeaseProviderInput], + workspaceId: String + ) { + __data = InputDict([ + "providers": providers, + "workspaceId": workspaceId + ]) + } + + public var providers: [CreateWorkspaceByokLocalLeaseProviderInput] { + get { __data["providers"] } + set { __data["providers"] = newValue } + } + + public var workspaceId: String { + get { __data["workspaceId"] } + set { __data["workspaceId"] = newValue } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/CreateWorkspaceByokLocalLeaseProviderInput.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/CreateWorkspaceByokLocalLeaseProviderInput.graphql.swift new file mode 100644 index 0000000000..d049d82256 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/CreateWorkspaceByokLocalLeaseProviderInput.graphql.swift @@ -0,0 +1,67 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public struct CreateWorkspaceByokLocalLeaseProviderInput: InputObject { + public private(set) var __data: InputDict + + public init(_ data: InputDict) { + __data = data + } + + public init( + apiKey: String, + description: GraphQLNullable = nil, + enabled: GraphQLNullable = nil, + endpoint: GraphQLNullable = nil, + name: String, + provider: GraphQLEnum, + sortOrder: GraphQLNullable = nil + ) { + __data = InputDict([ + "apiKey": apiKey, + "description": description, + "enabled": enabled, + "endpoint": endpoint, + "name": name, + "provider": provider, + "sortOrder": sortOrder + ]) + } + + public var apiKey: String { + get { __data["apiKey"] } + set { __data["apiKey"] = newValue } + } + + public var description: GraphQLNullable { + get { __data["description"] } + set { __data["description"] = newValue } + } + + public var enabled: GraphQLNullable { + get { __data["enabled"] } + set { __data["enabled"] = newValue } + } + + public var endpoint: GraphQLNullable { + get { __data["endpoint"] } + set { __data["endpoint"] = newValue } + } + + public var name: String { + get { __data["name"] } + set { __data["name"] = newValue } + } + + public var provider: GraphQLEnum { + get { __data["provider"] } + set { __data["provider"] = newValue } + } + + public var sortOrder: GraphQLNullable { + get { __data["sortOrder"] } + set { __data["sortOrder"] = newValue } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/ReorderWorkspaceByokConfigsInput.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/ReorderWorkspaceByokConfigsInput.graphql.swift new file mode 100644 index 0000000000..db64493001 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/ReorderWorkspaceByokConfigsInput.graphql.swift @@ -0,0 +1,39 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public struct ReorderWorkspaceByokConfigsInput: InputObject { + public private(set) var __data: InputDict + + public init(_ data: InputDict) { + __data = data + } + + public init( + ids: [ID], + storage: GraphQLEnum, + workspaceId: String + ) { + __data = InputDict([ + "ids": ids, + "storage": storage, + "workspaceId": workspaceId + ]) + } + + public var ids: [ID] { + get { __data["ids"] } + set { __data["ids"] = newValue } + } + + public var storage: GraphQLEnum { + get { __data["storage"] } + set { __data["storage"] = newValue } + } + + public var workspaceId: String { + get { __data["workspaceId"] } + set { __data["workspaceId"] = newValue } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/SubmitAudioTranscriptionInput.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/SubmitAudioTranscriptionInput.graphql.swift index 7f3fa49ded..e05bed4c94 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/SubmitAudioTranscriptionInput.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/SubmitAudioTranscriptionInput.graphql.swift @@ -13,12 +13,14 @@ public struct SubmitAudioTranscriptionInput: InputObject { public init( quality: GraphQLNullable = nil, sliceManifest: GraphQLNullable<[AudioSliceManifestItemInput]> = nil, - sourceAudio: GraphQLNullable = nil + sourceAudio: GraphQLNullable = nil, + strategy: GraphQLNullable = nil ) { __data = InputDict([ "quality": quality, "sliceManifest": sliceManifest, - "sourceAudio": sourceAudio + "sourceAudio": sourceAudio, + "strategy": strategy ]) } @@ -36,4 +38,9 @@ public struct SubmitAudioTranscriptionInput: InputObject { get { __data["sourceAudio"] } set { __data["sourceAudio"] = newValue } } + + public var strategy: GraphQLNullable { + get { __data["strategy"] } + set { __data["strategy"] = newValue } + } } diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/TestWorkspaceByokConfigInput.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/TestWorkspaceByokConfigInput.graphql.swift new file mode 100644 index 0000000000..b2fd78d7e9 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/TestWorkspaceByokConfigInput.graphql.swift @@ -0,0 +1,60 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public struct TestWorkspaceByokConfigInput: InputObject { + public private(set) var __data: InputDict + + public init(_ data: InputDict) { + __data = data + } + + public init( + apiKey: GraphQLNullable = nil, + configId: GraphQLNullable = nil, + endpoint: GraphQLNullable = nil, + provider: GraphQLEnum, + storage: GraphQLEnum, + workspaceId: String + ) { + __data = InputDict([ + "apiKey": apiKey, + "configId": configId, + "endpoint": endpoint, + "provider": provider, + "storage": storage, + "workspaceId": workspaceId + ]) + } + + public var apiKey: GraphQLNullable { + get { __data["apiKey"] } + set { __data["apiKey"] = newValue } + } + + public var configId: GraphQLNullable { + get { __data["configId"] } + set { __data["configId"] = newValue } + } + + public var endpoint: GraphQLNullable { + get { __data["endpoint"] } + set { __data["endpoint"] = newValue } + } + + public var provider: GraphQLEnum { + get { __data["provider"] } + set { __data["provider"] = newValue } + } + + public var storage: GraphQLEnum { + get { __data["storage"] } + set { __data["storage"] = newValue } + } + + public var workspaceId: String { + get { __data["workspaceId"] } + set { __data["workspaceId"] = newValue } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/UpsertWorkspaceByokConfigInput.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/UpsertWorkspaceByokConfigInput.graphql.swift new file mode 100644 index 0000000000..6ed2bee3df --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/UpsertWorkspaceByokConfigInput.graphql.swift @@ -0,0 +1,88 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public struct UpsertWorkspaceByokConfigInput: InputObject { + public private(set) var __data: InputDict + + public init(_ data: InputDict) { + __data = data + } + + public init( + apiKey: GraphQLNullable = nil, + description: GraphQLNullable = nil, + enabled: GraphQLNullable = nil, + endpoint: GraphQLNullable = nil, + id: GraphQLNullable = nil, + name: String, + provider: GraphQLEnum, + sortOrder: GraphQLNullable = nil, + storage: GraphQLEnum, + workspaceId: String + ) { + __data = InputDict([ + "apiKey": apiKey, + "description": description, + "enabled": enabled, + "endpoint": endpoint, + "id": id, + "name": name, + "provider": provider, + "sortOrder": sortOrder, + "storage": storage, + "workspaceId": workspaceId + ]) + } + + public var apiKey: GraphQLNullable { + get { __data["apiKey"] } + set { __data["apiKey"] = newValue } + } + + public var description: GraphQLNullable { + get { __data["description"] } + set { __data["description"] = newValue } + } + + public var enabled: GraphQLNullable { + get { __data["enabled"] } + set { __data["enabled"] = newValue } + } + + public var endpoint: GraphQLNullable { + get { __data["endpoint"] } + set { __data["endpoint"] = newValue } + } + + public var id: GraphQLNullable { + get { __data["id"] } + set { __data["id"] = newValue } + } + + public var name: String { + get { __data["name"] } + set { __data["name"] = newValue } + } + + public var provider: GraphQLEnum { + get { __data["provider"] } + set { __data["provider"] = newValue } + } + + public var sortOrder: GraphQLNullable { + get { __data["sortOrder"] } + set { __data["sortOrder"] = newValue } + } + + public var storage: GraphQLEnum { + get { __data["storage"] } + set { __data["storage"] = newValue } + } + + public var workspaceId: String { + get { __data["workspaceId"] } + set { __data["workspaceId"] = newValue } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/GrantedDocUserType.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/AdminLicensePreview.graphql.swift similarity index 66% rename from packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/GrantedDocUserType.graphql.swift rename to packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/AdminLicensePreview.graphql.swift index a4171bc4c0..8a37c4f036 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/GrantedDocUserType.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/AdminLicensePreview.graphql.swift @@ -4,8 +4,8 @@ import ApolloAPI public extension Objects { - static let GrantedDocUserType = ApolloAPI.Object( - typename: "GrantedDocUserType", + static let AdminLicensePreview = ApolloAPI.Object( + typename: "AdminLicensePreview", implementedInterfaces: [], keyFields: nil ) diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/CreateWorkspaceByokLocalLeaseResultType.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/CreateWorkspaceByokLocalLeaseResultType.graphql.swift new file mode 100644 index 0000000000..521553a12d --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/CreateWorkspaceByokLocalLeaseResultType.graphql.swift @@ -0,0 +1,12 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + static let CreateWorkspaceByokLocalLeaseResultType = ApolloAPI.Object( + typename: "CreateWorkspaceByokLocalLeaseResultType", + implementedInterfaces: [], + keyFields: nil + ) +} \ No newline at end of file diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/ContextWorkspaceEmbeddingStatus.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/TestWorkspaceByokConfigResultType.graphql.swift similarity index 60% rename from packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/ContextWorkspaceEmbeddingStatus.graphql.swift rename to packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/TestWorkspaceByokConfigResultType.graphql.swift index b9adafd879..283d8a5389 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/ContextWorkspaceEmbeddingStatus.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/TestWorkspaceByokConfigResultType.graphql.swift @@ -4,8 +4,8 @@ import ApolloAPI public extension Objects { - static let ContextWorkspaceEmbeddingStatus = ApolloAPI.Object( - typename: "ContextWorkspaceEmbeddingStatus", + static let TestWorkspaceByokConfigResultType = ApolloAPI.Object( + typename: "TestWorkspaceByokConfigResultType", implementedInterfaces: [], keyFields: nil ) diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/WorkspaceByokCapabilityWarningType.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/WorkspaceByokCapabilityWarningType.graphql.swift new file mode 100644 index 0000000000..7000188c32 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/WorkspaceByokCapabilityWarningType.graphql.swift @@ -0,0 +1,12 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + static let WorkspaceByokCapabilityWarningType = ApolloAPI.Object( + typename: "WorkspaceByokCapabilityWarningType", + implementedInterfaces: [], + keyFields: nil + ) +} \ No newline at end of file diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/InviteUserType.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/WorkspaceByokKeyConfigType.graphql.swift similarity index 63% rename from packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/InviteUserType.graphql.swift rename to packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/WorkspaceByokKeyConfigType.graphql.swift index e73982ee35..2086215fea 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/InviteUserType.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/WorkspaceByokKeyConfigType.graphql.swift @@ -4,8 +4,8 @@ import ApolloAPI public extension Objects { - static let InviteUserType = ApolloAPI.Object( - typename: "InviteUserType", + static let WorkspaceByokKeyConfigType = ApolloAPI.Object( + typename: "WorkspaceByokKeyConfigType", implementedInterfaces: [], keyFields: nil ) diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/GrantedDocUserTypeEdge.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/WorkspaceByokSettingsType.graphql.swift similarity index 64% rename from packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/GrantedDocUserTypeEdge.graphql.swift rename to packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/WorkspaceByokSettingsType.graphql.swift index 1ec4e04c47..a1a23565cf 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/GrantedDocUserTypeEdge.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/WorkspaceByokSettingsType.graphql.swift @@ -4,8 +4,8 @@ import ApolloAPI public extension Objects { - static let GrantedDocUserTypeEdge = ApolloAPI.Object( - typename: "GrantedDocUserTypeEdge", + static let WorkspaceByokSettingsType = ApolloAPI.Object( + typename: "WorkspaceByokSettingsType", implementedInterfaces: [], keyFields: nil ) diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/PaginatedGrantedDocUserType.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/WorkspaceByokUsagePointType.graphql.swift similarity index 63% rename from packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/PaginatedGrantedDocUserType.graphql.swift rename to packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/WorkspaceByokUsagePointType.graphql.swift index 55d018410a..1bcfb07308 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/PaginatedGrantedDocUserType.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/WorkspaceByokUsagePointType.graphql.swift @@ -4,8 +4,8 @@ import ApolloAPI public extension Objects { - static let PaginatedGrantedDocUserType = ApolloAPI.Object( - typename: "PaginatedGrantedDocUserType", + static let WorkspaceByokUsagePointType = ApolloAPI.Object( + typename: "WorkspaceByokUsagePointType", implementedInterfaces: [], keyFields: nil ) diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/SchemaMetadata.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/SchemaMetadata.graphql.swift index 57d8187cee..2e71aca130 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/SchemaMetadata.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/SchemaMetadata.graphql.swift @@ -24,6 +24,7 @@ public enum SchemaMetadata: ApolloAPI.SchemaMetadata { "AdminDashboard": AffineGraphQL.Objects.AdminDashboard, "AdminDashboardMinutePoint": AffineGraphQL.Objects.AdminDashboardMinutePoint, "AdminDashboardValueDayPoint": AffineGraphQL.Objects.AdminDashboardValueDayPoint, + "AdminLicensePreview": AffineGraphQL.Objects.AdminLicensePreview, "AdminSharedLinkTopItem": AffineGraphQL.Objects.AdminSharedLinkTopItem, "AdminWorkspace": AffineGraphQL.Objects.AdminWorkspace, "AdminWorkspaceMember": AffineGraphQL.Objects.AdminWorkspaceMember, @@ -37,8 +38,7 @@ public enum SchemaMetadata: ApolloAPI.SchemaMetadata { "BlobUploadPart": AffineGraphQL.Objects.BlobUploadPart, "BlobUploadedPart": AffineGraphQL.Objects.BlobUploadedPart, "CalendarAccountObjectType": AffineGraphQL.Objects.CalendarAccountObjectType, - "CalendarCalDAVProviderPresetObjectType": AffineGraphQL.Objects - .CalendarCalDAVProviderPresetObjectType, + "CalendarCalDAVProviderPresetObjectType": AffineGraphQL.Objects.CalendarCalDAVProviderPresetObjectType, "CalendarEventObjectType": AffineGraphQL.Objects.CalendarEventObjectType, "CalendarSubscriptionObjectType": AffineGraphQL.Objects.CalendarSubscriptionObjectType, "ChatMessage": AffineGraphQL.Objects.ChatMessage, @@ -48,7 +48,6 @@ public enum SchemaMetadata: ApolloAPI.SchemaMetadata { "CommentObjectTypeEdge": AffineGraphQL.Objects.CommentObjectTypeEdge, "ContextMatchedDocChunk": AffineGraphQL.Objects.ContextMatchedDocChunk, "ContextMatchedFileChunk": AffineGraphQL.Objects.ContextMatchedFileChunk, - "ContextWorkspaceEmbeddingStatus": AffineGraphQL.Objects.ContextWorkspaceEmbeddingStatus, "Copilot": AffineGraphQL.Objects.Copilot, "CopilotContext": AffineGraphQL.Objects.CopilotContext, "CopilotContextBlob": AffineGraphQL.Objects.CopilotContextBlob, @@ -65,6 +64,7 @@ public enum SchemaMetadata: ApolloAPI.SchemaMetadata { "CopilotWorkspaceFileTypeEdge": AffineGraphQL.Objects.CopilotWorkspaceFileTypeEdge, "CopilotWorkspaceIgnoredDoc": AffineGraphQL.Objects.CopilotWorkspaceIgnoredDoc, "CopilotWorkspaceIgnoredDocTypeEdge": AffineGraphQL.Objects.CopilotWorkspaceIgnoredDocTypeEdge, + "CreateWorkspaceByokLocalLeaseResultType": AffineGraphQL.Objects.CreateWorkspaceByokLocalLeaseResultType, "CredentialsRequirementType": AffineGraphQL.Objects.CredentialsRequirementType, "DeleteAccount": AffineGraphQL.Objects.DeleteAccount, "DocHistoryType": AffineGraphQL.Objects.DocHistoryType, @@ -77,13 +77,10 @@ public enum SchemaMetadata: ApolloAPI.SchemaMetadata { "DocType": AffineGraphQL.Objects.DocType, "DocTypeEdge": AffineGraphQL.Objects.DocTypeEdge, "EditorType": AffineGraphQL.Objects.EditorType, - "GrantedDocUserType": AffineGraphQL.Objects.GrantedDocUserType, - "GrantedDocUserTypeEdge": AffineGraphQL.Objects.GrantedDocUserTypeEdge, "InvitationType": AffineGraphQL.Objects.InvitationType, "InvitationWorkspaceType": AffineGraphQL.Objects.InvitationWorkspaceType, "InviteLink": AffineGraphQL.Objects.InviteLink, "InviteResult": AffineGraphQL.Objects.InviteResult, - "InviteUserType": AffineGraphQL.Objects.InviteUserType, "InvoiceType": AffineGraphQL.Objects.InvoiceType, "License": AffineGraphQL.Objects.License, "LimitedUserType": AffineGraphQL.Objects.LimitedUserType, @@ -102,7 +99,6 @@ public enum SchemaMetadata: ApolloAPI.SchemaMetadata { "PaginatedCopilotWorkspaceFileType": AffineGraphQL.Objects.PaginatedCopilotWorkspaceFileType, "PaginatedDocMemberLastAccess": AffineGraphQL.Objects.PaginatedDocMemberLastAccess, "PaginatedDocType": AffineGraphQL.Objects.PaginatedDocType, - "PaginatedGrantedDocUserType": AffineGraphQL.Objects.PaginatedGrantedDocUserType, "PaginatedIgnoredDocsType": AffineGraphQL.Objects.PaginatedIgnoredDocsType, "PaginatedNotificationObjectType": AffineGraphQL.Objects.PaginatedNotificationObjectType, "PasswordLimitsType": AffineGraphQL.Objects.PasswordLimitsType, @@ -120,6 +116,7 @@ public enum SchemaMetadata: ApolloAPI.SchemaMetadata { "StreamObject": AffineGraphQL.Objects.StreamObject, "SubscriptionPrice": AffineGraphQL.Objects.SubscriptionPrice, "SubscriptionType": AffineGraphQL.Objects.SubscriptionType, + "TestWorkspaceByokConfigResultType": AffineGraphQL.Objects.TestWorkspaceByokConfigResultType, "TimeWindow": AffineGraphQL.Objects.TimeWindow, "TranscriptionItemType": AffineGraphQL.Objects.TranscriptionItemType, "TranscriptionQualityType": AffineGraphQL.Objects.TranscriptionQualityType, @@ -131,6 +128,10 @@ public enum SchemaMetadata: ApolloAPI.SchemaMetadata { "UserQuotaUsageType": AffineGraphQL.Objects.UserQuotaUsageType, "UserSettingsType": AffineGraphQL.Objects.UserSettingsType, "UserType": AffineGraphQL.Objects.UserType, + "WorkspaceByokCapabilityWarningType": AffineGraphQL.Objects.WorkspaceByokCapabilityWarningType, + "WorkspaceByokKeyConfigType": AffineGraphQL.Objects.WorkspaceByokKeyConfigType, + "WorkspaceByokSettingsType": AffineGraphQL.Objects.WorkspaceByokSettingsType, + "WorkspaceByokUsagePointType": AffineGraphQL.Objects.WorkspaceByokUsagePointType, "WorkspaceCalendarItemObjectType": AffineGraphQL.Objects.WorkspaceCalendarItemObjectType, "WorkspaceCalendarObjectType": AffineGraphQL.Objects.WorkspaceCalendarObjectType, "WorkspaceDocMeta": AffineGraphQL.Objects.WorkspaceDocMeta, @@ -140,7 +141,7 @@ public enum SchemaMetadata: ApolloAPI.SchemaMetadata { "WorkspaceRolePermissions": AffineGraphQL.Objects.WorkspaceRolePermissions, "WorkspaceType": AffineGraphQL.Objects.WorkspaceType, "WorkspaceUserType": AffineGraphQL.Objects.WorkspaceUserType, - "tokenType": AffineGraphQL.Objects.TokenType, + "tokenType": AffineGraphQL.Objects.TokenType ] public static func objectType(forTypename typename: String) -> ApolloAPI.Object? { diff --git a/packages/frontend/core/src/blocksuite/ai/runtime/request/action-definitions.ts b/packages/frontend/core/src/blocksuite/ai/runtime/request/action-definitions.ts index c414b36bca..67c931c134 100644 --- a/packages/frontend/core/src/blocksuite/ai/runtime/request/action-definitions.ts +++ b/packages/frontend/core/src/blocksuite/ai/runtime/request/action-definitions.ts @@ -184,7 +184,10 @@ Could you make a new website based on these notes and send back just the html fi ? filterStyleToPromptName.get(options.style) : undefined, actionVersion: 'v1', - buildContent: options => options.input, + buildContent: options => + !options.input && options.attachments + ? `Apply the ${options.style} to this image.` + : options.input, }, processImage: { id: 'processImage', @@ -200,7 +203,10 @@ Could you make a new website based on these notes and send back just the html fi }, responseType: 'image', timeout: 180000, - buildContent: options => options.input, + buildContent: options => + !options.input && options.attachments + ? `Apply ${options.type} processing to this image.` + : options.input, }, generateCaption: textAction('generateCaption', 'Generate a caption'), continueWriting: textAction('continueWriting', 'Continue writing'), diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/ai-plan-card.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/ai-plan-card.tsx index 251d594a8e..1763d082c3 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/ai-plan-card.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/ai-plan-card.tsx @@ -7,7 +7,6 @@ import { useLiveData, useService } from '@toeverything/infra'; import { useEffect, useMemo } from 'react'; import { AICancel, AIResume, AISubscribe } from '../plans/ai/actions'; -import { AIRedeemCodeButton } from '../plans/ai/actions/redeem'; import { CardNameLabelRow } from './card-name-label-row'; import { PaymentMethodUpdater } from './payment-method'; import * as styles from './style.css'; @@ -21,7 +20,6 @@ export const AIPlanCard = ({ onClick }: { onClick: () => void }) => { }, [subscriptionService]); const price = useLiveData(subscriptionService.prices.aiPrice$); const subscription = useLiveData(subscriptionService.subscription.ai$); - const isOnetime = useLiveData(subscriptionService.subscription.isOnetimeAI$); const priceReadable = price?.yearlyAmount ? `$${(price.yearlyAmount / 100).toFixed(2)}` @@ -53,13 +51,13 @@ export const AIPlanCard = ({ onClick }: { onClick: () => void }) => { }), }); } - if ((isOnetime || subscription?.canceledAt) && subscription?.end) { + if (subscription?.canceledAt && subscription?.end) { return t['com.affine.payment.ai.billing-tip.end-at']({ end: i18nTime(subscription.end, { absolute: { accuracy: 'day' } }), }); } return null; - }, [subscription, isOnetime, onClick, t]); + }, [subscription, onClick, t]); if (subscription === null) { return ; @@ -82,9 +80,7 @@ export const AIPlanCard = ({ onClick }: { onClick: () => void }) => {
{price?.yearlyAmount ? ( subscription ? ( - isOnetime ? ( - - ) : subscription.canceledAt ? ( + subscription.canceledAt ? ( ) : ( diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/billing-history.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/billing-history.tsx index 11e822dca6..795eb22a9b 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/billing-history.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/billing-history.tsx @@ -59,8 +59,11 @@ export const BillingHistory = () => { {t['com.affine.payment.billing-setting.no-invoice']()}

) : ( - pageInvoices?.map(invoice => ( - + pageInvoices?.map((invoice, index) => ( + )) )}
@@ -93,7 +96,6 @@ const InvoiceLine = ({ return ( { const proSubscription = useLiveData(subscriptionService.subscription.pro$); const isBeliever = useLiveData(subscriptionService.subscription.isBeliever$); - const isOnetime = useLiveData(subscriptionService.subscription.isOnetimeAI$); const [openCancelModal, setOpenCancelModal] = useState(false); return ( @@ -43,7 +42,7 @@ export const PaymentMethod = () => { > - {isBeliever || isOnetime ? null : proSubscription?.end && + {isBeliever ? null : proSubscription?.end && proSubscription?.canceledAt ? ( { const t = useI18n(); - const subscription = useService(SubscriptionService).subscription; - const isOnetimePro = useLiveData(subscription.isOnetimePro$); - - if (isOnetimePro) { - return ; - } - return (
- )} - /> - ); -}; diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/ai/ai-plan.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/ai/ai-plan.tsx index aac3392559..6b1afe5134 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/ai/ai-plan.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/ai/ai-plan.tsx @@ -5,7 +5,6 @@ import { useLiveData, useService } from '@toeverything/infra'; import { useEffect } from 'react'; import { AICancel, AILogin, AIResume, AISubscribe } from './actions'; -import { AIRedeemCodeButton } from './actions/redeem'; import * as styles from './ai-plan.css'; import { AIPlanLayout } from './layout'; @@ -18,7 +17,6 @@ export const AIPlan = () => { const price = useLiveData(subscriptionService.prices.aiPrice$); const isLoggedIn = useLiveData(authService.session.status$) === 'authenticated'; - const isOnetime = useLiveData(subscriptionService.subscription.isOnetimeAI$); useEffect(() => { subscriptionService.subscription.revalidate(); @@ -54,9 +52,7 @@ export const AIPlan = () => { actionButtons={ isLoggedIn ? ( subscription ? ( - isOnetime ? ( - - ) : subscription.canceledAt ? ( + subscription.canceledAt ? ( ) : ( diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/cloud-plans.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/cloud-plans.tsx index db88da3939..afe660d968 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/cloud-plans.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/cloud-plans.tsx @@ -198,9 +198,6 @@ export const CloudPlans = () => { const prices = useLiveData(subscriptionService.prices.prices$); const loggedIn = useLiveData(authService.session.status$) === 'authenticated'; const proSubscription = useLiveData(subscriptionService.subscription.pro$); - const isOnetimePro = useLiveData( - subscriptionService.subscription.isOnetimePro$ - ); const [recurring, setRecurring] = useState( proSubscription?.recurring ?? SubscriptionRecurring.Yearly @@ -352,7 +349,7 @@ export const CloudPlans = () => { toggle={cloudToggle} scroll={cloudScroll} scrollRef={scrollWrapper as RefObject} - lifetime={isOnetimePro ? null : } + lifetime={} /> ); }; diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/plan-card.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/plan-card.tsx index 83faf16c45..26bdda49ac 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/plan-card.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/plan-card.tsx @@ -12,9 +12,8 @@ import { UrlService } from '@affine/core/modules/url'; import { type CreateCheckoutSessionInput, SubscriptionPlan, - SubscriptionRecurring, + type SubscriptionRecurring, SubscriptionStatus, - SubscriptionVariant, } from '@affine/graphql'; import { Trans, useI18n } from '@affine/i18n'; import { track } from '@affine/track'; @@ -119,7 +118,6 @@ const ActionButton = ({ detail, recurring }: PlanCardProps) => { ); const currentPlan = primarySubscription?.plan ?? SubscriptionPlan.Free; const currentRecurring = primarySubscription?.recurring; - const isOnetime = useLiveData(subscriptionService.subscription.isOnetimePro$); const isFree = detail.plan === SubscriptionPlan.Free; const signUpText = useMemo( @@ -136,9 +134,6 @@ const ActionButton = ({ detail, recurring }: PlanCardProps) => { // else // if team => 'Start 14-day free trial' // if isBeliever => 'Included in Lifetime' - // if onetime - // if free => 'Included in Pro' - // else => 'Redeem Code' // if isCurrent // if canceled => 'Resume' // else => 'Current Plan' @@ -165,17 +160,6 @@ const ActionButton = ({ detail, recurring }: PlanCardProps) => { ); } - // onetime - if (isOnetime) { - return isFree ? ( - - ) : ( - - ); - } - const isCanceled = !!primarySubscription?.canceledAt; const isCurrent = detail.plan === currentPlan && @@ -471,29 +455,3 @@ const ResumeButton = () => { ); }; - -const redeemCodeCheckoutInput = { variant: SubscriptionVariant.Onetime }; -export const RedeemCode = ({ - className, - recurring = SubscriptionRecurring.Yearly, - plan, - children, - ...btnProps -}: ButtonProps & { - recurring?: SubscriptionRecurring; - plan?: SubscriptionPlan; -}) => { - const t = useI18n(); - - return ( - - {children ?? t['com.affine.payment.redeem-code']()} - - ); -}; diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/billing/billing-history.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/billing/billing-history.tsx index cd904f24b8..c5592a5e5f 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/billing/billing-history.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/billing/billing-history.tsx @@ -55,8 +55,11 @@ export const BillingHistory = () => { {t['com.affine.payment.billing-setting.no-invoice']()}

) : ( - pageInvoices?.map(invoice => ( - + pageInvoices?.map((invoice, index) => ( + )) )}
@@ -89,7 +92,6 @@ const InvoiceLine = ({ return ( ((acc, result) => { - if (!result.sentSuccess) { - acc.push(result.email); - } - return acc; - }, []); if (results) { notify({ title: t['com.affine.payment.member.team.invite.notify.title']({ - successCount: ( - uniqueEmails.length - unSuccessInvites.length - ).toString(), - failedCount: unSuccessInvites.length.toString(), + count: results.length.toString(), }), - message: , + message: t['Invitation sent hint'](), }); setOpenInvite(false); membersService.members.revalidate(); @@ -351,27 +342,6 @@ export const CloudWorkspaceMembersPanel = ({ ); }; -const NotifyMessage = ({ - unSuccessInvites, -}: { - unSuccessInvites: string[]; -}) => { - const t = useI18n(); - - if (unSuccessInvites.length === 0) { - return t['Invitation sent hint'](); - } - - return ( -
- {t['com.affine.payment.member.team.invite.notify.fail-message']()} - {unSuccessInvites.map((email, index) => ( -
{email}
- ))} -
- ); -}; - export const MembersPanelFallback = () => { const t = useI18n(); diff --git a/packages/frontend/core/src/desktop/pages/subscribe/index.tsx b/packages/frontend/core/src/desktop/pages/subscribe/index.tsx index 9c2a183f19..47c732a8f2 100644 --- a/packages/frontend/core/src/desktop/pages/subscribe/index.tsx +++ b/packages/frontend/core/src/desktop/pages/subscribe/index.tsx @@ -1,11 +1,7 @@ import { Button, Loading } from '@affine/component'; import { UrlService } from '@affine/core/modules/url'; import { UserFriendlyError } from '@affine/error'; -import { - SubscriptionPlan, - SubscriptionRecurring, - SubscriptionVariant, -} from '@affine/graphql'; +import { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql'; import { track } from '@affine/track'; import { effect, fromPromise, useServices } from '@toeverything/infra'; import { nanoid } from 'nanoid'; @@ -24,7 +20,7 @@ import { container } from './subscribe.css'; interface ProductTriple { plan: SubscriptionPlan; recurring: SubscriptionRecurring; - variant: SubscriptionVariant | null; + variant: null; } const products = { @@ -36,9 +32,6 @@ const products = { 'monthly-team': 'team_monthly', 'yearly-selfhost-team': 'selfhost-team_yearly', 'monthly-selfhost-team': 'selfhost-team_monthly', - 'oneyear-ai': 'ai_yearly_onetime', - 'oneyear-pro': 'pro_yearly_onetime', - 'onemonth-pro': 'pro_monthly_onetime', }; const allowedPlan = { @@ -53,11 +46,6 @@ const allowedRecurring = { lifetime: SubscriptionRecurring.Lifetime, }; -const allowedVariant = { - earlyaccess: SubscriptionVariant.EA, - onetime: SubscriptionVariant.Onetime, -}; - function getProductTriple(searchParams: URLSearchParams): ProductTriple { const triple: ProductTriple = { plan: SubscriptionPlan.Pro, @@ -72,13 +60,10 @@ function getProductTriple(searchParams: URLSearchParams): ProductTriple { let recurring = searchParams.get('recurring') as | keyof typeof allowedRecurring | null; - let variant = searchParams.get('variant') as - | keyof typeof allowedVariant - | null; if (productName && products[productName]) { // @ts-expect-error safe - [plan, recurring, variant] = products[productName].split('_'); + [plan, recurring] = products[productName].split('_'); } if (plan) { @@ -88,9 +73,6 @@ function getProductTriple(searchParams: URLSearchParams): ProductTriple { if (recurring) { triple.recurring = allowedRecurring[recurring]; } - if (variant) { - triple.variant = allowedVariant[variant]; - } return triple; } diff --git a/packages/frontend/core/src/modules/cloud/entities/subscription.ts b/packages/frontend/core/src/modules/cloud/entities/subscription.ts index 50b21b5bbe..d1f2070a7b 100644 --- a/packages/frontend/core/src/modules/cloud/entities/subscription.ts +++ b/packages/frontend/core/src/modules/cloud/entities/subscription.ts @@ -2,7 +2,6 @@ import { SubscriptionPlan, type SubscriptionQuery, SubscriptionRecurring, - SubscriptionVariant, } from '@affine/graphql'; import { catchErrorInto, @@ -44,12 +43,6 @@ export class Subscription extends Entity { isBeliever$ = this.pro$.map( sub => sub?.recurring === SubscriptionRecurring.Lifetime ); - isOnetimePro$ = this.pro$.map( - sub => sub?.variant === SubscriptionVariant.Onetime - ); - isOnetimeAI$ = this.ai$.map( - sub => sub?.variant === SubscriptionVariant.Onetime - ); constructor( private readonly authService: AuthService, diff --git a/packages/frontend/core/src/modules/cloud/entities/user-feature.ts b/packages/frontend/core/src/modules/cloud/entities/user-feature.ts index fc4afabb09..ee6dce836d 100644 --- a/packages/frontend/core/src/modules/cloud/entities/user-feature.ts +++ b/packages/frontend/core/src/modules/cloud/entities/user-feature.ts @@ -23,12 +23,6 @@ export class UserFeature extends Entity { features === null ? null : features?.some(f => f === FeatureType.Admin) ); - isEarlyAccess$ = this.features$.map(features => - features === null - ? null - : features?.some(f => f === FeatureType.EarlyAccess) - ); - isRevalidating$ = new LiveData(false); error$ = new LiveData(null); diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index 1fbc35df35..b1e7056556 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -4208,12 +4208,11 @@ export function useAFFiNEI18N(): { */ ["com.affine.payment.member.team.invite.done"](): string; /** - * `Invitation sent,{{successCount}} successful, {{failedCount}} failed` + * `Invitations sent: {{count}}` */ - ["com.affine.payment.member.team.invite.notify.title"](options: Readonly<{ - successCount: string; - failedCount: string; - }>): string; + ["com.affine.payment.member.team.invite.notify.title"](options: { + readonly count: string; + }): string; /** * `These email addresses have already been invited:` */ diff --git a/packages/frontend/i18n/src/resources/ar.json b/packages/frontend/i18n/src/resources/ar.json index bce2c40e93..a586bfb0e5 100644 --- a/packages/frontend/i18n/src/resources/ar.json +++ b/packages/frontend/i18n/src/resources/ar.json @@ -1325,7 +1325,6 @@ "com.affine.payment.member.team.invite.invite-link": "رابط الدعوة", "com.affine.payment.member.team.invite.link-expiration": "انتهاء صلاحية الرابط", "com.affine.payment.member.team.invite.notify.fail-message": "تمت دعوة عناوين البريد الإلكتروني هذه بالفعل:", - "com.affine.payment.member.team.invite.notify.title": "تم إرسال الدعوة،{{successCount}} ناجحة، {{failedCount}} فاشلة", "com.affine.payment.member.team.invite.send-invites": "إرسال الدعوات", "com.affine.payment.member.team.invite.title": "دعوة أعضاء الفريق", "com.affine.payment.member.team.remove": "إزالة العضو", diff --git a/packages/frontend/i18n/src/resources/ca.json b/packages/frontend/i18n/src/resources/ca.json index 42ded44a4c..434d4f6396 100644 --- a/packages/frontend/i18n/src/resources/ca.json +++ b/packages/frontend/i18n/src/resources/ca.json @@ -1146,7 +1146,6 @@ "com.affine.payment.member.team.invite.invite-link": "Enllaç d'invitació", "com.affine.payment.member.team.invite.link-expiration": "Expiració de l'enllaç", "com.affine.payment.member.team.invite.notify.fail-message": "Aquestes adreces de correu electrònic ja han estat convidades:", - "com.affine.payment.member.team.invite.notify.title": "Invitació enviada, {{successCount}} exitoses, {{failedCount}} fallides", "com.affine.payment.member.team.invite.send-invites": "Envia invitacions", "com.affine.payment.member.team.invite.title": "Convida membres de l'equip", "com.affine.payment.member.team.remove": "Suprimeix membre", diff --git a/packages/frontend/i18n/src/resources/de.json b/packages/frontend/i18n/src/resources/de.json index fde5e4f7e9..5dbdf00107 100644 --- a/packages/frontend/i18n/src/resources/de.json +++ b/packages/frontend/i18n/src/resources/de.json @@ -1044,7 +1044,6 @@ "com.affine.payment.member.team.invite.generate": "Erzeugen", "com.affine.payment.member.team.invite.copy": "Kopieren", "com.affine.payment.member.team.invite.done": "Fertig", - "com.affine.payment.member.team.invite.notify.title": "Einladung gesendet, {{successCount}} erfolgreich, {{failedCount}} fehlgeschlagen", "com.affine.payment.member.team.invite.notify.fail-message": "Diese E-Mail-Adressen wurden bereits eingeladen:", "com.affine.payment.member.team.revoke": "Einladung widerrufen", "com.affine.payment.member.team.approve": "Genehmigen", diff --git a/packages/frontend/i18n/src/resources/el-GR.json b/packages/frontend/i18n/src/resources/el-GR.json index 3557d8c0e3..bd298e240e 100644 --- a/packages/frontend/i18n/src/resources/el-GR.json +++ b/packages/frontend/i18n/src/resources/el-GR.json @@ -1010,7 +1010,6 @@ "com.affine.payment.member.team.invite.generate": "Δημιουργία", "com.affine.payment.member.team.invite.copy": "Αντιγραφή", "com.affine.payment.member.team.invite.done": "Ολοκληρώθηκε", - "com.affine.payment.member.team.invite.notify.title": "Η πρόσκληση στάλθηκε, {{successCount}} επιτυχής, {{failedCount}} απέτυχε", "com.affine.payment.member.team.invite.notify.fail-message": "Αυτές οι διευθύνσεις email έχουν ήδη προσκληθεί:", "com.affine.payment.member.team.revoke": "Ανάκληση πρόσκλησης", "com.affine.payment.member.team.approve": "Έγκριση", diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 302c0cc6e0..a7bb6ed134 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1044,7 +1044,7 @@ "com.affine.payment.member.team.invite.generate": "Generate", "com.affine.payment.member.team.invite.copy": "Copy", "com.affine.payment.member.team.invite.done": "Done", - "com.affine.payment.member.team.invite.notify.title": "Invitation sent,{{successCount}} successful, {{failedCount}} failed", + "com.affine.payment.member.team.invite.notify.title": "Invitations sent: {{count}}", "com.affine.payment.member.team.invite.notify.fail-message": "These email addresses have already been invited:", "com.affine.payment.member.team.revoke": "Revoke invitation", "com.affine.payment.member.team.approve": "Approve", diff --git a/packages/frontend/i18n/src/resources/es.json b/packages/frontend/i18n/src/resources/es.json index 9bd5a7a145..c2a7c404e1 100644 --- a/packages/frontend/i18n/src/resources/es.json +++ b/packages/frontend/i18n/src/resources/es.json @@ -1010,7 +1010,6 @@ "com.affine.payment.member.team.invite.generate": "Generar", "com.affine.payment.member.team.invite.copy": "Copiar", "com.affine.payment.member.team.invite.done": "Hecho", - "com.affine.payment.member.team.invite.notify.title": "Invitación enviada, {{successCount}} exitosas, {{failedCount}} fallidas", "com.affine.payment.member.team.invite.notify.fail-message": "Estas direcciones de correo electrónico ya han sido invitadas:", "com.affine.payment.member.team.revoke": "Revocar invitación", "com.affine.payment.member.team.approve": "Aprobar", diff --git a/packages/frontend/i18n/src/resources/fa.json b/packages/frontend/i18n/src/resources/fa.json index bdc9cccf1e..8daa17a5ef 100644 --- a/packages/frontend/i18n/src/resources/fa.json +++ b/packages/frontend/i18n/src/resources/fa.json @@ -1010,7 +1010,6 @@ "com.affine.payment.member.team.invite.generate": "ایجاد", "com.affine.payment.member.team.invite.copy": "کپی", "com.affine.payment.member.team.invite.done": "انجام شد", - "com.affine.payment.member.team.invite.notify.title": "دعوت ارسال شد، {{successCount}} موفق، {{failedCount}} ناموفق", "com.affine.payment.member.team.invite.notify.fail-message": "این آدرس‌های ایمیل قبلاً دعوت شده‌اند:", "com.affine.payment.member.team.revoke": "دعوت را لغو کنید", "com.affine.payment.member.team.approve": "تأیید", diff --git a/packages/frontend/i18n/src/resources/fr.json b/packages/frontend/i18n/src/resources/fr.json index b3c03dfa1e..839f5ee6d4 100644 --- a/packages/frontend/i18n/src/resources/fr.json +++ b/packages/frontend/i18n/src/resources/fr.json @@ -1027,7 +1027,6 @@ "com.affine.payment.member.team.invite.generate": "Générer", "com.affine.payment.member.team.invite.copy": "Copier", "com.affine.payment.member.team.invite.done": "Terminé", - "com.affine.payment.member.team.invite.notify.title": "Invitation envoyée, {{successCount}} réussies, {{failedCount}} échouées", "com.affine.payment.member.team.invite.notify.fail-message": "Ces adresses e-mail ont déjà été invitées :", "com.affine.payment.member.team.revoke": "Révoquer l'invitation", "com.affine.payment.member.team.approve": "Approuver", diff --git a/packages/frontend/i18n/src/resources/it.json b/packages/frontend/i18n/src/resources/it.json index 4b0198ff7c..051e296a5f 100644 --- a/packages/frontend/i18n/src/resources/it.json +++ b/packages/frontend/i18n/src/resources/it.json @@ -1028,7 +1028,6 @@ "com.affine.payment.member.team.invite.generate": "Genera", "com.affine.payment.member.team.invite.copy": "Copia", "com.affine.payment.member.team.invite.done": "Fatto", - "com.affine.payment.member.team.invite.notify.title": "Invito inviato, {{successCount}} riusciti, {{failedCount}} falliti", "com.affine.payment.member.team.invite.notify.fail-message": "Questi indirizzi email sono già stati invitati:", "com.affine.payment.member.team.revoke": "Revoca invito", "com.affine.payment.member.team.approve": "Approva", diff --git a/packages/frontend/i18n/src/resources/ja.json b/packages/frontend/i18n/src/resources/ja.json index f2ffee2553..3bb50f78d1 100644 --- a/packages/frontend/i18n/src/resources/ja.json +++ b/packages/frontend/i18n/src/resources/ja.json @@ -1010,7 +1010,6 @@ "com.affine.payment.member.team.invite.generate": "生成", "com.affine.payment.member.team.invite.copy": "コピー", "com.affine.payment.member.team.invite.done": "完了", - "com.affine.payment.member.team.invite.notify.title": "招待が送信されました。{{successCount}} が成功し、{{failedCount}} が失敗しました", "com.affine.payment.member.team.invite.notify.fail-message": "これらのメールアドレスはすでに招待されています:", "com.affine.payment.member.team.revoke": "招待を取り消す", "com.affine.payment.member.team.approve": "承認", diff --git a/packages/frontend/i18n/src/resources/kk.json b/packages/frontend/i18n/src/resources/kk.json index 0c139fd831..f8f14e205c 100644 --- a/packages/frontend/i18n/src/resources/kk.json +++ b/packages/frontend/i18n/src/resources/kk.json @@ -1044,7 +1044,6 @@ "com.affine.payment.member.team.invite.generate": "Жаса", "com.affine.payment.member.team.invite.copy": "Көшір", "com.affine.payment.member.team.invite.done": "Дайын", - "com.affine.payment.member.team.invite.notify.title": "Шақыру жіберілді: {{successCount}} сәтті, {{failedCount}} сәтсіз", "com.affine.payment.member.team.invite.notify.fail-message": "Мына электрондық пошта мекенжайлары бұрын шақырылған:", "com.affine.payment.member.team.revoke": "Шақыруды қайтарып ал", "com.affine.payment.member.team.approve": "Мақұлда", diff --git a/packages/frontend/i18n/src/resources/ko.json b/packages/frontend/i18n/src/resources/ko.json index 3957e7d1ed..a6d5573628 100644 --- a/packages/frontend/i18n/src/resources/ko.json +++ b/packages/frontend/i18n/src/resources/ko.json @@ -1016,7 +1016,6 @@ "com.affine.payment.member.team.invite.generate": "생성", "com.affine.payment.member.team.invite.copy": "복사", "com.affine.payment.member.team.invite.done": "완료", - "com.affine.payment.member.team.invite.notify.title": "초대장 전송됨, {{successCount}}건 성공, {{failedCount}}건 실패", "com.affine.payment.member.team.invite.notify.fail-message": "다음 이메일 주소는 이미 초대되었습니다:", "com.affine.payment.member.team.revoke": "초대 취소", "com.affine.payment.member.team.approve": "승인", diff --git a/packages/frontend/i18n/src/resources/pl.json b/packages/frontend/i18n/src/resources/pl.json index 24a94adfe4..f5c5fbba94 100644 --- a/packages/frontend/i18n/src/resources/pl.json +++ b/packages/frontend/i18n/src/resources/pl.json @@ -1028,7 +1028,6 @@ "com.affine.payment.member.team.invite.generate": "Wygeneruj", "com.affine.payment.member.team.invite.copy": "Kopiuj", "com.affine.payment.member.team.invite.done": "Gotowe", - "com.affine.payment.member.team.invite.notify.title": "Zaproszenie wysłane, {{successCount}} udane, {{failedCount}} nieudane", "com.affine.payment.member.team.invite.notify.fail-message": "Te adresy e-mail zostały już zaproszone:", "com.affine.payment.member.team.revoke": "Odwołaj zaproszenie", "com.affine.payment.member.team.approve": "Zatwierdź", diff --git a/packages/frontend/i18n/src/resources/pt-BR.json b/packages/frontend/i18n/src/resources/pt-BR.json index ddc5cbdeaf..73b056ca48 100644 --- a/packages/frontend/i18n/src/resources/pt-BR.json +++ b/packages/frontend/i18n/src/resources/pt-BR.json @@ -1010,7 +1010,6 @@ "com.affine.payment.member.team.invite.generate": "Gerar", "com.affine.payment.member.team.invite.copy": "Copiar", "com.affine.payment.member.team.invite.done": "Concluído", - "com.affine.payment.member.team.invite.notify.title": "Convite enviado, {{successCount}} com sucesso, {{failedCount}} falhou", "com.affine.payment.member.team.invite.notify.fail-message": "Esses endereços de e-mail já foram convidados:", "com.affine.payment.member.team.revoke": "Revogar convite", "com.affine.payment.member.team.approve": "Aprovar", diff --git a/packages/frontend/i18n/src/resources/ru.json b/packages/frontend/i18n/src/resources/ru.json index 734f870e93..07ab5ee328 100644 --- a/packages/frontend/i18n/src/resources/ru.json +++ b/packages/frontend/i18n/src/resources/ru.json @@ -1028,7 +1028,6 @@ "com.affine.payment.member.team.invite.generate": "Создать", "com.affine.payment.member.team.invite.copy": "Копировать", "com.affine.payment.member.team.invite.done": "Готово", - "com.affine.payment.member.team.invite.notify.title": "Приглашение отправлено: {{successCount}} успешно, {{failedCount}} не удалось", "com.affine.payment.member.team.invite.notify.fail-message": "Эти адреса электронной почты уже были приглашены:", "com.affine.payment.member.team.revoke": "Отменить приглашение", "com.affine.payment.member.team.approve": "Одобрить", diff --git a/packages/frontend/i18n/src/resources/sv-SE.json b/packages/frontend/i18n/src/resources/sv-SE.json index a84590f1ba..5fd1951240 100644 --- a/packages/frontend/i18n/src/resources/sv-SE.json +++ b/packages/frontend/i18n/src/resources/sv-SE.json @@ -1013,7 +1013,6 @@ "com.affine.payment.member.team.invite.generate": "Generera", "com.affine.payment.member.team.invite.copy": "Kopiera", "com.affine.payment.member.team.invite.done": "Klar", - "com.affine.payment.member.team.invite.notify.title": "Inbjudan skickad,{{successCount}} lyckades, {{failedCount}} misslyckades", "com.affine.payment.member.team.invite.notify.fail-message": "Dessa e-postadresser har redan blivit inbjudna:", "com.affine.payment.member.team.revoke": "Återkalla inbjudan", "com.affine.payment.member.team.approve": "Godkänn", diff --git a/packages/frontend/i18n/src/resources/tr.json b/packages/frontend/i18n/src/resources/tr.json index 0d709c54c4..3b7b1c83b1 100644 --- a/packages/frontend/i18n/src/resources/tr.json +++ b/packages/frontend/i18n/src/resources/tr.json @@ -1044,7 +1044,6 @@ "com.affine.payment.member.team.invite.generate": "Oluştur", "com.affine.payment.member.team.invite.copy": "Kopyala", "com.affine.payment.member.team.invite.done": "Tamam", - "com.affine.payment.member.team.invite.notify.title": "Davetiye gönderildi,{{successCount}} başarılı, {{failedCount}} başarısız oldu", "com.affine.payment.member.team.invite.notify.fail-message": "Bu e-posta adresleri zaten davet edildi:", "com.affine.payment.member.team.revoke": "Daveti iptal et", "com.affine.payment.member.team.approve": "Onayla", diff --git a/packages/frontend/i18n/src/resources/uk.json b/packages/frontend/i18n/src/resources/uk.json index 11e6bbf0dd..febe0d7c24 100644 --- a/packages/frontend/i18n/src/resources/uk.json +++ b/packages/frontend/i18n/src/resources/uk.json @@ -1010,7 +1010,6 @@ "com.affine.payment.member.team.invite.generate": "Створити", "com.affine.payment.member.team.invite.copy": "Копіювати", "com.affine.payment.member.team.invite.done": "Готово", - "com.affine.payment.member.team.invite.notify.title": "Запрошення надіслано, успішних {{successCount}}, невдалих {{failedCount}}", "com.affine.payment.member.team.invite.notify.fail-message": "Ці електронні адреси вже були запрошені:", "com.affine.payment.member.team.revoke": "Відкликати запрошення", "com.affine.payment.member.team.approve": "Затвердити", diff --git a/packages/frontend/i18n/src/resources/ur.json b/packages/frontend/i18n/src/resources/ur.json index 229bb72bca..082fac3db9 100644 --- a/packages/frontend/i18n/src/resources/ur.json +++ b/packages/frontend/i18n/src/resources/ur.json @@ -1331,7 +1331,6 @@ "com.affine.payment.member.team.invite.invite-link": "لنک کو مدعو کریں۔", "com.affine.payment.member.team.invite.link-expiration": "لنک کی میعاد ختم", "com.affine.payment.member.team.invite.notify.fail-message": "یہ ای میل پتے پہلے ہی مدعو کیے جا چکے ہیں:", - "com.affine.payment.member.team.invite.notify.title": "دعوت نامہ بھیجا گیا،{{successCount}} کامیاب، {{failedCount}} ناکام", "com.affine.payment.member.team.invite.send-invites": "دعوت نامے بھیجیں۔", "com.affine.payment.member.team.invite.title": "ٹیم کے ارکان کو مدعو کریں۔", "com.affine.payment.member.team.remove": "ممبر کو ہٹا دیں۔", diff --git a/packages/frontend/i18n/src/resources/zh-Hans.json b/packages/frontend/i18n/src/resources/zh-Hans.json index 37c7de4c72..aaf84ea868 100644 --- a/packages/frontend/i18n/src/resources/zh-Hans.json +++ b/packages/frontend/i18n/src/resources/zh-Hans.json @@ -1044,7 +1044,7 @@ "com.affine.payment.member.team.invite.generate": "生成", "com.affine.payment.member.team.invite.copy": "复制", "com.affine.payment.member.team.invite.done": "完成", - "com.affine.payment.member.team.invite.notify.title": "邀请已发送,成功 {{successCount}} 条,失败 {{failedCount}} 条", + "com.affine.payment.member.team.invite.notify.title": "邀请已发送 {{count}} 条", "com.affine.payment.member.team.invite.notify.fail-message": "这些电子邮件地址已被邀请:", "com.affine.payment.member.team.revoke": "撤回邀请", "com.affine.payment.member.team.approve": "批准", diff --git a/packages/frontend/i18n/src/resources/zh-Hant.json b/packages/frontend/i18n/src/resources/zh-Hant.json index d735db7697..2feeb7e018 100644 --- a/packages/frontend/i18n/src/resources/zh-Hant.json +++ b/packages/frontend/i18n/src/resources/zh-Hant.json @@ -1017,7 +1017,7 @@ "com.affine.payment.member.team.invite.generate": "生成", "com.affine.payment.member.team.invite.copy": "複製", "com.affine.payment.member.team.invite.done": "完成", - "com.affine.payment.member.team.invite.notify.title": "邀請已發送,成功 {{successCount}} 條,失敗 {{failedCount}} 條", + "com.affine.payment.member.team.invite.notify.title": "邀請已發送 {{count}} 條", "com.affine.payment.member.team.invite.notify.fail-message": "這些電子郵件地址已被邀請:", "com.affine.payment.member.team.revoke": "撤銷邀請", "com.affine.payment.member.team.approve": "批准",