From 412ec999facc4147ed8242ea9a3cccdd20920e9f Mon Sep 17 00:00:00 2001 From: KallynGowdy Date: Thu, 28 Sep 2023 15:10:45 -0400 Subject: [PATCH] feat: Support customizing the expiration mode --- .../redis/RedisTempInstRecordsStore.ts | 48 +- .../aux-backend/server/.dev.env.json | 2 +- src/aux-server/aux-backend/server/server.ts | 2 +- .../serverless/aws/src/LoadServer.ts | 2 +- .../aux-backend/shared/ServerBuilder.ts | 607 ++++++++++++++---- 5 files changed, 515 insertions(+), 146 deletions(-) diff --git a/src/aux-server/aux-backend/redis/RedisTempInstRecordsStore.ts b/src/aux-server/aux-backend/redis/RedisTempInstRecordsStore.ts index 0e40c84272..c3d9ddf70f 100644 --- a/src/aux-server/aux-backend/redis/RedisTempInstRecordsStore.ts +++ b/src/aux-server/aux-backend/redis/RedisTempInstRecordsStore.ts @@ -14,18 +14,26 @@ export class RedisTempInstRecordsStore implements TemporaryInstRecordsStore { private _redis: RedisClientType; private _currentGenerationKey: string; private _instDataExpirationSeconds: number | null = null; + private _instDataExpirationMode: 'NX' | 'XX' | 'LT' | 'GT' | null = null; /** * Creates a new instance of the RedisTempInstRecordsStore class. * @param globalNamespace The namespace that should be used for all redis keys. * @param redis The client that should be used. * @param tempInstDataExpirationSeconds The number of seconds that recordless (temporary) inst data should be stored for. If null, then the data will not expire. + * @param tempInstDataExpirationMode The expiration mode that should be used for recordless (temporary) inst data. */ - constructor(globalNamespace: string, redis: RedisClientType, tempInstDataExpirationSeconds: number | null = null) { + constructor( + globalNamespace: string, + redis: RedisClientType, + tempInstDataExpirationSeconds: number | null = null, + tempInstDataExpirationMode: 'NX' | 'XX' | 'LT' | 'GT' | null = null + ) { this._globalNamespace = globalNamespace; this._redis = redis; this._currentGenerationKey = `${this._globalNamespace}/currentGeneration`; this._instDataExpirationSeconds = tempInstDataExpirationSeconds; + this._instDataExpirationMode = tempInstDataExpirationMode; } async setDirtyBranchGeneration(generation: string): Promise { @@ -122,7 +130,11 @@ export class RedisTempInstRecordsStore implements TemporaryInstRecordsStore { await this._redis.set(key, JSON.stringify(branch)); if (!branch.recordName && this._instDataExpirationSeconds) { - await this._redis.expire(key, this._instDataExpirationSeconds); + await this._redis.expire( + key, + this._instDataExpirationSeconds, + this._instDataExpirationMode ?? undefined + ); } } @@ -199,7 +211,11 @@ export class RedisTempInstRecordsStore implements TemporaryInstRecordsStore { if (!recordName && this._instDataExpirationSeconds) { const multi = this._redis.multi(); multi.rPush(key, finalUpdates); - multi.expire(key, this._instDataExpirationSeconds, 'LT'); + multi.expire( + key, + this._instDataExpirationSeconds, + this._instDataExpirationMode ?? undefined + ); promise = multi.exec(); } else { promise = this._redis.rPush(key, finalUpdates); @@ -229,7 +245,11 @@ export class RedisTempInstRecordsStore implements TemporaryInstRecordsStore { if (!recordName && this._instDataExpirationSeconds) { const multi = this._redis.multi(); multi.set(key, sizeInBytes.toString()); - multi.expire(key, this._instDataExpirationSeconds, 'LT'); + multi.expire( + key, + this._instDataExpirationSeconds, + this._instDataExpirationMode ?? undefined + ); await multi.exec(); } else { await this._redis.set(key, sizeInBytes.toString()); @@ -246,7 +266,11 @@ export class RedisTempInstRecordsStore implements TemporaryInstRecordsStore { if (!recordName && this._instDataExpirationSeconds) { const multi = this._redis.multi(); multi.incrBy(key, sizeInBytes); - multi.expire(key, this._instDataExpirationSeconds, 'LT'); + multi.expire( + key, + this._instDataExpirationSeconds, + this._instDataExpirationMode ?? undefined + ); await multi.exec(); } else { await this._redis.incrBy(key, sizeInBytes); @@ -278,7 +302,11 @@ export class RedisTempInstRecordsStore implements TemporaryInstRecordsStore { if (!recordName && this._instDataExpirationSeconds) { const multi = this._redis.multi(); multi.set(key, sizeInBytes.toString()); - multi.expire(key, this._instDataExpirationSeconds, 'LT'); + multi.expire( + key, + this._instDataExpirationSeconds, + this._instDataExpirationMode ?? undefined + ); await multi.exec(); } else { await this._redis.set(key, sizeInBytes.toString()); @@ -292,10 +320,14 @@ export class RedisTempInstRecordsStore implements TemporaryInstRecordsStore { sizeInBytes: number ): Promise { const key = this._getBranchSizeKey(recordName, inst, branch); - if(!recordName && this._instDataExpirationSeconds) { + if (!recordName && this._instDataExpirationSeconds) { const multi = this._redis.multi(); multi.incrBy(key, sizeInBytes); - multi.expire(key, this._instDataExpirationSeconds, 'LT'); + multi.expire( + key, + this._instDataExpirationSeconds, + this._instDataExpirationMode ?? undefined + ); await multi.exec(); } else { await this._redis.incrBy(key, sizeInBytes); diff --git a/src/aux-server/aux-backend/server/.dev.env.json b/src/aux-server/aux-backend/server/.dev.env.json index e7f4eb8a4e..4af0d931ed 100644 --- a/src/aux-server/aux-backend/server/.dev.env.json +++ b/src/aux-server/aux-backend/server/.dev.env.json @@ -16,7 +16,7 @@ "tls": false, "tempInstRecordsStoreNamespace": "tempInsts", "websocketConnectionNamespace": "connections", - "publicInstRecordsStoreNamespace": "insts" + "instRecordsStoreNamespace": "insts" }, "livekit": { "apiKey": "APIu7LWFmsZckWx", diff --git a/src/aux-server/aux-backend/server/server.ts b/src/aux-server/aux-backend/server/server.ts index 8f53c31d89..58006ae577 100644 --- a/src/aux-server/aux-backend/server/server.ts +++ b/src/aux-server/aux-backend/server/server.ts @@ -226,7 +226,7 @@ export class Server { if ( options.redis && options.redis.tempInstRecordsStoreNamespace && - options.redis.publicInstRecordsStoreNamespace && + options.redis.instRecordsStoreNamespace && options.prisma ) { builder.usePrismaAndRedisInstRecords(); diff --git a/src/aux-server/aux-backend/serverless/aws/src/LoadServer.ts b/src/aux-server/aux-backend/serverless/aws/src/LoadServer.ts index 1b34198a37..721fe94afb 100644 --- a/src/aux-server/aux-backend/serverless/aws/src/LoadServer.ts +++ b/src/aux-server/aux-backend/serverless/aws/src/LoadServer.ts @@ -113,7 +113,7 @@ export function constructServerBuilder() { if ( config.redis && config.redis.tempInstRecordsStoreNamespace && - config.redis.publicInstRecordsStoreNamespace && + config.redis.instRecordsStoreNamespace && config.prisma ) { builder.usePrismaAndRedisInstRecords(); diff --git a/src/aux-server/aux-backend/shared/ServerBuilder.ts b/src/aux-server/aux-backend/shared/ServerBuilder.ts index 4a98c8cb80..c3a92237eb 100644 --- a/src/aux-server/aux-backend/shared/ServerBuilder.ts +++ b/src/aux-server/aux-backend/shared/ServerBuilder.ts @@ -525,9 +525,9 @@ export class ServerBuilder implements SubscriptionLike { ); } - if (!options.redis.publicInstRecordsStoreNamespace) { + if (!options.redis.instRecordsStoreNamespace) { throw new Error( - 'Redis public inst records store namespace must be provided.' + 'Redis inst records store namespace must be provided.' ); } @@ -540,9 +540,10 @@ export class ServerBuilder implements SubscriptionLike { ); this._instRecordsStore = new SplitInstRecordsStore( new RedisTempInstRecordsStore( - options.redis.publicInstRecordsStoreNamespace, - redis - options.redis.publicInstRecordsLifetimeSeconds + options.redis.instRecordsStoreNamespace, + redis, + options.redis.publicInstRecordsLifetimeSeconds, + options.redis.publicInstRecordsLifetimeExpireMode ), new PrismaInstRecordsStore(prisma) ); @@ -1106,7 +1107,7 @@ export class ServerBuilder implements SubscriptionLike { options: Pick ): PolicyStore { const policyStore = new PrismaPolicyStore(prismaClient); - if (this._multiCache) { + if (this._multiCache && options.prisma.policiesCacheSeconds) { const cache = this._multiCache.getCache('policies'); return new CachingPolicyStore( policyStore, @@ -1125,7 +1126,7 @@ export class ServerBuilder implements SubscriptionLike { const configStore = new PrismaConfigurationStore(prismaClient, { subscriptions: options.subscriptions as SubscriptionConfiguration, }); - if (this._multiCache) { + if (this._multiCache && options.prisma.configurationCacheSeconds) { const cache = this._multiCache.getCache('config'); return new CachingConfigStore( configStore, @@ -1138,214 +1139,550 @@ export class ServerBuilder implements SubscriptionLike { } } -/** - * The schema for the DynamoDB configuration. - */ -const dynamoDbSchema = z.object({ - usersTable: z.string().nonempty(), - userAddressesTable: z.string().nonempty(), - loginRequestsTable: z.string().nonempty(), - sessionsTable: z.string().nonempty(), - emailTable: z.string().nonempty(), - smsTable: z.string().nonempty(), - policiesTable: z.string().nonempty(), - rolesTable: z.string().nonempty(), - subjectRolesTable: z.string().nonempty(), - roleSubjectsTable: z.string().nonempty(), - stripeCustomerIdsIndexName: z.string().nonempty(), - publicRecordsTable: z.string().nonempty(), - publicRecordsKeysTable: z.string().nonempty(), - dataTable: z.string().nonempty(), - manualDataTable: z.string().nonempty(), - filesTable: z.string().nonempty(), - eventsTable: z.string().nonempty(), - - endpoint: z.string().nonempty().optional(), -}); - /** * The schema for the S3 configuration. */ const s3Schema = z.object({ - region: z.string().nonempty(), - filesBucket: z.string().nonempty(), - filesStorageClass: z.string().nonempty(), - - messagesBucket: z.string().nonempty().optional(), - - options: z.object({ - endpoint: z.string().nonempty().optional(), - s3ForcePathStyle: z.boolean().optional(), - }), + region: z + .string() + .describe( + 'The region of the file records and websocket message buckets.' + ) + .nonempty(), + filesBucket: z + .string() + .describe( + 'The name of the bucket that file records should be placed in.' + ) + .nonempty(), + filesStorageClass: z + .string() + .describe( + 'The S3 File Storage Class that should be used for file records.' + ) + .nonempty(), + + messagesBucket: z + .string() + .describe( + 'The name of the bucket that large websocket messages should be placed in.' + ) + .nonempty() + .optional(), - host: z.string().nonempty().optional(), + options: z + .object({ + endpoint: z + .string() + .describe('The endpoint of the S3 API.') + .nonempty() + .optional(), + s3ForcePathStyle: z + .boolean() + .describe( + 'Wether to force the S3 client to use the path style API. Defaults to false.' + ) + .optional(), + }) + .describe('Options for the S3 client.'), + + host: z + .string() + .describe( + 'The S3 host that should be used for file record storage. If omitted, then the default S3 host will be used.' + ) + .nonempty() + .optional(), }); const livekitSchema = z.object({ - apiKey: z.string().nonempty().nullable(), - secretKey: z.string().nonempty().nullable(), - endpoint: z.string().nonempty().nullable(), + apiKey: z + .string() + .describe('The API Key for Livekit.') + .nonempty() + .nullable(), + secretKey: z + .string() + .describe('The secret key for Livekit.') + .nonempty() + .nullable(), + endpoint: z + .string() + .describe('The URL that the Livekit server is publicly available at.') + .nonempty() + .nullable(), }); const textItSchema = z.object({ - apiKey: z.string().nonempty().nullable(), - flowId: z.string().nonempty().nullable(), + apiKey: z + .string() + .describe('The API Key for TextIt.') + .nonempty() + .nullable(), + flowId: z + .string() + .describe( + 'The ID of the flow that should be triggered for sending login codes.' + ) + .nonempty() + .nullable(), }); const sesContentSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal('template'), - templateArn: z.string().nonempty(), + templateArn: z + .string() + .describe('The ARN of the SES email template that should be used.') + .nonempty(), }), z.object({ type: z.literal('plain'), - subject: z.string().nonempty(), - body: z.string().nonempty(), + subject: z.string().describe('The subject of the email.').nonempty(), + body: z + .string() + .describe( + 'The body of the email. Use double curly-braces {{variable}} to insert variables.' + ) + .nonempty(), }), ]); const sesSchema = z.object({ - fromAddress: z.string().nonempty(), - content: sesContentSchema, + fromAddress: z + .string() + .describe('The email address that SES messages should be sent from.') + .nonempty(), + content: sesContentSchema.describe( + 'The content that should be sent in login codes in emails.' + ), }); const redisSchema = z.object({ - url: z.string().nonempty().optional(), - host: z.string().nonempty().optional(), - port: z.number().optional(), - password: z.string().nonempty().optional(), - tls: z.boolean().optional(), - - causalRepoNamespace: z.string().nonempty().optional(), - rateLimitPrefix: z.string().nonempty().optional(), - - maxBranchSizeBytes: z.number().optional(), - mergeUpdatesOnMaxSizeExceeded: z.boolean().optional(), - - websocketConnectionNamespace: z.string().optional(), - publicInstRecordsStoreNamespace: z.string().optional(), - tempInstRecordsStoreNamespace: z.string().optional(), - publicInstRecordsLifetimeSeconds: z.number() - .describe('The lifetime of public inst records in seconds. If null, then public inst records never expire. Defaults to 1 day in seconds (86,400)') + url: z + .string() + .describe( + 'The Redis connection URL that should be used. If omitted, then host, port, and password must be provided.' + ) + .nonempty() + .optional(), + host: z + .string() + .describe( + 'The host that the redis client should connect to. Ignored if url is provided.' + ) + .nonempty() + .optional(), + port: z + .number() + .describe( + 'The port that the redis client should connect to. Ignored if url is provided.' + ) + .optional(), + password: z + .string() + .describe( + 'The password that the redis client should use. Ignored if url is provided.' + ) + .nonempty() + .optional(), + tls: z + .boolean() + .describe( + 'Whether to use TLS for connecting to the Redis server. Ignored if url is provided.' + ) + .optional(), + + rateLimitPrefix: z + .string() + .describe( + 'The namespace that rate limit counters are stored under. If omitted, then redis rate limiting is not possible.' + ) + .nonempty() + .optional(), + + websocketConnectionNamespace: z + .string() + .describe( + 'The namespace that websocket connections are stored under. If omitted, then redis inst records are not possible.' + ) + .optional(), + instRecordsStoreNamespace: z + .string() + .describe( + 'The namespace that inst records are stored under. If omitted, then redis inst records are not possible.' + ) + .optional(), + publicInstRecordsLifetimeSeconds: z + .number() + .describe( + 'The lifetime of public inst records in seconds. If null, then public inst records never expire. Defaults to 1 day in seconds (86,400)' + ) .positive() .nullable() .optional() .default(60 * 60 * 24), + publicInstRecordsLifetimeExpireMode: z + .union([ + z.literal('NX').describe('The Redis NX expire mode.'), + z.literal('XX').describe('The Redis XX expire mode.'), + z.literal('GT').describe('The Redis GT expire mode.'), + z.literal('LT').describe('The Redis LT expire mode.'), + z.null().describe('The expiration will be updated every time.'), + ]) + .describe( + 'The Redis expire mode that should be used for public inst records. Defaults to NX. If null, then the expiration will update every time the inst data is updated.' + ) + .optional() + .default('NX'), + + tempInstRecordsStoreNamespace: z + .string() + .describe( + 'The namespace that temporary inst records are stored under. If omitted, then redis inst records are not possible.' + ) + .optional(), // The number of seconds that authorizations for repo/add_updates permissions (inst.read and inst.updateData) are cached for. // Because repo/add_updates is a very common permission, we periodically cache permissions to avoid hitting the database too often. // 5 minutes by default - connectionAuthorizationCacheSeconds: z.number().positive().default(300), - - cacheNamespace: z.string().nonempty().default('/cache'), + connectionAuthorizationCacheSeconds: z + .number() + .describe( + `The number of seconds that authorizations for repo/add_updates permissions (inst.read and inst.updateData) are cached for. +Because repo/add_updates is a very common permission, we periodically cache permissions to avoid hitting the database too often. Defaults to 5 minutes.` + ) + .positive() + .default(300), + + cacheNamespace: z + .string() + .describe( + 'The namespace for cached items. (policies & configuration) Defaults to "/cache". Set to null to disable caching of policies and configuration.' + ) + .nonempty() + .nullable() + .optional() + .default('/cache'), }); const rateLimitSchema = z.object({ - maxHits: z.number().positive(), - windowMs: z.number().positive(), + maxHits: z + .number() + .describe( + 'The maximum number of hits allowed from a single IP Address within the window.' + ) + .positive(), + windowMs: z + .number() + .describe('The size of the window in miliseconds.') + .positive(), }); const stripeSchema = z.object({ - secretKey: z.string().nonempty(), - publishableKey: z.string().nonempty(), - testClock: z.string().nonempty().optional(), + secretKey: z + .string() + .describe('The Stripe secret key that should be used.') + .nonempty(), + publishableKey: z + .string() + .describe('The Stripe publishable key that should be used.') + .nonempty(), + testClock: z + .string() + .describe('The stripe test clock that should be used.') + .nonempty() + .optional(), }); const mongodbSchema = z.object({ - url: z.string().nonempty(), - useNewUrlParser: z.boolean().optional().default(false), - database: z.string().nonempty(), - fileUploadUrl: z.string().nonempty().optional(), + url: z + .string() + .describe('The MongoDB URL that should be used to connect to MongoDB.') + .nonempty(), + useNewUrlParser: z + .boolean() + .describe('Whether to use the new URL parser. Defaults to false.') + .optional() + .default(false), + database: z + .string() + .describe('The database that should be used.') + .nonempty(), + fileUploadUrl: z + .string() + .describe('The URL that files records need to be uploaded to.') + .nonempty() + .optional(), }); const prismaSchema = z.object({ - options: z.object({}).passthrough().optional(), + options: z + .object({}) + .describe( + 'Generic options that should be passed to the Prisma client constructor.' + ) + .passthrough() + .optional(), - policiesCacheSeconds: z.number().positive().optional().default(60), - configurationCacheSeconds: z.number().positive().optional().default(60), + policiesCacheSeconds: z + .number() + .describe( + 'The number of seconds that policies are cached for. Defaults to 60 seconds. Set to null to disable caching of policies.' + ) + .positive() + .nullable() + .optional() + .default(60), + configurationCacheSeconds: z + .number() + .describe( + 'The number of seconds that configuration items are cached for. Defaults to 60 seconds. Set to null to disable caching of configuration items.' + ) + .positive() + .nullable() + .optional() + .default(60), }); const openAiSchema = z.object({ - apiKey: z.string().nonempty(), + apiKey: z + .string() + .describe('The OpenAI API Key that should be used.') + .nonempty(), }); const blockadeLabsSchema = z.object({ - apiKey: z.string().nonempty(), + apiKey: z + .string() + .describe('The Blockade Labs API Key that should be used.') + .nonempty(), }); const stabilityAiSchema = z.object({ - apiKey: z.string().nonempty(), + apiKey: z + .string() + .describe('The StabilityAI API Key that should be used.') + .nonempty(), }); const aiSchema = z.object({ chat: z .object({ - provider: z.literal('openai'), - defaultModel: z.string().nonempty(), - allowedModels: z.array(z.string().nonempty()), - allowedSubscriptionTiers: z.union([ - z.literal(true), - z.array(z.string().nonempty()), - ]), + provider: z + .literal('openai') + .describe( + 'The provider that should be used for Chat AI requests.' + ), + defaultModel: z + .string() + .describe( + 'The model that should be used for Chat AI requests when one is not specified.' + ) + .nonempty(), + allowedModels: z + .array(z.string().nonempty()) + .describe( + 'The list of models that are allowed to be used for Chat AI requets.' + ), + allowedSubscriptionTiers: z + .union([z.literal(true), z.array(z.string().nonempty())]) + .describe( + 'The subscription tiers that are allowed to use Chat AI. If true, then all tiers are allowed.' + ), }) + .describe('Options for Chat AI. If omitted, then chat AI is disabled.') .optional(), generateSkybox: z .object({ - provider: z.literal('blockadeLabs'), - allowedSubscriptionTiers: z.union([ - z.literal(true), - z.array(z.string().nonempty()), - ]), + provider: z + .literal('blockadeLabs') + .describe( + 'The provider that should be used for Skybox Generation AI requests.' + ), + allowedSubscriptionTiers: z + .union([z.literal(true), z.array(z.string().nonempty())]) + .describe( + 'The subscription tiers that are allowed to use Skybox AI. If true, then all tiers are allowed.' + ), }) + .describe( + 'Options for Skybox Generation AI. If omitted, then Skybox AI is disabled.' + ) .optional(), images: z .object({ - defaultModel: z.string(), - defaultWidth: z.number().int().positive(), - defaultHeight: z.number().int().positive(), - maxWidth: z.number().int().positive().optional(), - maxHeight: z.number().int().positive().optional(), - maxSteps: z.number().int().positive().optional(), - maxImages: z.number().int().positive().optional(), - allowedModels: z.object({ - openai: z.array(z.string().nonempty()).optional(), - stabilityai: z.array(z.string().nonempty()).optional(), - }), - allowedSubscriptionTiers: z.union([ - z.literal(true), - z.array(z.string().nonempty()), - ]), + defaultModel: z + .string() + .describe( + 'The model that should be used for Image AI requests when one is not specified.' + ) + .nonempty(), + defaultWidth: z + .number() + .describe('The default width of generated images.') + .int() + .positive(), + defaultHeight: z + .number() + .describe('The default height of generated images.') + .int() + .positive(), + maxWidth: z + .number() + .describe( + 'The maximum width of generated images. If omitted, then the max width is controlled by the model.' + ) + .int() + .positive() + .optional(), + maxHeight: z + .number() + .describe( + 'The maximum height of generated images. If omitted, then the max height is controlled by the model.' + ) + .int() + .positive() + .optional(), + maxSteps: z + .number() + .describe( + 'The maximum number of steps that can be used to generate an image. If omitted, then the max steps is controlled by the model.' + ) + .int() + .positive() + .optional(), + maxImages: z + .number() + .describe( + 'The maximum number of images that can be generated in a single request. If omitted, then the max images is controlled by the model.' + ) + .int() + .positive() + .optional(), + allowedModels: z + .object({ + openai: z + .array(z.string().nonempty()) + .describe( + 'The list of OpenAI DALL-E models that are allowed to be used. If omitted, then no OpenAI models are allowed.' + ) + .optional(), + stabilityai: z + .array(z.string().nonempty()) + .describe( + 'The list of StabilityAI models that are allowed to be used. If omitted, then no StabilityAI models are allowed.' + ) + .optional(), + }) + .describe( + 'The models that are allowed to be used from each provider.' + ), + allowedSubscriptionTiers: z + .union([z.literal(true), z.array(z.string().nonempty())]) + .describe( + 'The subscription tiers that are allowed to use Image AI. If true, then all tiers are allowed.' + ), }) + .describe( + 'Options for Image AI. If omitted, then Image AI is disabled.' + ) .optional(), }); const apiGatewaySchema = z.object({ - endpoint: z.string(), + endpoint: z + .string() + .describe( + 'The API Gateway endpoint that should be used for sending messages to connected clients.' + ), }); const wsSchema = z.object({}); export const optionsSchema = z.object({ - dynamodb: dynamoDbSchema.optional(), - s3: s3Schema.optional(), - apiGateway: apiGatewaySchema.optional(), - mongodb: mongodbSchema.optional(), - prisma: prismaSchema.optional(), - livekit: livekitSchema.optional(), - textIt: textItSchema.optional(), - ses: sesSchema.optional(), - redis: redisSchema.optional(), - rateLimit: rateLimitSchema.optional(), - openai: openAiSchema.optional(), - blockadeLabs: blockadeLabsSchema.optional(), - stabilityai: stabilityAiSchema.optional(), - ai: aiSchema.optional(), - ws: wsSchema.optional(), - - subscriptions: subscriptionConfigSchema.optional(), - stripe: stripeSchema.optional(), + s3: s3Schema + .describe( + 'S3 Configuration Options. If omitted, then S3 cannot be used for file storage.' + ) + .optional(), + apiGateway: apiGatewaySchema + .describe( + 'AWS API Gateway configuration options. If omitted, then inst records cannot be used on AWS Lambda.' + ) + .optional(), + mongodb: mongodbSchema + .describe( + 'MongoDB configuration options. If omitted, then MongoDB cannot be used.' + ) + .optional(), + prisma: prismaSchema + .describe( + 'Prisma configuration options. If omitted, then Prisma (CockroachDB) cannot be used.' + ) + .optional(), + livekit: livekitSchema + .describe( + 'Livekit configuration options. If omitted, then Livekit features will be disabled.' + ) + .optional(), + textIt: textItSchema + .describe( + 'TextIt configuration options. If omitted, then SMS login will be disabled.' + ) + .optional(), + ses: sesSchema + .describe( + 'AWS SES configuration options. If omitted, then sending login codes via SES is not possible.' + ) + .optional(), + redis: redisSchema + .describe( + 'Redis configuration options. If omitted, then using Redis is not possible.' + ) + .optional(), + rateLimit: rateLimitSchema + .describe( + 'Rate limit options. If omitted, then rate limiting will be disabled.' + ) + .optional(), + openai: openAiSchema + .describe( + 'OpenAI options. If omitted, then it will not be possible to use GPT or DALL-E.' + ) + .optional(), + blockadeLabs: blockadeLabsSchema + .describe( + 'Blockade Labs options. If omitted, then it will not be possible to generate skyboxes.' + ) + .optional(), + stabilityai: stabilityAiSchema + .describe( + 'Stability AI options. If omitted, then it will not be possible to use Stable Diffusion.' + ) + .optional(), + ai: aiSchema + .describe( + 'AI configuration options. If omitted, then all AI features will be disabled.' + ) + .optional(), + ws: wsSchema + .describe( + 'WebSocket Server configuration options. If omitted, then inst records cannot be used in standalone deployments.' + ) + .optional(), + + subscriptions: subscriptionConfigSchema + .describe( + 'The default subscription configuration. If omitted, then subscription features will be disabled.' + ) + .optional(), + stripe: stripeSchema + .describe( + 'Stripe options. If omitted, then Stripe features will be disabled.' + ) + .optional(), }); -export type DynamoDBConfig = z.infer; export type S3Config = z.infer; export type BuilderOptions = z.infer;