chore: cleanup legacy logic (#15072)

This commit is contained in:
DarkSky authored and GitHub committed 2026-06-03 16:20:15 +08:00
1 parent 8c0e1ba04e
commit 81760fd45c
142 files changed
+3303 -2401

No files matched your search

-26
View File
@@ -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",
Generated
+338 -35
View File
@@ -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"
+1
View File
@@ -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"
+1 -7
View File
@@ -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]
+70 -1
View File
@@ -48,6 +48,8 @@ export interface ActionTrace {
errorCode?: string
}
export declare function activateLicense(request: LicenseKeyRequest): Promise<LicenseResponse>
/**
* 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<CapabilityModelCapability>
}
export declare function checkLicenseHealth(request: LicenseHealthRequest): Promise<LicenseResponse>
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<PortalResponse>
export declare function deactivateLicense(request: LicenseKeyRequest): Promise<CommandResponse>
export declare function evaluatePermissionV1(input: any): any
export declare function fetchRemoteAttachment(request: RemoteAttachmentFetchRequest): Promise<RemoteAttachmentFetchResponse>
@@ -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<Buffer>
export type PromptBuiltin = 'Date'|
@@ -687,15 +741,26 @@ export declare function runNativeActionRecipePreparedStream(input: ActionRuntime
export declare function safeFetch(request: SafeFetchRequest): Promise<SafeFetchResponse>
export type SafeFetchMethod = 'get'|
'head';
'head'|
'post'|
'put'|
'propfind'|
'report';
export interface SafeFetchRequest {
url: string
method?: SafeFetchMethod
headers?: Record<string, string>
body?: Buffer
timeoutMs?: number
maxRedirects?: number
maxBytes?: number
allowedHeaders?: Array<string>
allowedHosts?: Array<string>
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<CommandResponse>
export declare function updateLicenseSeats(request: LicenseSeatsRequest): Promise<CommandResponse>
/**
* Updates a document title in the workspace root doc's meta.pages array.
*
+1
View File
@@ -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;
+501
View File
@@ -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<Mutex<Option<Vec<u8>>>> = 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<LicenseInfo>,
pub error: Option<LicenseError>,
}
#[napi(object)]
pub struct CommandResponse {
pub error: Option<LicenseError>,
}
#[napi(object)]
pub struct PortalResponse {
pub url: Option<String>,
pub error: Option<LicenseError>,
}
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<Self::Output> {
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<Self::JsValue> {
Ok(output)
}
}
#[napi]
pub fn activate_license(request: LicenseKeyRequest) -> AsyncTask<AsyncActivateLicenseTask> {
AsyncTask::new(AsyncActivateLicenseTask { request })
}
#[napi]
impl Task for AsyncDeactivateLicenseTask {
type Output = CommandResponse;
type JsValue = CommandResponse;
fn compute(&mut self) -> Result<Self::Output> {
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<Self::JsValue> {
Ok(output)
}
}
#[napi]
pub fn deactivate_license(request: LicenseKeyRequest) -> AsyncTask<AsyncDeactivateLicenseTask> {
AsyncTask::new(AsyncDeactivateLicenseTask { request })
}
#[napi]
impl Task for AsyncCheckLicenseHealthTask {
type Output = LicenseResponse;
type JsValue = LicenseResponse;
fn compute(&mut self) -> Result<Self::Output> {
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<Self::JsValue> {
Ok(output)
}
}
#[napi]
pub fn check_license_health(request: LicenseHealthRequest) -> AsyncTask<AsyncCheckLicenseHealthTask> {
AsyncTask::new(AsyncCheckLicenseHealthTask { request })
}
#[napi]
impl Task for AsyncUpdateLicenseRecurringTask {
type Output = CommandResponse;
type JsValue = CommandResponse;
fn compute(&mut self) -> Result<Self::Output> {
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<Self::JsValue> {
Ok(output)
}
}
#[napi]
pub fn update_license_recurring(request: LicenseRecurringRequest) -> AsyncTask<AsyncUpdateLicenseRecurringTask> {
AsyncTask::new(AsyncUpdateLicenseRecurringTask { request })
}
#[napi]
impl Task for AsyncUpdateLicenseSeatsTask {
type Output = CommandResponse;
type JsValue = CommandResponse;
fn compute(&mut self) -> Result<Self::Output> {
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<Self::JsValue> {
Ok(output)
}
}
#[napi]
pub fn update_license_seats(request: LicenseSeatsRequest) -> AsyncTask<AsyncUpdateLicenseSeatsTask> {
AsyncTask::new(AsyncUpdateLicenseSeatsTask { request })
}
#[napi]
impl Task for AsyncCreateCustomerPortalTask {
type Output = PortalResponse;
type JsValue = PortalResponse;
fn compute(&mut self) -> Result<Self::Output> {
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<Self::JsValue> {
Ok(output)
}
}
#[napi]
pub fn create_license_customer_portal(request: LicenseKeyRequest) -> AsyncTask<AsyncCreateCustomerPortalTask> {
AsyncTask::new(AsyncCreateCustomerPortalTask { request })
}
fn license_info(
path: &str,
method: safefetch::SafeFetchMethod,
headers: Option<HashMap<String, String>>,
body: Option<Vec<u8>>,
) -> AnyResult<LicenseResponse> {
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<HashMap<String, String>>,
body: Option<Vec<u8>>,
) -> AnyResult<CommandResponse> {
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<HashMap<String, String>>,
body: Option<Vec<u8>>,
) -> AnyResult<safefetch::SafeFetchResponse> {
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<LicenseInfo> {
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<LicenseError> {
if (200..300).contains(&response.status) {
return None;
}
let body = String::from_utf8_lossy(&response.body).to_string();
if serde_json::from_str::<serde_json::Value>(&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<T: DeserializeOwned>(response: &safefetch::SafeFetchResponse) -> AnyResult<T> {
serde_json::from_slice(&response.body).context("invalid affine pro response")
}
fn parse_future_end_at(value: &serde_json::Value) -> AnyResult<f64> {
let millis = match value {
serde_json::Value::Number(number) => number.as_f64().context("invalid license expiration")?,
serde_json::Value::String(value) => value
.parse::<f64>()
.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<Vec<u8>> {
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,
}
+71 -502
View File
@@ -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<SafeFetchMethod>,
pub headers: Option<HashMap<String, String>>,
pub body: Option<Buffer>,
pub timeout_ms: Option<u32>,
pub max_redirects: Option<u32>,
pub max_bytes: Option<u32>,
pub allowed_headers: Option<Vec<String>>,
pub allowed_hosts: Option<Vec<String>>,
pub allow_http: Option<bool>,
pub allow_private_target_origin: Option<bool>,
pub enable_ech: Option<bool>,
pub ech_config_list: Option<Buffer>,
}
#[napi(object)]
@@ -111,36 +102,27 @@ pub struct AsyncRemoteMimeTypeTask {
request: RemoteMimeTypeRequest,
}
pub struct SafeFetchOutput {
status: u16,
final_url: String,
headers: HashMap<String, String>,
body: Vec<u8>,
impl From<SafeFetchMethod> 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,
}
struct SafeFetchParams {
url: String,
method: Option<SafeFetchMethod>,
headers: Option<HashMap<String, String>>,
timeout_ms: Option<u32>,
max_redirects: Option<u32>,
max_bytes: Option<u32>,
allow_private_origins: Option<Vec<String>>,
}
pub struct RemoteAttachmentFetchOutput {
final_url: String,
mime_type: String,
body: Vec<u8>,
}
#[napi]
impl Task for AsyncSafeFetchTask {
type Output = SafeFetchOutput;
type Output = safefetch::SafeFetchResponse;
type JsValue = SafeFetchResponse;
fn compute(&mut self) -> Result<Self::Output> {
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<Self::JsValue> {
@@ -160,29 +142,44 @@ pub fn safe_fetch(request: SafeFetchRequest) -> AsyncTask<AsyncSafeFetchTask> {
#[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<ImageInspectionOptions>) -> Result<ImageInspection> {
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<Self::Output> {
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<Self::JsValue> {
@@ -205,7 +202,10 @@ impl Task for AsyncRemoteMimeTypeTask {
type JsValue = String;
fn compute(&mut self) -> Result<Self::Output> {
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<Self::JsValue> {
@@ -218,472 +218,41 @@ pub fn infer_remote_mime_type(request: RemoteMimeTypeRequest) -> AsyncTask<Async
AsyncTask::new(AsyncRemoteMimeTypeTask { request })
}
fn safe_fetch_inner(request: &SafeFetchRequest) -> AnyResult<SafeFetchOutput> {
safe_fetch_params_inner(&SafeFetchParams {
pub(crate) fn safe_fetch_request(request: &SafeFetchRequest) -> anyhow::Result<safefetch::SafeFetchRequest> {
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<SafeFetchOutput> {
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(&current, request.allow_private_origins.as_deref())?;
let client = build_pinned_client(&current, &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);
}
bail!("too_many_redirects")
}
fn fetch_remote_attachment_inner(request: &RemoteAttachmentFetchRequest) -> AnyResult<RemoteAttachmentFetchOutput> {
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,
},
)?;
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,
}
}
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<Option<Vec<String>>> {
if !allow_private_target_origin {
fn ech_config_list(request: &SafeFetchRequest) -> anyhow::Result<Option<Vec<u8>>> {
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<Url> {
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<Vec<SocketAddr>> {
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<SocketAddr> = (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<Client> {
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<rustls::ClientConfig> {
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<String, String>>) -> AnyResult<HeaderMap> {
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<Response> {
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<SafeFetchOutput> {
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<String, String> {
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<std::net::Ipv4Addr> {
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<std::net::Ipv4Addr> {
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<ImageInspection> {
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<ImageInspection> {
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())
}
@@ -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',
},
})
@@ -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<string, string>).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)
@@ -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,
},
@@ -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 => {
@@ -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(((
@@ -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',
]
@@ -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<string, never>;
const test = ava as TestFn<Context>;
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 {
license: {
plan: SubscriptionPlan.SelfHostedTeam,
recurring: SubscriptionRecurring.Monthly,
quantity: 3,
endAt: expiresAt,
res: new Response(null, {
headers: {
'x-next-validate-key': 'next-validate-key',
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',
@@ -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<EventBus>;
feature: Sinon.SinonStubbedInstance<FeatureService>;
stripe: {
customers: Sinon.SinonStubbedInstance<Stripe.CustomersResource>;
prices: Sinon.SinonStubbedInstance<Stripe.PricesResource>;
@@ -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 => {
@@ -21,7 +21,7 @@ export async function inviteUsers(
app: TestingApp,
workspaceId: string,
emails: string[]
): Promise<Array<{ email: string; inviteId?: string; sentSuccess?: boolean }>> {
): Promise<Array<{ email: string; inviteId?: string }>> {
const res = await app.gql(
`
mutation inviteMembers($workspaceId: String!, $emails: [String!]!) {
@@ -31,7 +31,6 @@ export async function inviteUsers(
) {
email
inviteId
sentSuccess
}
}
`,
@@ -42,19 +42,6 @@ export async function listNotifications(
return res.currentUser.notifications;
}
export async function getNotificationCount(app: TestingApp): Promise<number> {
const res = await app.gql(
`
query notificationCount {
currentUser {
notificationCount
}
}
`
);
return res.currentUser.notificationCount;
}
export async function mentionUser(
app: TestingApp,
input: MentionInput
@@ -34,12 +34,11 @@ export async function getPublicUserById(
export async function sendChangeEmail(
app: TestingApp,
email: string,
callbackUrl: string
): Promise<boolean> {
const res = await app.gql(`
mutation {
sendChangeEmail(email: "${email}", callbackUrl: "${callbackUrl}")
sendChangeEmail(callbackUrl: "${callbackUrl}")
}
`);
@@ -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,
+33 -4
View File
@@ -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<string>([
'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<URL> {
@@ -72,20 +80,25 @@ export async function safeFetch(
): Promise<Response> {
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<SafeFetchRequest['method']>,
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);
@@ -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();
@@ -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<PaginatedCommentChangeObjectType> {
// 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'
);
+12 -1
View File
@@ -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;
@@ -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
@@ -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';
@@ -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'
);
}
}
@@ -5,14 +5,10 @@ import { Feature, UserFeatureName } from '../../models';
@Injectable()
export class AvailableUserFeatureConfig {
availableUserFeatures(): Set<UserFeatureName> {
return new Set([Feature.Admin, Feature.EarlyAccess, Feature.AIEarlyAccess]);
return new Set([Feature.Admin]);
}
configurableUserFeatures(): Set<UserFeatureName> {
return new Set(
env.selfhosted
? [Feature.Admin]
: [Feature.EarlyAccess, Feature.AIEarlyAccess, Feature.Admin]
);
return new Set([Feature.Admin]);
}
}
@@ -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<number> {
// 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',
})
@@ -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'
);
@@ -76,7 +76,6 @@ export class QuotaService {
usedStorageQuota: Number(state.usedStorageQuota),
memberCount: state.memberCount,
overcapacityMemberCount: state.overcapacityMemberCount,
usedSize: Number(state.usedStorageQuota),
};
}
@@ -118,12 +118,4 @@ export class WorkspaceQuotaType implements Partial<WorkspaceQuota> {
@Field()
humanReadable!: WorkspaceQuotaHumanReadableType;
/**
* @deprecated
*/
@Field(() => SafeIntResolver, {
deprecationReason: 'use `usedStorageQuota` instead',
})
usedSize!: number;
}
@@ -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',
@@ -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,
};
+25
View File
@@ -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;
@@ -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<Response> {
async fetch(url: string, init: RequestInit): Promise<Response> {
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.',
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;
}
const nextUrl = resolveHref(location, url);
return this.fetch(nextUrl, init, redirects + 1);
}
}
return response;
}
private async assertAllowedUrl(urlValue: string) {
@@ -599,52 +586,62 @@ class CalDAVRequestPolicy {
message: 'CalDAV host is not allowed.',
});
}
if (!this.blockPrivateNetwork) {
return;
}
try {
await assertSsrFSafeUrl(url);
} catch (error) {
if (error instanceof SsrfBlockedError) {
private toGraphqlSsrfError(error: unknown) {
if (!(error instanceof SsrfBlockedError)) {
return null;
}
const reason = String(error.data?.reason ?? '');
if (reason === 'blocked_ip') {
throw new GraphqlBadRequest({
return new GraphqlBadRequest({
code: 'caldav_private_network',
message: 'CalDAV host is in a private network.',
});
}
if (reason === 'unresolvable_hostname') {
throw new GraphqlBadRequest({
return new GraphqlBadRequest({
code: 'caldav_dns_failed',
message: 'Unable to resolve CalDAV host.',
});
}
if (reason === 'disallowed_protocol') {
throw new GraphqlBadRequest({
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'
) {
throw new GraphqlBadRequest({
return new GraphqlBadRequest({
code: 'caldav_invalid_url',
message: 'CalDAV URL is invalid.',
});
}
}
throw error;
}
return null;
}
}
@@ -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,23 +763,25 @@ 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 response = await this.probeFetch(
request.url,
{
method: request.method,
headers: request.headers as unknown as Record<string, string>,
signal: controller.signal,
});
},
{
timeoutMs: TEST_TIMEOUT_MS,
maxRedirects: 3,
maxBytes: 1024,
allowedHeaders: Object.keys(request.headers),
}
);
if (!response.ok) {
throw new BadRequestException(
this.providerProbeFailureMessage(response.status)
);
}
} finally {
clearTimeout(timeout);
}
}
private buildProbeRequest(
@@ -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) }
);
}
@@ -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),
@@ -300,21 +300,19 @@ export class CopilotEmbeddingJob {
}
}
if (contextId) {
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,
workspaceId,
fileId,
error: mapAnyError(error).message,
});
}
// passthrough error to job queue
throw error;
@@ -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;
};
@@ -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' },
},
});
@@ -1,10 +0,0 @@
import './config';
import { Module } from '@nestjs/common';
import { CustomerIoService } from './service';
@Module({
providers: [CustomerIoService],
})
export class CustomerIoModule {}
@@ -1,72 +0,0 @@
import { Injectable } from '@nestjs/common';
import { Config, OnEvent } from '../../base';
@Injectable()
export class CustomerIoService {
#fetch: ((url: string, options?: RequestInit) => Promise<Response>) | 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',
}
);
}
}
@@ -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: {
@@ -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;
@@ -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<string, any>[] = [];
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}`);
@@ -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<License>(
`/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<License>(
`/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<License, 'endAt'>) {
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<T = any>(
path: string,
init?: RequestInit
): Promise<T & { res: Response }> {
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'
);
}
}
@@ -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) {
@@ -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<typeof OIDCConfigurationSchema>;
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<typeof createRemoteJWKSet> | 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) {
@@ -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: {
@@ -19,9 +19,7 @@ export class StripeWebhookController {
@Public()
@Post('/webhook')
async handleWebhook(@Req() req: RawBodyRequest<Request>) {
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 {
@@ -256,19 +256,21 @@ export abstract class SubscriptionManager {
}
async getPrice(lookupKey: LookupKey): Promise<KnownStripePrice | null> {
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(
@@ -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<typeof UserSubscriptionCheckoutArgs>
) {
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<PriceStrategyStatus> {
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(
@@ -166,14 +166,6 @@ export class InvoiceType implements Partial<Invoice> {
@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;
@@ -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;
}
@@ -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:
@@ -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,
};
}
@@ -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';
}
}
+1 -12
View File
@@ -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!
}
@@ -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 = {
@@ -2,7 +2,6 @@ query invoices($take: Int!, $skip: Int!) {
currentUser {
invoiceCount
invoices(take: $take, skip: $skip) {
id
status
currency
amount
@@ -2,6 +2,5 @@ mutation inviteByEmails($workspaceId: String!, $emails: [String!]!) {
inviteMembers(workspaceId: $workspaceId, emails: $emails) {
email
inviteId
sentSuccess
}
}
@@ -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
-21
View File
@@ -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<Scalars['JSONObject']['output']>;
/** Invite id, null if invite record create failed */
inviteId: Maybe<Scalars['String']['output']>;
/**
* 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<Scalars['String']['output']>;
lastPaymentError: Maybe<Scalars['String']['output']>;
link: Maybe<Scalars['String']['output']>;
/** @deprecated removed */
@@ -2273,7 +2264,6 @@ export interface MutationRevokeUserAccessTokenArgs {
export interface MutationSendChangeEmailArgs {
callbackUrl: Scalars['String']['input'];
email?: InputMaybe<Scalars['String']['input']>;
}
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<InvoiceType>;
/** 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;
-20
View File
@@ -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",
@@ -37,13 +37,7 @@ type ConfigGroup<T extends AppConfigModule> = {
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(
@@ -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
@@ -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"] }
}
}
@@ -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"] }
}
}
}
@@ -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"] }
}
}
@@ -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"] }
}
}
}
@@ -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<AffineGraphQL.SubscriptionPlan>.self),
.field("recurring", GraphQLEnum<AffineGraphQL.SubscriptionRecurring>.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<AffineGraphQL.SubscriptionPlan> { __data["plan"] }
public var recurring: GraphQLEnum<AffineGraphQL.SubscriptionRecurring> { __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"] }
}
}
}
@@ -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"] }
}
}
}
@@ -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<AffineGraphQL.AiJobStatus>.self),
] }
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
RetryTranscriptTaskMutation.Data.RetryTranscriptTask.self
] }
public var id: AffineGraphQL.ID { __data["id"] }
public var status: GraphQLEnum<AffineGraphQL.AiJobStatus> { __data["status"] }
}
}
}
@@ -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"),
@@ -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<AffineGraphQL.AiJobStatus>.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<AffineGraphQL.AiJobStatus> { __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"] }
}
}
}
}
@@ -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<Upload>
public var blobs: GraphQLNullable<[Upload]>
public var input: GraphQLNullable<SubmitAudioTranscriptionInput>
public init(
workspaceId: String,
blobId: String,
blob: GraphQLNullable<Upload>,
blobs: GraphQLNullable<[Upload]>,
input: GraphQLNullable<SubmitAudioTranscriptionInput>
) {
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<AffineGraphQL.AiJobStatus>.self),
] }
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
SubmitTranscriptTaskMutation.Data.SubmitTranscriptTask.self
] }
public var id: AffineGraphQL.ID { __data["id"] }
public var status: GraphQLEnum<AffineGraphQL.AiJobStatus> { __data["status"] }
}
}
}
@@ -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<AffineGraphQL.ByokKeyTestStatus>.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<AffineGraphQL.ByokKeyTestStatus> { __data["status"] }
public var message: String? { __data["message"] }
}
}
}
@@ -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"] }
}
}
}
@@ -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<String>
public var blobId: GraphQLNullable<String>
public init(
workspaceId: String,
taskId: GraphQLNullable<String>,
blobId: GraphQLNullable<String>
) {
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<AffineGraphQL.AiJobStatus>.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<AffineGraphQL.AiJobStatus> { __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"] }
}
}
}
}
}
}
@@ -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<AffineGraphQL.InvoiceStatus>.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<AffineGraphQL.InvoiceStatus> { __data["status"] }
public var currency: String { __data["currency"] }
public var amount: Int { __data["amount"] }
@@ -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<AffineGraphQL.ByokProvider>].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<AffineGraphQL.ByokProvider>] { __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<AffineGraphQL.ByokProvider>.self),
.field("name", String.self),
.field("description", String?.self),
.field("storage", GraphQLEnum<AffineGraphQL.ByokKeyStorage>.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<AffineGraphQL.ByokKeyTestStatus>.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<AffineGraphQL.ByokProvider> { __data["provider"] }
public var name: String { __data["name"] }
public var description: String? { __data["description"] }
public var storage: GraphQLEnum<AffineGraphQL.ByokKeyStorage> { __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<AffineGraphQL.ByokKeyTestStatus> { __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<AffineGraphQL.ByokProvider>].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<AffineGraphQL.ByokProvider>] { __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"] }
}
}
}
}
@@ -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<AffineGraphQL.InvoiceStatus>.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<AffineGraphQL.InvoiceStatus> { __data["status"] }
public var currency: String { __data["currency"] }
public var amount: Int { __data["amount"] }
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
@@ -4,6 +4,5 @@
import ApolloAPI
public enum SubscriptionVariant: String, EnumType {
case ea = "EA"
case onetime = "Onetime"
}
@@ -10,6 +10,23 @@ public struct CreateChatMessageInput: InputObject {
__data = data
}
public init(
blob: GraphQLNullable<Upload> = nil,
blobs: GraphQLNullable<[Upload]> = nil,
content: GraphQLNullable<String> = nil,
params: GraphQLNullable<JSON> = 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<Upload> = 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 }
@@ -10,6 +10,25 @@ public struct CreateCheckoutSessionInput: InputObject {
__data = data
}
public init(
args: GraphQLNullable<JSONObject> = nil,
coupon: GraphQLNullable<String> = nil,
plan: GraphQLNullable<GraphQLEnum<SubscriptionPlan>> = nil,
recurring: GraphQLNullable<GraphQLEnum<SubscriptionRecurring>> = nil,
successCallbackLink: String,
variant: GraphQLNullable<GraphQLEnum<SubscriptionVariant>> = 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<JSONObject> = nil,
coupon: GraphQLNullable<String> = nil,
@@ -40,6 +59,7 @@ public struct CreateCheckoutSessionInput: InputObject {
set { __data["coupon"] = newValue }
}
@available(*, deprecated, message: "not required anymore")
public var idempotencyKey: GraphQLNullable<String> {
get { __data["idempotencyKey"] }
set { __data["idempotencyKey"] = newValue }
@@ -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 }
}
}
@@ -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<String> = nil,
enabled: GraphQLNullable<Bool> = nil,
endpoint: GraphQLNullable<String> = nil,
name: String,
provider: GraphQLEnum<ByokProvider>,
sortOrder: GraphQLNullable<SafeInt> = 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<String> {
get { __data["description"] }
set { __data["description"] = newValue }
}
public var enabled: GraphQLNullable<Bool> {
get { __data["enabled"] }
set { __data["enabled"] = newValue }
}
public var endpoint: GraphQLNullable<String> {
get { __data["endpoint"] }
set { __data["endpoint"] = newValue }
}
public var name: String {
get { __data["name"] }
set { __data["name"] = newValue }
}
public var provider: GraphQLEnum<ByokProvider> {
get { __data["provider"] }
set { __data["provider"] = newValue }
}
public var sortOrder: GraphQLNullable<SafeInt> {
get { __data["sortOrder"] }
set { __data["sortOrder"] = newValue }
}
}
@@ -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<ByokKeyStorage>,
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<ByokKeyStorage> {
get { __data["storage"] }
set { __data["storage"] = newValue }
}
public var workspaceId: String {
get { __data["workspaceId"] }
set { __data["workspaceId"] = newValue }
}
}
@@ -13,12 +13,14 @@ public struct SubmitAudioTranscriptionInput: InputObject {
public init(
quality: GraphQLNullable<TranscriptionQualityInput> = nil,
sliceManifest: GraphQLNullable<[AudioSliceManifestItemInput]> = nil,
sourceAudio: GraphQLNullable<TranscriptionSourceAudioInput> = nil
sourceAudio: GraphQLNullable<TranscriptionSourceAudioInput> = nil,
strategy: GraphQLNullable<String> = 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<String> {
get { __data["strategy"] }
set { __data["strategy"] = newValue }
}
}
@@ -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<String> = nil,
configId: GraphQLNullable<ID> = nil,
endpoint: GraphQLNullable<String> = nil,
provider: GraphQLEnum<ByokProvider>,
storage: GraphQLEnum<ByokKeyStorage>,
workspaceId: String
) {
__data = InputDict([
"apiKey": apiKey,
"configId": configId,
"endpoint": endpoint,
"provider": provider,
"storage": storage,
"workspaceId": workspaceId
])
}
public var apiKey: GraphQLNullable<String> {
get { __data["apiKey"] }
set { __data["apiKey"] = newValue }
}
public var configId: GraphQLNullable<ID> {
get { __data["configId"] }
set { __data["configId"] = newValue }
}
public var endpoint: GraphQLNullable<String> {
get { __data["endpoint"] }
set { __data["endpoint"] = newValue }
}
public var provider: GraphQLEnum<ByokProvider> {
get { __data["provider"] }
set { __data["provider"] = newValue }
}
public var storage: GraphQLEnum<ByokKeyStorage> {
get { __data["storage"] }
set { __data["storage"] = newValue }
}
public var workspaceId: String {
get { __data["workspaceId"] }
set { __data["workspaceId"] = newValue }
}
}
@@ -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<String> = nil,
description: GraphQLNullable<String> = nil,
enabled: GraphQLNullable<Bool> = nil,
endpoint: GraphQLNullable<String> = nil,
id: GraphQLNullable<ID> = nil,
name: String,
provider: GraphQLEnum<ByokProvider>,
sortOrder: GraphQLNullable<SafeInt> = nil,
storage: GraphQLEnum<ByokKeyStorage>,
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<String> {
get { __data["apiKey"] }
set { __data["apiKey"] = newValue }
}
public var description: GraphQLNullable<String> {
get { __data["description"] }
set { __data["description"] = newValue }
}
public var enabled: GraphQLNullable<Bool> {
get { __data["enabled"] }
set { __data["enabled"] = newValue }
}
public var endpoint: GraphQLNullable<String> {
get { __data["endpoint"] }
set { __data["endpoint"] = newValue }
}
public var id: GraphQLNullable<ID> {
get { __data["id"] }
set { __data["id"] = newValue }
}
public var name: String {
get { __data["name"] }
set { __data["name"] = newValue }
}
public var provider: GraphQLEnum<ByokProvider> {
get { __data["provider"] }
set { __data["provider"] = newValue }
}
public var sortOrder: GraphQLNullable<SafeInt> {
get { __data["sortOrder"] }
set { __data["sortOrder"] = newValue }
}
public var storage: GraphQLEnum<ByokKeyStorage> {
get { __data["storage"] }
set { __data["storage"] = newValue }
}
public var workspaceId: String {
get { __data["workspaceId"] }
set { __data["workspaceId"] = newValue }
}
}
@@ -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
)
@@ -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
)
}
Loaded 100 of 142 files, more files were not shown because too many files have changed in this diff. Show more