@volter/twin-upstash 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +202 -0
- package/api/src/fetch.ts +54 -0
- package/api/src/generated/surface.gen.json +1 -0
- package/api/src/generated/ui.gen.json +1 -0
- package/api/src/index.ts +19 -0
- package/api/src/key-gate.ts +30 -0
- package/api/src/manifest.ts +103 -0
- package/api/src/screens/developer-api.tsx +106 -0
- package/api/src/screens/qstash.tsx +99 -0
- package/api/src/screens/session.tsx +125 -0
- package/api/src/screens/teams.tsx +114 -0
- package/api/src/semantics/backups.ts +90 -0
- package/api/src/semantics/index.ts +191 -0
- package/api/src/semantics/shared.ts +42 -0
- package/api/src/semantics/teams.ts +108 -0
- package/api/src/semantics/time.ts +40 -0
- package/dist/api/src/fetch.d.ts +15 -0
- package/dist/api/src/fetch.js +44 -0
- package/dist/api/src/fetch.ts +54 -0
- package/dist/api/src/generated/surface.gen.json +1 -0
- package/dist/api/src/generated/ui.gen.json +1 -0
- package/dist/api/src/index.ts +19 -0
- package/dist/api/src/key-gate.d.ts +3 -0
- package/dist/api/src/key-gate.js +30 -0
- package/dist/api/src/key-gate.ts +30 -0
- package/dist/api/src/manifest.d.ts +2 -0
- package/dist/api/src/manifest.js +81 -0
- package/dist/api/src/manifest.ts +103 -0
- package/dist/api/src/screens/developer-api.d.ts +3 -0
- package/dist/api/src/screens/developer-api.js +101 -0
- package/dist/api/src/screens/developer-api.tsx +106 -0
- package/dist/api/src/screens/qstash.d.ts +3 -0
- package/dist/api/src/screens/qstash.js +92 -0
- package/dist/api/src/screens/qstash.tsx +99 -0
- package/dist/api/src/screens/session.d.ts +9 -0
- package/dist/api/src/screens/session.js +118 -0
- package/dist/api/src/screens/session.tsx +125 -0
- package/dist/api/src/screens/teams.d.ts +3 -0
- package/dist/api/src/screens/teams.js +99 -0
- package/dist/api/src/screens/teams.tsx +114 -0
- package/dist/api/src/semantics/backups.d.ts +7 -0
- package/dist/api/src/semantics/backups.js +75 -0
- package/dist/api/src/semantics/backups.ts +90 -0
- package/dist/api/src/semantics/index.d.ts +10 -0
- package/dist/api/src/semantics/index.js +191 -0
- package/dist/api/src/semantics/index.ts +191 -0
- package/dist/api/src/semantics/shared.d.ts +21 -0
- package/dist/api/src/semantics/shared.js +34 -0
- package/dist/api/src/semantics/shared.ts +42 -0
- package/dist/api/src/semantics/teams.d.ts +13 -0
- package/dist/api/src/semantics/teams.js +100 -0
- package/dist/api/src/semantics/teams.ts +108 -0
- package/dist/api/src/semantics/time.d.ts +2 -0
- package/dist/api/src/semantics/time.js +34 -0
- package/dist/api/src/semantics/time.ts +40 -0
- package/dist/qstash/src/doors.d.ts +6 -0
- package/dist/qstash/src/doors.js +33 -0
- package/dist/qstash/src/doors.ts +51 -0
- package/dist/qstash/src/egress.d.ts +7 -0
- package/dist/qstash/src/egress.js +66 -0
- package/dist/qstash/src/egress.ts +58 -0
- package/dist/qstash/src/fetch.d.ts +7 -0
- package/dist/qstash/src/fetch.js +48 -0
- package/dist/qstash/src/fetch.ts +46 -0
- package/dist/qstash/src/generated/surface.gen.json +1 -0
- package/dist/qstash/src/generated/ui.gen.json +1 -0
- package/dist/qstash/src/index.ts +35 -0
- package/dist/qstash/src/manifest.d.ts +10 -0
- package/dist/qstash/src/manifest.js +105 -0
- package/dist/qstash/src/manifest.ts +134 -0
- package/dist/qstash/src/semantics/account.d.ts +29 -0
- package/dist/qstash/src/semantics/account.js +91 -0
- package/dist/qstash/src/semantics/account.ts +98 -0
- package/dist/qstash/src/semantics/delivery.d.ts +17 -0
- package/dist/qstash/src/semantics/delivery.js +274 -0
- package/dist/qstash/src/semantics/delivery.ts +264 -0
- package/dist/qstash/src/semantics/dlq.d.ts +4 -0
- package/dist/qstash/src/semantics/dlq.js +51 -0
- package/dist/qstash/src/semantics/dlq.ts +61 -0
- package/dist/qstash/src/semantics/index.d.ts +2 -0
- package/dist/qstash/src/semantics/index.js +10 -0
- package/dist/qstash/src/semantics/index.ts +13 -0
- package/dist/qstash/src/semantics/keys.d.ts +2 -0
- package/dist/qstash/src/semantics/keys.js +9 -0
- package/dist/qstash/src/semantics/keys.ts +14 -0
- package/dist/qstash/src/semantics/messages.d.ts +74 -0
- package/dist/qstash/src/semantics/messages.js +233 -0
- package/dist/qstash/src/semantics/messages.ts +249 -0
- package/dist/qstash/src/semantics/queues.d.ts +2 -0
- package/dist/qstash/src/semantics/queues.js +60 -0
- package/dist/qstash/src/semantics/queues.ts +66 -0
- package/dist/qstash/src/semantics/schedules.d.ts +19 -0
- package/dist/qstash/src/semantics/schedules.js +125 -0
- package/dist/qstash/src/semantics/schedules.ts +132 -0
- package/dist/qstash/src/semantics/shared.d.ts +45 -0
- package/dist/qstash/src/semantics/shared.js +115 -0
- package/dist/qstash/src/semantics/shared.ts +121 -0
- package/dist/qstash/src/semantics/urlgroups.d.ts +2 -0
- package/dist/qstash/src/semantics/urlgroups.js +58 -0
- package/dist/qstash/src/semantics/urlgroups.ts +69 -0
- package/dist/qstash/src/semantics/workflows.d.ts +44 -0
- package/dist/qstash/src/semantics/workflows.js +379 -0
- package/dist/qstash/src/semantics/workflows.ts +401 -0
- package/dist/qstash/src/signing.d.ts +4 -0
- package/dist/qstash/src/signing.js +16 -0
- package/dist/qstash/src/signing.ts +19 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +35 -0
- package/dist/src/generated/surface.gen.json +1 -0
- package/dist/src/index.d.ts +18 -0
- package/dist/src/index.js +124 -0
- package/dist/src/manifest.d.ts +14 -0
- package/dist/src/manifest.js +8 -0
- package/dist/src/upstash-budget.d.ts +85 -0
- package/dist/src/upstash-budget.js +440 -0
- package/dist/src/upstash-capabilities.d.ts +4 -0
- package/dist/src/upstash-capabilities.js +1286 -0
- package/dist/src/upstash-conformance.d.ts +7 -0
- package/dist/src/upstash-conformance.js +119 -0
- package/dist/src/upstash-connector.d.ts +115 -0
- package/dist/src/upstash-connector.js +309 -0
- package/dist/src/upstash-lua.d.ts +140 -0
- package/dist/src/upstash-lua.js +1229 -0
- package/dist/src/upstash-server.d.ts +29 -0
- package/dist/src/upstash-server.js +81 -0
- package/dist/src/upstash-store.d.ts +114 -0
- package/dist/src/upstash-store.js +1663 -0
- package/dist/src/upstash-twin.d.ts +73 -0
- package/dist/src/upstash-twin.js +437 -0
- package/package.json +59 -0
- package/qstash/src/doors.ts +51 -0
- package/qstash/src/egress.ts +58 -0
- package/qstash/src/fetch.ts +46 -0
- package/qstash/src/generated/surface.gen.json +1 -0
- package/qstash/src/generated/ui.gen.json +1 -0
- package/qstash/src/index.ts +35 -0
- package/qstash/src/manifest.ts +134 -0
- package/qstash/src/semantics/account.ts +98 -0
- package/qstash/src/semantics/delivery.ts +264 -0
- package/qstash/src/semantics/dlq.ts +61 -0
- package/qstash/src/semantics/index.ts +13 -0
- package/qstash/src/semantics/keys.ts +14 -0
- package/qstash/src/semantics/messages.ts +249 -0
- package/qstash/src/semantics/queues.ts +66 -0
- package/qstash/src/semantics/schedules.ts +132 -0
- package/qstash/src/semantics/shared.ts +121 -0
- package/qstash/src/semantics/urlgroups.ts +69 -0
- package/qstash/src/semantics/workflows.ts +401 -0
- package/qstash/src/signing.ts +19 -0
- package/src/cli.ts +36 -0
- package/src/generated/surface.gen.json +1 -0
- package/src/index.ts +203 -0
- package/src/manifest.ts +26 -0
- package/src/upstash-budget.ts +486 -0
- package/src/upstash-capabilities.ts +1418 -0
- package/src/upstash-conformance.ts +131 -0
- package/src/upstash-connector.ts +340 -0
- package/src/upstash-lua.ts +1120 -0
- package/src/upstash-server.ts +103 -0
- package/src/upstash-store.ts +1437 -0
- package/src/upstash-twin.ts +465 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"generatedBy":"scripts/derive-pack.ts","spec":"openapi.yaml.gz","format":"openapi","version":"1.0.0","basePath":"/v2","resources":[{"name":"AuditLog","schema":"AuditLog","fields":[{"name":"action","type":"integer","nullable":false},{"name":"action_string","type":"string","nullable":false},{"name":"actor","type":"string","nullable":false},{"name":"customer_id","type":"string","nullable":false},{"name":"entity","type":"string","nullable":false},{"name":"ip","type":"string","nullable":false},{"name":"log_id","type":"string","nullable":false},{"name":"notes","type":"string","nullable":false},{"name":"readable_format","type":"string","nullable":false},{"name":"reason","type":"string","nullable":false},{"name":"secondary_entity","type":"string","nullable":false},{"name":"source","type":"string","nullable":false},{"name":"third_entity","type":"string","nullable":false},{"name":"timestamp","type":"integer","nullable":false},{"name":"ttl","type":"integer","nullable":false}],"stateCandidates":[],"expandable":[]},{"name":"Backup","schema":"Backup","fields":[{"name":"backup_id","type":"string","nullable":false},{"name":"backup_size","type":"integer","nullable":false},{"name":"creation_time","type":"integer","nullable":false},{"name":"customer_id","type":"string","nullable":false},{"name":"daily_backup","type":"string","nullable":false,"enum":["true","false"]},{"name":"database_id","type":"string","nullable":false},{"name":"hourly_backup","type":"string","nullable":false,"enum":["true","false"]},{"name":"internal_backup_tag","type":"string","nullable":false},{"name":"name","type":"string","nullable":false},{"name":"state","type":"string","nullable":false,"enum":["pending","completed","failed"]}],"stateCandidates":[{"field":"daily_backup","values":["false","true"]},{"field":"hourly_backup","values":["false","true"]},{"field":"state","values":["completed","failed","pending"]}],"expandable":[]},{"name":"Database","schema":"Database","fields":[{"name":"all_members","type":"array","nullable":false},{"name":"auto_upgrade","type":"boolean","nullable":false},{"name":"budget","type":"integer","nullable":false},{"name":"consistent","type":"boolean","nullable":false},{"name":"creation_time","type":"integer","nullable":false},{"name":"customer_id","type":"string","nullable":false},{"name":"daily_backup_enabled","type":"boolean","nullable":false},{"name":"database_id","type":"string","nullable":false},{"name":"database_name","type":"string","nullable":false},{"name":"db_acl_default_user_status","type":"string","nullable":false,"enum":["true","false"]},{"name":"db_acl_enabled","type":"string","nullable":false,"enum":["true","false"]},{"name":"db_conn_idle_timeout","type":"integer","nullable":false},{"name":"db_disk_threshold","type":"integer","nullable":false},{"name":"db_eviction","type":"boolean","nullable":false},{"name":"db_lua_credits_per_min","type":"integer","nullable":false},{"name":"db_lua_timeout","type":"integer","nullable":false},{"name":"db_max_clients","type":"integer","nullable":false},{"name":"db_max_commands_per_second","type":"integer","nullable":false},{"name":"db_max_entry_size","type":"integer","nullable":false},{"name":"db_max_loads_per_sec","type":"integer","nullable":false},{"name":"db_max_request_size","type":"integer","nullable":false},{"name":"db_memory_threshold","type":"integer","nullable":false},{"name":"db_request_limit","type":"integer","nullable":false},{"name":"db_resource_size","type":"string","nullable":false,"enum":["S","M","L","XL","XXL","3XL"]},{"name":"db_store_max_idle","type":"integer","nullable":false},{"name":"db_type","type":"string","nullable":false,"enum":["bolt","badger","pebble"]},{"name":"endpoint","type":"string","nullable":false},{"name":"eviction","type":"boolean","nullable":false},{"name":"last_plan_upgrade_time","type":"integer","nullable":false},{"name":"modifying_state","type":"string","nullable":false},{"name":"password","type":"string","nullable":false},{"name":"port","type":"integer","nullable":false},{"name":"primary_members","type":"array","nullable":false},{"name":"primary_region","type":"string","nullable":false,"enum":["us-east-1","us-east-2","us-west-1","us-west-2","ca-central-1","eu-central-1","eu-west-1","eu-west-2","sa-east-1","ap-south-1","ap-northeast-1","ap-southeast-1","ap-southeast-2","af-south-1","us-central1","us-east4","europe-west1","asia-northeast1"]},{"name":"prod_pack_enabled","type":"boolean","nullable":false},{"name":"prometheus_enabled","type":"string","nullable":false,"enum":["true","false"]},{"name":"read_only_rest_token","type":"string","nullable":false},{"name":"read_regions","type":"array","nullable":false},{"name":"region","type":"string","nullable":false,"enum":["global"]},{"name":"replicas","type":"integer","nullable":false},{"name":"rest_token","type":"string","nullable":false},{"name":"securityAddons","type":"object","nullable":false},{"name":"state","type":"string","nullable":false,"enum":["active","suspended","passive"]},{"name":"tls","type":"boolean","nullable":false},{"name":"type","type":"string","nullable":false,"enum":["free","payg","pro","paid"]}],"stateCandidates":[{"field":"db_acl_default_user_status","values":["false","true"]},{"field":"db_acl_enabled","values":["false","true"]},{"field":"db_resource_size","values":["3XL","L","M","S","XL","XXL"]},{"field":"db_type","values":["badger","bolt","pebble"]},{"field":"primary_region","values":["af-south-1","ap-northeast-1","ap-south-1","ap-southeast-1","ap-southeast-2","asia-northeast1","ca-central-1","eu-central-1","eu-west-1","eu-west-2","europe-west1","sa-east-1","us-central1","us-east-1","us-east-2","us-east4","us-west-1","us-west-2"]},{"field":"prometheus_enabled","values":["false","true"]},{"field":"state","values":["active","passive","suspended"]},{"field":"type","values":["free","paid","payg","pro"]}],"expandable":[]},{"name":"QStashUser","schema":"QStashUser","fields":[{"name":"active","type":"boolean","nullable":false},{"name":"budget","type":"integer","nullable":false},{"name":"created_by","type":"string","nullable":false},{"name":"creation_time","type":"integer","nullable":false},{"name":"customer_id","type":"string","nullable":false},{"name":"deletion_time","type":"integer","nullable":false},{"name":"enterprise","type":"object","nullable":false},{"name":"id","type":"string","nullable":false},{"name":"last_plan_upgrade_time","type":"integer","nullable":false},{"name":"max_bandwidth_per_second","type":"integer","nullable":false},{"name":"max_delay","type":"integer","nullable":false},{"name":"max_dlq_retention_time_milis","type":"integer","nullable":false},{"name":"max_dlq_size","type":"integer","nullable":false},{"name":"max_endpoints_per_topic","type":"integer","nullable":false},{"name":"max_events_size","type":"integer","nullable":false},{"name":"max_global_parallelism","type":"integer","nullable":false},{"name":"max_message_size","type":"integer","nullable":false},{"name":"max_parallelism","type":"integer","nullable":false},{"name":"max_queues","type":"integer","nullable":false},{"name":"max_requests_per_day","type":"integer","nullable":false},{"name":"max_requests_per_day_hard","type":"integer","nullable":false},{"name":"max_requests_per_second","type":"integer","nullable":false},{"name":"max_retries","type":"integer","nullable":false},{"name":"max_schedules","type":"integer","nullable":false},{"name":"max_topics","type":"integer","nullable":false},{"name":"modifying_state","type":"string","nullable":false},{"name":"prod_pack_enabled","type":"boolean","nullable":false},{"name":"prometheus_enabled","type":"string","nullable":false,"enum":["true","false"]},{"name":"read_only_token","type":"string","nullable":false},{"name":"region","type":"string","nullable":false,"enum":["eu-central-1","us-east-1"]},{"name":"reserved_price","type":"integer","nullable":false},{"name":"reserved_type","type":"string","nullable":false},{"name":"state","type":"string","nullable":false},{"name":"timeout","type":"integer","nullable":false},{"name":"token","type":"string","nullable":false},{"name":"type","type":"string","nullable":false,"enum":["free","paid"]}],"stateCandidates":[{"field":"active","values":["false","true"]},{"field":"prometheus_enabled","values":["false","true"]},{"field":"region","values":["eu-central-1","us-east-1"]},{"field":"state","values":[]},{"field":"type","values":["free","paid"]}],"expandable":[]},{"name":"SearchIndex","schema":"SearchIndex","fields":[{"name":"creation_time","type":"integer","nullable":false},{"name":"customer_id","type":"string","nullable":false},{"name":"endpoint","type":"string","nullable":false},{"name":"id","type":"string","nullable":false},{"name":"input_enrichment_enabled","type":"boolean","nullable":false},{"name":"max_daily_queries","type":"integer","nullable":false},{"name":"max_daily_updates","type":"integer","nullable":false},{"name":"max_monthly_bandwidth","type":"integer","nullable":false},{"name":"max_monthly_reranks","type":"integer","nullable":false},{"name":"max_query_per_second","type":"integer","nullable":false},{"name":"max_reads_per_request","type":"integer","nullable":false},{"name":"max_vector_count","type":"integer","nullable":false},{"name":"max_writes_per_request","type":"integer","nullable":false},{"name":"max_writes_per_second","type":"integer","nullable":false},{"name":"name","type":"string","nullable":false},{"name":"read_only_token","type":"string","nullable":false},{"name":"region","type":"string","nullable":false,"enum":["eu-west-1","us-central1"]},{"name":"throughput_vector","type":"array","nullable":false},{"name":"token","type":"string","nullable":false},{"name":"type","type":"string","nullable":false,"enum":["free","payg","fixed"]},{"name":"vercel_email","type":"string","nullable":false}],"stateCandidates":[{"field":"region","values":["eu-west-1","us-central1"]},{"field":"type","values":["fixed","free","payg"]}],"expandable":[]},{"name":"Team","schema":"Team","fields":[{"name":"copy_cc","type":"boolean","nullable":false},{"name":"team_id","type":"string","nullable":false},{"name":"team_name","type":"string","nullable":false}],"stateCandidates":[],"expandable":[]},{"name":"TeamMember","schema":"TeamMember","fields":[{"name":"copy_cc","type":"boolean","nullable":false},{"name":"member_email","type":"string","nullable":false},{"name":"member_role","type":"string","nullable":false,"enum":["owner","admin","dev","finance"]},{"name":"team_id","type":"string","nullable":false},{"name":"team_name","type":"string","nullable":false}],"stateCandidates":[{"field":"member_role","values":["admin","dev","finance","owner"]}],"expandable":[]},{"name":"VectorIndex","schema":"VectorIndex","fields":[{"name":"creation_time","type":"number","nullable":false},{"name":"customer_id","type":"string","nullable":false},{"name":"dimension_count","type":"number","nullable":false},{"name":"embedding_model","type":"string","nullable":false,"enum":["BGE_SMALL_EN_V1_5","BGE_BASE_EN_V1_5","BGE_LARGE_EN_V1_5","BGE_M3"]},{"name":"endpoint","type":"string","nullable":false},{"name":"id","type":"string","nullable":false},{"name":"index_type","type":"string","nullable":false,"enum":["DENSE","SPARSE","HYBRID"]},{"name":"max_daily_queries","type":"number","nullable":false},{"name":"max_daily_updates","type":"number","nullable":false},{"name":"max_monthly_bandwidth","type":"number","nullable":false},{"name":"max_query_per_second","type":"number","nullable":false},{"name":"max_reads_per_request","type":"number","nullable":false},{"name":"max_total_metadata_size","type":"number","nullable":false},{"name":"max_vector_count","type":"number","nullable":false},{"name":"max_writes_per_request","type":"number","nullable":false},{"name":"max_writes_per_second","type":"number","nullable":false},{"name":"name","type":"string","nullable":false},{"name":"read_only_token","type":"string","nullable":false},{"name":"region","type":"string","nullable":false,"enum":["eu-west-1","us-east-1","us-central1"]},{"name":"reserved_price","type":"number","nullable":false},{"name":"similarity_function","type":"string","nullable":false,"enum":["COSINE","EUCLIDEAN","DOT_PRODUCT"]},{"name":"sparse_embedding_model","type":"string","nullable":false,"enum":["BM25","BGE_M3"]},{"name":"throughput_vector","type":"array","nullable":false},{"name":"token","type":"string","nullable":false},{"name":"type","type":"string","nullable":false,"enum":["free","payg","fixed"]}],"stateCandidates":[{"field":"embedding_model","values":["BGE_BASE_EN_V1_5","BGE_LARGE_EN_V1_5","BGE_M3","BGE_SMALL_EN_V1_5"]},{"field":"index_type","values":["DENSE","HYBRID","SPARSE"]},{"field":"region","values":["eu-west-1","us-central1","us-east-1"]},{"field":"similarity_function","values":["COSINE","DOT_PRODUCT","EUCLIDEAN"]},{"field":"sparse_embedding_model","values":["BGE_M3","BM25"]},{"field":"type","values":["fixed","free","payg"]}],"expandable":[]}],"operations":[{"id":"listAuditLogs","method":"get","path":"/auditlogs","pathParams":[],"query":[],"basePath":"","body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"AuditLog","list":true},"class":"list","resource":"AuditLog"},{"id":"disableQStashProdPack","method":"post","path":"/qstash/disable-prodpack/{id}","pathParams":["id"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"non-resource"},{"id":"enableQStashProdPack","method":"post","path":"/qstash/enable-prodpack/{id}","pathParams":["id"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"non-resource"},{"id":"getQStashIPv4","method":"get","path":"/qstash/ipv4","pathParams":[],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"moveQStashToTeam","method":"post","path":"/qstash/move-to-team","pathParams":[],"query":[],"body":[{"name":"qstash_id","type":"string","required":true},{"name":"target_team_id","type":"string","required":true}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"resetQStashToken","method":"post","path":"/qstash/rotate-token/{id}","pathParams":["id"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"QStashUser","list":false},"class":"action","resource":"QStashUser"},{"id":"setQStashPlan","method":"post","path":"/qstash/set-plan/{id}","pathParams":["id"],"query":[],"body":[{"name":"plan_name","type":"string","required":true}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"getQStashStats","method":"get","path":"/qstash/stats/{id}","pathParams":["id"],"query":[{"name":"period","type":"string","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"updateQStashBudget","method":"patch","path":"/qstash/update-budget/{id}","pathParams":["id"],"query":[],"body":[{"name":"budget","type":"integer","required":true}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"getQStashUser","method":"get","path":"/qstash/user/{id}","pathParams":["id"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"QStashUser","list":false},"class":"retrieve","resource":"QStashUser"},{"id":"listQStashUsers","method":"get","path":"/qstash/users","pathParams":[],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"QStashUser","list":true},"class":"list","resource":"QStashUser"},{"id":"changePlan","method":"post","path":"/redis/{id}/change-plan","pathParams":["id"],"query":[],"body":[{"name":"auto_upgrade","type":"boolean","required":false},{"name":"database_id","type":"string","required":false},{"name":"plan_name","type":"string","required":true},{"name":"prod_pack_enabled","type":"boolean","required":false}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"createBackup","method":"post","path":"/redis/create-backup/{id}","pathParams":["id"],"query":[],"body":[{"name":"name","type":"string","required":true}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"createDatabase","method":"post","path":"/redis/database","pathParams":[],"query":[],"body":[{"name":"budget","type":"integer","required":false},{"name":"database_name","type":"string","required":true},{"name":"eviction","type":"boolean","required":false},{"name":"plan","type":"string","required":false},{"name":"platform","type":"string","required":false},{"name":"primary_region","type":"string","required":true},{"name":"read_regions","type":"array","required":false},{"name":"region","type":"string","required":false},{"name":"tls","type":"boolean","required":false}],"bodyEncoding":"json","successStatus":200,"answers":{"resource":"Database","list":false},"class":"create","resource":"Database"},{"id":"getDatabase","method":"get","path":"/redis/database/{id}","pathParams":["id"],"query":[{"name":"credentials","type":"string","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"Database","list":false},"class":"retrieve","resource":"Database"},{"id":"deleteDatabase","method":"delete","path":"/redis/database/{id}","pathParams":["id"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"delete","resource":"Database"},{"id":"listDatabases","method":"get","path":"/redis/databases","pathParams":[],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"Database","list":true},"class":"list","resource":"Database"},{"id":"deleteBackup","method":"delete","path":"/redis/delete-backup/{id}/{backup_id}","pathParams":["id","backup_id"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"delete"},{"id":"disableAutoUpgrade","method":"post","path":"/redis/disable-autoupgrade/{id}","pathParams":["id"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"non-resource"},{"id":"disableDailyBackup","method":"patch","path":"/redis/disable-dailybackup/{id}","pathParams":["id"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"non-resource"},{"id":"disableEviction","method":"post","path":"/redis/disable-eviction/{id}","pathParams":["id"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"non-resource"},{"id":"enableAutoUpgrade","method":"post","path":"/redis/enable-autoupgrade/{id}","pathParams":["id"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"non-resource"},{"id":"enableDailyBackup","method":"patch","path":"/redis/enable-dailybackup/{id}","pathParams":["id"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"non-resource"},{"id":"enableEviction","method":"post","path":"/redis/enable-eviction/{id}","pathParams":["id"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"non-resource"},{"id":"enableTls","method":"post","path":"/redis/enable-tls/{id}","pathParams":["id"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"Database","list":false},"class":"action","resource":"Database"},{"id":"listBackup","method":"get","path":"/redis/list-backup/{id}","pathParams":["id"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"Backup","list":true},"class":"list","resource":"Backup"},{"id":"moveToTeam","method":"post","path":"/redis/move-to-team","pathParams":[],"query":[],"body":[{"name":"database_id","type":"string","required":true},{"name":"team_id","type":"string","required":true}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"renameDatabase","method":"post","path":"/redis/rename/{id}","pathParams":["id"],"query":[],"body":[{"name":"name","type":"string","required":true}],"bodyEncoding":"json","successStatus":200,"answers":{"resource":"Database","list":false},"class":"action","resource":"Database"},{"id":"resetPassword","method":"post","path":"/redis/reset-password/{id}","pathParams":["id"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"Database","list":false},"class":"action","resource":"Database"},{"id":"restoreBackup","method":"post","path":"/redis/restore-backup/{id}","pathParams":["id"],"query":[],"body":[{"name":"backup_id","type":"string","required":true}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"getDatabaseStats","method":"get","path":"/redis/stats/{id}","pathParams":["id"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"updateBudget","method":"patch","path":"/redis/update-budget/{id}","pathParams":["id"],"query":[],"body":[{"name":"budget","type":"integer","required":true}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"updateRegions","method":"post","path":"/redis/update-regions/{id}","pathParams":["id"],"query":[],"body":[{"name":"read_regions","type":"array","required":true}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"listSearchIndexes","method":"get","path":"/search","pathParams":[],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"SearchIndex","list":true},"class":"computed","resource":"SearchIndex"},{"id":"createSearchIndex","method":"post","path":"/search","pathParams":[],"query":[],"body":[{"name":"name","type":"string","required":true},{"name":"region","type":"string","required":true},{"name":"type","type":"string","required":true}],"bodyEncoding":"json","successStatus":200,"answers":{"resource":"SearchIndex","list":false},"class":"create","resource":"SearchIndex"},{"id":"getSearchIndex","method":"get","path":"/search/{id}","pathParams":["id"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"SearchIndex","list":false},"class":"retrieve","resource":"SearchIndex"},{"id":"deleteSearchIndex","method":"delete","path":"/search/{id}","pathParams":["id"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"delete","resource":"SearchIndex"},{"id":"renameSearchIndex","method":"post","path":"/search/{id}/rename","pathParams":["id"],"query":[],"body":[{"name":"name","type":"string","required":true}],"bodyEncoding":"json","successStatus":200,"class":"action","resource":"SearchIndex"},{"id":"resetSearchPassword","method":"post","path":"/search/{id}/reset-password","pathParams":["id"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action","resource":"SearchIndex"},{"id":"getSearchIndexStats","method":"get","path":"/search/{id}/stats","pathParams":["id"],"query":[{"name":"period","type":"string","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed","resource":"SearchIndex"},{"id":"transferSearchIndex","method":"post","path":"/search/{id}/transfer","pathParams":["id"],"query":[],"body":[{"name":"target_account","type":"string","required":true}],"bodyEncoding":"json","successStatus":200,"class":"action","resource":"SearchIndex"},{"id":"getGlobalSearchStats","method":"get","path":"/search/stats","pathParams":[],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"createTeam","method":"post","path":"/team","pathParams":[],"query":[],"body":[{"name":"copy_cc","type":"boolean","required":true},{"name":"team_name","type":"string","required":true}],"bodyEncoding":"json","successStatus":200,"answers":{"resource":"Team","list":false},"class":"action","resource":"Team"},{"id":"deleteTeam","method":"delete","path":"/team/{id}","pathParams":["id"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"delete"},{"id":"listTeams","method":"get","path":"/teams","pathParams":[],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"Team","list":true},"class":"list","resource":"Team"},{"id":"getTeamMembers","method":"get","path":"/teams/{team_id}","pathParams":["team_id"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"TeamMember","list":true},"class":"list","resource":"TeamMember"},{"id":"addTeamMember","method":"post","path":"/teams/member","pathParams":[],"query":[],"body":[{"name":"member_email","type":"string","required":true},{"name":"member_role","type":"string","required":true},{"name":"team_id","type":"string","required":true}],"bodyEncoding":"json","successStatus":200,"answers":{"resource":"TeamMember","list":false},"class":"action","resource":"TeamMember"},{"id":"deleteTeamMember","method":"delete","path":"/teams/member","pathParams":[],"query":[],"body":[{"name":"member_email","type":"string","required":true},{"name":"team_id","type":"string","required":true}],"bodyEncoding":"json","successStatus":200,"class":"action"},{"id":"listIndices","method":"get","path":"/vector/index","pathParams":[],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"VectorIndex","list":true},"class":"list","resource":"VectorIndex"},{"id":"createIndex","method":"post","path":"/vector/index","pathParams":[],"query":[],"body":[{"name":"dimension_count","type":"number","required":true},{"name":"embedding_model","type":"string","required":false},{"name":"index_type","type":"string","required":false},{"name":"name","type":"string","required":true},{"name":"region","type":"string","required":true},{"name":"similarity_function","type":"string","required":true},{"name":"sparse_embedding_model","type":"string","required":false},{"name":"type","type":"string","required":false}],"bodyEncoding":"json","successStatus":200,"answers":{"resource":"VectorIndex","list":false},"class":"create","resource":"VectorIndex"},{"id":"getIndex","method":"get","path":"/vector/index/{id}","pathParams":["id"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"VectorIndex","list":false},"class":"retrieve","resource":"VectorIndex"},{"id":"deleteIndex","method":"delete","path":"/vector/index/{id}","pathParams":["id"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"delete","resource":"VectorIndex"},{"id":"renameIndex","method":"post","path":"/vector/index/{id}/rename","pathParams":["id"],"query":[],"body":[{"name":"name","type":"string","required":true}],"bodyEncoding":"json","successStatus":200,"class":"action","resource":"VectorIndex"},{"id":"resetIndexPasswords","method":"post","path":"/vector/index/{id}/reset-password","pathParams":["id"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action","resource":"VectorIndex"},{"id":"setIndexPlan","method":"post","path":"/vector/index/{id}/setplan","pathParams":["id"],"query":[],"body":[{"name":"target_plan","type":"string","required":true}],"bodyEncoding":"json","successStatus":200,"class":"action","resource":"VectorIndex"},{"id":"getVectorIndexStats","method":"get","path":"/vector/index/{id}/stats","pathParams":["id"],"query":[{"name":"period","type":"string","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed","resource":"VectorIndex"},{"id":"transferIndex","method":"post","path":"/vector/index/{id}/transfer","pathParams":["id"],"query":[],"body":[{"name":"target_account","type":"string","required":true}],"bodyEncoding":"json","successStatus":200,"class":"action","resource":"VectorIndex"},{"id":"getGlobalVectorStats","method":"get","path":"/vector/index/stats","pathParams":[],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"}]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"generatedBy":"scripts/derive-pack.ts","resources":{"Database":{"retrieve":"/v2/redis/database/{id}","create":{"path":"/v2/redis/database","encoding":"json","body":[{"name":"budget","type":"integer","required":false},{"name":"database_name","type":"string","required":true},{"name":"eviction","type":"boolean","required":false},{"name":"plan","type":"string","required":false},{"name":"platform","type":"string","required":false},{"name":"primary_region","type":"string","required":true},{"name":"read_regions","type":"array","required":false},{"name":"region","type":"string","required":false},{"name":"tls","type":"boolean","required":false}]},"actions":[],"states":{},"embeds":{}},"Backup":{"list":"/v2/redis/list-backup/{id}","actions":[],"states":{"state":["completed","pending"]},"embeds":{}},"Team":{"actions":[],"states":{},"embeds":{}},"TeamMember":{"actions":[],"states":{},"embeds":{}}}}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// The Developer API lane of the upstash pack (api.upstash.com and console.upstash.com), as a plain fetch: the whole pack's
|
|
2
|
+
// fetch (the Redis REST wire, the Developer API and the console's pages, and QStash), whose owners are the operations this
|
|
3
|
+
// lane serves. Its life walks through all of them: a customer makes its API keys on the console, makes and changes its
|
|
4
|
+
// databases, backups and team through the Developer API, and its game reads and writes the databases through the REST API
|
|
5
|
+
// and publishes through QStash (journeys/customer-life.outline.md).
|
|
6
|
+
import type { DerivedFetch } from '@volter/world-core';
|
|
7
|
+
import { createUpstashRedisTwinFetch } from '../../src/upstash-server.ts';
|
|
8
|
+
import { createUpstashApiLaneFetch } from './fetch.ts';
|
|
9
|
+
|
|
10
|
+
export { manifest } from './manifest.ts';
|
|
11
|
+
export { createUpstashApiLaneFetch, API_HOST, CONSOLE_HOST, type UpstashRedisApiFetch } from './fetch.ts';
|
|
12
|
+
// how a journey reads a REST answer sent with Upstash-Encoding: base64: the pack's own view (../../src/index.ts)
|
|
13
|
+
export { answerViews } from '../../src/index.ts';
|
|
14
|
+
|
|
15
|
+
export function createUpstashApiTwinFetch(options: { root?: string; readOnly?: boolean } = {}): DerivedFetch {
|
|
16
|
+
const lane = createUpstashApiLaneFetch(options);
|
|
17
|
+
const pack = createUpstashRedisTwinFetch({ ...(options.root !== undefined ? { root: options.root } : {}), ...(options.readOnly !== undefined ? { readOnly: options.readOnly } : {}) });
|
|
18
|
+
return Object.assign((request: Request) => pack(request), { owners: () => lane.owners() });
|
|
19
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// The Developer API lane's key gate, in front of its dispatch. "Upstash API uses HTTP Basic authentication. You should
|
|
2
|
+
// pass EMAIL and API_KEY as basic authentication username and password respectively"
|
|
3
|
+
// (https://upstash.com/docs/devops/developer-api/authentication); a key is made on the console's Developer API tab with a
|
|
4
|
+
// name, a permission (Read/Write or Read Only) and an expiration (https://upstash.com/docs/devops/developer-api/introduction).
|
|
5
|
+
//
|
|
6
|
+
// The gate admits exactly an email and a key this lane issued, live and unexpired: a key made on the console's Developer
|
|
7
|
+
// API tab (screens/developer-api.tsx) or, for an account a World needs of its own, one the World issues through
|
|
8
|
+
// `POST /_twin/users/:email/api-key` (screens/session.tsx). Where the reference stops and the twin decides: anything
|
|
9
|
+
// else (no well-formed Basic credential, an email or key the lane never issued, a deleted or expired key) answers 401
|
|
10
|
+
// {code: unauthorized}; a Read Only key asking for anything but a read answers 403 {code: forbidden}.
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
const refuse = (status, code, message) => Response.json({ code, message }, { status });
|
|
13
|
+
/** The gate's answer for a request, or undefined when it may pass. */
|
|
14
|
+
export function keyGate(ctx, request) {
|
|
15
|
+
const m = /^basic\s+(\S+)$/i.exec(request.headers.get('authorization') ?? '');
|
|
16
|
+
const decoded = m ? Buffer.from(m[1], 'base64').toString('utf8') : '';
|
|
17
|
+
const colon = decoded.indexOf(':');
|
|
18
|
+
if (colon <= 0)
|
|
19
|
+
return refuse(401, 'unauthorized', 'Unauthorized: send EMAIL:API_KEY as HTTP Basic authentication');
|
|
20
|
+
const email = decoded.slice(0, colon).toLowerCase();
|
|
21
|
+
const key = decoded.slice(colon + 1);
|
|
22
|
+
const sha = createHash('sha256').update(key).digest('hex');
|
|
23
|
+
const held = ctx.rowsRaw('_api_key').find((k) => k.email === email && k.key_sha256 === sha && k.deleted !== true
|
|
24
|
+
&& (k.expires_at === null || String(k.expires_at) > ctx.occurredAt));
|
|
25
|
+
if (!held)
|
|
26
|
+
return refuse(401, 'unauthorized', 'Unauthorized: the API key is not valid');
|
|
27
|
+
if (held.permission === 'read_only' && request.method !== 'GET')
|
|
28
|
+
return refuse(403, 'forbidden', 'Forbidden: this API key is Read Only');
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// The Developer API lane's key gate, in front of its dispatch. "Upstash API uses HTTP Basic authentication. You should
|
|
2
|
+
// pass EMAIL and API_KEY as basic authentication username and password respectively"
|
|
3
|
+
// (https://upstash.com/docs/devops/developer-api/authentication); a key is made on the console's Developer API tab with a
|
|
4
|
+
// name, a permission (Read/Write or Read Only) and an expiration (https://upstash.com/docs/devops/developer-api/introduction).
|
|
5
|
+
//
|
|
6
|
+
// The gate admits exactly an email and a key this lane issued, live and unexpired: a key made on the console's Developer
|
|
7
|
+
// API tab (screens/developer-api.tsx) or, for an account a World needs of its own, one the World issues through
|
|
8
|
+
// `POST /_twin/users/:email/api-key` (screens/session.tsx). Where the reference stops and the twin decides: anything
|
|
9
|
+
// else (no well-formed Basic credential, an email or key the lane never issued, a deleted or expired key) answers 401
|
|
10
|
+
// {code: unauthorized}; a Read Only key asking for anything but a read answers 403 {code: forbidden}.
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import type { SemanticsContext } from '@volter/world-core';
|
|
13
|
+
|
|
14
|
+
const refuse = (status: number, code: string, message: string): Response => Response.json({ code, message }, { status });
|
|
15
|
+
|
|
16
|
+
/** The gate's answer for a request, or undefined when it may pass. */
|
|
17
|
+
export function keyGate(ctx: SemanticsContext, request: Request): Response | undefined {
|
|
18
|
+
const m = /^basic\s+(\S+)$/i.exec(request.headers.get('authorization') ?? '');
|
|
19
|
+
const decoded = m ? Buffer.from(m[1]!, 'base64').toString('utf8') : '';
|
|
20
|
+
const colon = decoded.indexOf(':');
|
|
21
|
+
if (colon <= 0) return refuse(401, 'unauthorized', 'Unauthorized: send EMAIL:API_KEY as HTTP Basic authentication');
|
|
22
|
+
const email = decoded.slice(0, colon).toLowerCase();
|
|
23
|
+
const key = decoded.slice(colon + 1);
|
|
24
|
+
const sha = createHash('sha256').update(key).digest('hex');
|
|
25
|
+
const held = ctx.rowsRaw('_api_key').find((k) => k.email === email && k.key_sha256 === sha && k.deleted !== true
|
|
26
|
+
&& (k.expires_at === null || String(k.expires_at) > ctx.occurredAt));
|
|
27
|
+
if (!held) return refuse(401, 'unauthorized', 'Unauthorized: the API key is not valid');
|
|
28
|
+
if (held.permission === 'read_only' && request.method !== 'GET') return refuse(403, 'forbidden', 'Forbidden: this API key is Read Only');
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** A backup's `state` (the Backup schema: pending, completed, failed). A backup is `pending` when it is made and
|
|
2
|
+
* `completed` two minutes later, on the World clock (semantics/time.ts): the duration is the twin's decision; Upstash's
|
|
3
|
+
* backup page says only that "Backup process will start and will appear in the backups table"
|
|
4
|
+
* (https://upstash.com/docs/redis/features/backup). Not made by the twin: failed. */
|
|
5
|
+
const backupState = {
|
|
6
|
+
initial: 'pending',
|
|
7
|
+
transitions: [
|
|
8
|
+
{ actor: 'time', from: ['pending'], to: 'completed', source: 'spec:/components/schemas/Backup/properties/state "State of the backup"' },
|
|
9
|
+
],
|
|
10
|
+
};
|
|
11
|
+
export const manifest = {
|
|
12
|
+
vendor: 'upstash',
|
|
13
|
+
service: 'upstash',
|
|
14
|
+
body: { json: 'always' },
|
|
15
|
+
// database ids are UUIDs (the Database schema's example, 96ad0856-03b1-4ee7-9666-e81abd0349e1); the handler mints one
|
|
16
|
+
// per database from the World's instant and name (semantics/index.ts), so a World mints the same ids every run
|
|
17
|
+
ids: { template: '{uuid}' },
|
|
18
|
+
// `creation_time`: "Creation time of the database as Unix time"
|
|
19
|
+
time: 'unix',
|
|
20
|
+
error: { code: '{code}', message: '{message}' },
|
|
21
|
+
readOnly: { status: 403, code: 'forbidden', message: 'The twin is read-only' },
|
|
22
|
+
malformedBody: { status: 400, code: 'bad_request', message: 'The request body is not valid JSON' },
|
|
23
|
+
notFound: { status: 404, code: 'not_found', message: 'database not found' },
|
|
24
|
+
// listDatabases answers a bare array, which the envelope cannot state; it is the gap. Required by the type.
|
|
25
|
+
list: { style: 'envelope', envelope: { data: '{data}' }, limit: { param: 'limit', default: 100, max: 100 } },
|
|
26
|
+
// deleteDatabase answers the string "OK" (its 200 schema: type string, example OK)
|
|
27
|
+
deleted: 'OK',
|
|
28
|
+
// the operations of a declared resource the lane does not serve: the gap
|
|
29
|
+
unmodeled: [
|
|
30
|
+
'listDatabases', 'renameDatabase', 'enableTls', 'enableEviction', 'disableEviction', 'enableAutoUpgrade',
|
|
31
|
+
'disableAutoUpgrade', 'changePlan', 'updateBudget', 'getDatabaseStats', 'listTeams', 'getTeamMembers',
|
|
32
|
+
],
|
|
33
|
+
screens: [{
|
|
34
|
+
id: 'sign-in', kind: 'flow', host: 'console.upstash.com', path: '/login',
|
|
35
|
+
demand: 'every console page a person reaches: each asks who is signed in', status: 'done',
|
|
36
|
+
controls: ['Email', 'Password', 'Sign in'], source: 'https://upstash.com/docs/devops/developer-api/introduction',
|
|
37
|
+
}, {
|
|
38
|
+
id: 'developer-api', kind: 'flow', host: 'console.upstash.com', path: '/account/api',
|
|
39
|
+
demand: 'the Developer API needs an API key, and a key is made only here',
|
|
40
|
+
status: 'done', controls: ['Create API Key', 'Name', 'Permissions', 'Expiration', 'Submit', 'Delete'],
|
|
41
|
+
source: 'https://upstash.com/docs/devops/developer-api/introduction',
|
|
42
|
+
}, {
|
|
43
|
+
id: 'qstash', kind: 'flow', host: 'console.upstash.com', path: '/qstash',
|
|
44
|
+
demand: "QStash's token and signing keys are shown, and the token reset, only here (the qstash lane's life)",
|
|
45
|
+
status: 'done', controls: ['Select', 'Reset token', 'Roll keys'], source: 'https://upstash.com/docs/qstash/howto/reset-token',
|
|
46
|
+
}, {
|
|
47
|
+
id: 'teams', kind: 'workspace', host: 'console.upstash.com', path: '/account/teams',
|
|
48
|
+
demand: 'a pending member is shown, and a pending invitation revoked, only here', status: 'done',
|
|
49
|
+
controls: ['Pending', 'Revoke Invite'], source: 'https://upstash.com/docs/common/account/teams',
|
|
50
|
+
}, {
|
|
51
|
+
id: 'team-invite', kind: 'flow', host: 'console.upstash.com', path: '/invite/{token}',
|
|
52
|
+
demand: 'a person joins a team by the acceptance link in the invitation email; no API operation accepts one',
|
|
53
|
+
status: 'done', controls: ['Accept invitation'], source: 'https://upstash.com/docs/common/account/teams',
|
|
54
|
+
}],
|
|
55
|
+
resources: {
|
|
56
|
+
Database: {
|
|
57
|
+
storedAs: '_database',
|
|
58
|
+
idPrefix: '',
|
|
59
|
+
// Not state the twin moves. `state` (active, suspended, passive): a database is active from its create (the
|
|
60
|
+
// getDatabase example's "state": "active"); suspension is Upstash's act on an account, which the twin does not
|
|
61
|
+
// model. `type` is the plan and `primary_region`, `db_type`, `db_resource_size` are set when it is made;
|
|
62
|
+
// `db_acl_enabled`, `db_acl_default_user_status` and `prometheus_enabled` are settings no served operation changes.
|
|
63
|
+
// Fields no page states a value for, answered with the Database schema's example values as the reference page
|
|
64
|
+
// renders them (https://upstash.com/docs/devops/developer-api/redis/get_database), the twin's choice: `replicas`
|
|
65
|
+
// (5, whatever the regions; no page says how Upstash counts them), `securityAddons` (all false), and
|
|
66
|
+
// `db_conn_idle_timeout`, `db_lua_timeout`, `db_lua_credits_per_min`, `db_store_max_idle`, `db_max_loads_per_sec`.
|
|
67
|
+
// Also the twin's, no page stating them: `db_memory_threshold` (256 MB on every plan), `db_max_clients` (10000), and
|
|
68
|
+
// how "Unlimited" monthly commands are written, `db_request_limit` 8024278031528737000 on pay-as-you-go and fixed
|
|
69
|
+
// plans (the schema's example value, a paid database's). Only the data size and the limits the pricing table gives
|
|
70
|
+
// (monthly commands, commands per second, request and record size) follow https://upstash.com/pricing/redis, per
|
|
71
|
+
// plan (semantics/index.ts, PLAN_LIMITS).
|
|
72
|
+
notState: ['state', 'type', 'primary_region', 'db_type', 'db_resource_size', 'db_acl_enabled', 'db_acl_default_user_status', 'prometheus_enabled'],
|
|
73
|
+
},
|
|
74
|
+
// `daily_backup` is whether the daily schedule made it, and `hourly_backup` is never set: both fixed when it is made
|
|
75
|
+
Backup: { storedAs: '_backup', idPrefix: '', notState: ['daily_backup', 'hourly_backup'], state: { state: backupState } },
|
|
76
|
+
Team: { storedAs: '_team', idPrefix: '' },
|
|
77
|
+
// a member's role is fixed: "If you want to change a user's role, you will need to delete and re-add them"
|
|
78
|
+
// (https://upstash.com/docs/common/account/teams); whether they are pending is the lane's bookkeeping (`_pending`)
|
|
79
|
+
TeamMember: { storedAs: '_team_member', idPrefix: '', notState: ['member_role'] },
|
|
80
|
+
},
|
|
81
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Upstash's Developer API manifest (the upstash pack's `api` lane; docs/contributing/architecture.md, "Other wires:
|
|
2
|
+
// lanes"): the vendor facts its published OpenAPI document (../spec/openapi.yaml) does not carry, and the resource whose
|
|
3
|
+
// state the lane moves. The surface is generated (./generated/surface.gen.json, by `bun scripts/derive-pack.ts
|
|
4
|
+
// upstash/api`), at `https://api.upstash.com/v2`.
|
|
5
|
+
//
|
|
6
|
+
// The lane serves the slice the customers' lives reach: a Redis database's create, read and delete, its read regions and
|
|
7
|
+
// password, its backups (semantics/backups.ts, with the daily backup on the World clock, semantics/time.ts), and teams
|
|
8
|
+
// (semantics/teams.ts). Every other operation of the document (renaming, plans, stats, audit logs, listing teams and
|
|
9
|
+
// members, Vector, Search, QStash) is the gap. The REST data plane (the pack
|
|
10
|
+
// root) reads the tokens this lane issues, so both write the one kernel service `upstash`; a database is a
|
|
11
|
+
// control-plane record, stored under the `_database` type, beside the root's `key` and `script` subjects.
|
|
12
|
+
//
|
|
13
|
+
// NO `auth`: the Developer API reads HTTP Basic with the account's email and an API key made in the console
|
|
14
|
+
// (https://upstash.com/docs/devops/developer-api/authentication), and a key's owner and its permission (Read/Write or
|
|
15
|
+
// Read Only) are the lane's records, which the kernel's gate cannot read. The lane's key gate (key-gate.ts) sits in front
|
|
16
|
+
// of the dispatch instead.
|
|
17
|
+
//
|
|
18
|
+
// THE ERROR BODY is the twin's decision: the document declares no error response for any operation. The twin answers in
|
|
19
|
+
// the document's own `Error` schema ({code, message}), which no operation references.
|
|
20
|
+
import type { DerivedManifest, StateField } from '@volter/world-core';
|
|
21
|
+
|
|
22
|
+
/** A backup's `state` (the Backup schema: pending, completed, failed). A backup is `pending` when it is made and
|
|
23
|
+
* `completed` two minutes later, on the World clock (semantics/time.ts): the duration is the twin's decision; Upstash's
|
|
24
|
+
* backup page says only that "Backup process will start and will appear in the backups table"
|
|
25
|
+
* (https://upstash.com/docs/redis/features/backup). Not made by the twin: failed. */
|
|
26
|
+
const backupState: StateField = {
|
|
27
|
+
initial: 'pending',
|
|
28
|
+
transitions: [
|
|
29
|
+
{ actor: 'time', from: ['pending'], to: 'completed', source: 'spec:/components/schemas/Backup/properties/state "State of the backup"' },
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const manifest: DerivedManifest = {
|
|
34
|
+
vendor: 'upstash',
|
|
35
|
+
service: 'upstash',
|
|
36
|
+
body: { json: 'always' },
|
|
37
|
+
// database ids are UUIDs (the Database schema's example, 96ad0856-03b1-4ee7-9666-e81abd0349e1); the handler mints one
|
|
38
|
+
// per database from the World's instant and name (semantics/index.ts), so a World mints the same ids every run
|
|
39
|
+
ids: { template: '{uuid}' },
|
|
40
|
+
// `creation_time`: "Creation time of the database as Unix time"
|
|
41
|
+
time: 'unix',
|
|
42
|
+
error: { code: '{code}', message: '{message}' },
|
|
43
|
+
readOnly: { status: 403, code: 'forbidden', message: 'The twin is read-only' },
|
|
44
|
+
malformedBody: { status: 400, code: 'bad_request', message: 'The request body is not valid JSON' },
|
|
45
|
+
notFound: { status: 404, code: 'not_found', message: 'database not found' },
|
|
46
|
+
// listDatabases answers a bare array, which the envelope cannot state; it is the gap. Required by the type.
|
|
47
|
+
list: { style: 'envelope', envelope: { data: '{data}' }, limit: { param: 'limit', default: 100, max: 100 } },
|
|
48
|
+
// deleteDatabase answers the string "OK" (its 200 schema: type string, example OK)
|
|
49
|
+
deleted: 'OK',
|
|
50
|
+
// the operations of a declared resource the lane does not serve: the gap
|
|
51
|
+
unmodeled: [
|
|
52
|
+
'listDatabases', 'renameDatabase', 'enableTls', 'enableEviction', 'disableEviction', 'enableAutoUpgrade',
|
|
53
|
+
'disableAutoUpgrade', 'changePlan', 'updateBudget', 'getDatabaseStats', 'listTeams', 'getTeamMembers',
|
|
54
|
+
],
|
|
55
|
+
screens: [{
|
|
56
|
+
id: 'sign-in', kind: 'flow', host: 'console.upstash.com', path: '/login',
|
|
57
|
+
demand: 'every console page a person reaches: each asks who is signed in', status: 'done',
|
|
58
|
+
controls: ['Email', 'Password', 'Sign in'], source: 'https://upstash.com/docs/devops/developer-api/introduction',
|
|
59
|
+
}, {
|
|
60
|
+
id: 'developer-api', kind: 'flow', host: 'console.upstash.com', path: '/account/api',
|
|
61
|
+
demand: 'the Developer API needs an API key, and a key is made only here',
|
|
62
|
+
status: 'done', controls: ['Create API Key', 'Name', 'Permissions', 'Expiration', 'Submit', 'Delete'],
|
|
63
|
+
source: 'https://upstash.com/docs/devops/developer-api/introduction',
|
|
64
|
+
}, {
|
|
65
|
+
id: 'qstash', kind: 'flow', host: 'console.upstash.com', path: '/qstash',
|
|
66
|
+
demand: "QStash's token and signing keys are shown, and the token reset, only here (the qstash lane's life)",
|
|
67
|
+
status: 'done', controls: ['Select', 'Reset token', 'Roll keys'], source: 'https://upstash.com/docs/qstash/howto/reset-token',
|
|
68
|
+
}, {
|
|
69
|
+
id: 'teams', kind: 'workspace', host: 'console.upstash.com', path: '/account/teams',
|
|
70
|
+
demand: 'a pending member is shown, and a pending invitation revoked, only here', status: 'done',
|
|
71
|
+
controls: ['Pending', 'Revoke Invite'], source: 'https://upstash.com/docs/common/account/teams',
|
|
72
|
+
}, {
|
|
73
|
+
id: 'team-invite', kind: 'flow', host: 'console.upstash.com', path: '/invite/{token}',
|
|
74
|
+
demand: 'a person joins a team by the acceptance link in the invitation email; no API operation accepts one',
|
|
75
|
+
status: 'done', controls: ['Accept invitation'], source: 'https://upstash.com/docs/common/account/teams',
|
|
76
|
+
}],
|
|
77
|
+
resources: {
|
|
78
|
+
Database: {
|
|
79
|
+
storedAs: '_database',
|
|
80
|
+
idPrefix: '',
|
|
81
|
+
// Not state the twin moves. `state` (active, suspended, passive): a database is active from its create (the
|
|
82
|
+
// getDatabase example's "state": "active"); suspension is Upstash's act on an account, which the twin does not
|
|
83
|
+
// model. `type` is the plan and `primary_region`, `db_type`, `db_resource_size` are set when it is made;
|
|
84
|
+
// `db_acl_enabled`, `db_acl_default_user_status` and `prometheus_enabled` are settings no served operation changes.
|
|
85
|
+
// Fields no page states a value for, answered with the Database schema's example values as the reference page
|
|
86
|
+
// renders them (https://upstash.com/docs/devops/developer-api/redis/get_database), the twin's choice: `replicas`
|
|
87
|
+
// (5, whatever the regions; no page says how Upstash counts them), `securityAddons` (all false), and
|
|
88
|
+
// `db_conn_idle_timeout`, `db_lua_timeout`, `db_lua_credits_per_min`, `db_store_max_idle`, `db_max_loads_per_sec`.
|
|
89
|
+
// Also the twin's, no page stating them: `db_memory_threshold` (256 MB on every plan), `db_max_clients` (10000), and
|
|
90
|
+
// how "Unlimited" monthly commands are written, `db_request_limit` 8024278031528737000 on pay-as-you-go and fixed
|
|
91
|
+
// plans (the schema's example value, a paid database's). Only the data size and the limits the pricing table gives
|
|
92
|
+
// (monthly commands, commands per second, request and record size) follow https://upstash.com/pricing/redis, per
|
|
93
|
+
// plan (semantics/index.ts, PLAN_LIMITS).
|
|
94
|
+
notState: ['state', 'type', 'primary_region', 'db_type', 'db_resource_size', 'db_acl_enabled', 'db_acl_default_user_status', 'prometheus_enabled'],
|
|
95
|
+
},
|
|
96
|
+
// `daily_backup` is whether the daily schedule made it, and `hourly_backup` is never set: both fixed when it is made
|
|
97
|
+
Backup: { storedAs: '_backup', idPrefix: '', notState: ['daily_backup', 'hourly_backup'], state: { state: backupState } },
|
|
98
|
+
Team: { storedAs: '_team', idPrefix: '' },
|
|
99
|
+
// a member's role is fixed: "If you want to change a user's role, you will need to delete and re-add them"
|
|
100
|
+
// (https://upstash.com/docs/common/account/teams); whether they are pending is the lane's bookkeeping (`_pending`)
|
|
101
|
+
TeamMember: { storedAs: '_team_member', idPrefix: '', notState: ['member_role'] },
|
|
102
|
+
},
|
|
103
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { SemanticsContext } from '@volter/world-core';
|
|
2
|
+
/** The Developer API tab, or undefined for any other request. `contextFor` gives the lane's semantics context. */
|
|
3
|
+
export declare function developerApiPage(request: Request, contextFor: (request: Request) => Promise<SemanticsContext>): Promise<Response | undefined>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// UPSTASH CONSOLE: ACCOUNT > DEVELOPER API — a flow (docs/contributing/architecture.md, "Screens"): the page where a person
|
|
3
|
+
// makes the API key the Developer API reads. No API operation makes a key, so the first can only come from here.
|
|
4
|
+
// https://upstash.com/docs/devops/developer-api/introduction: "Log in to the console, then open Account and click the
|
|
5
|
+
// Developer API tab. Click the Create API Key button. Enter a name for your key (you can not use the same name for
|
|
6
|
+
// multiple keys), choose the permissions (Read/Write or Read Only) and an expiration, then click Submit. You need to
|
|
7
|
+
// download or copy/save your API key. Upstash does not remember or keep your API for security reasons." and "By default
|
|
8
|
+
// one user can create up to 37 API keys." Built from @volter/world-ui's settings layout (Portal) under Upstash's skin;
|
|
9
|
+
// nothing of Upstash's page is copied. Its controls are the ones the pack's lives need: Create API Key with its Name,
|
|
10
|
+
// Permissions and Expiration, and Submit; and each key's Delete ("You can delete the API keys in the `Account > Developer
|
|
11
|
+
// API` tab").
|
|
12
|
+
//
|
|
13
|
+
// The person on the page is who signed in at /login (screens/session.tsx): a visitor nobody signed in as is sent there
|
|
14
|
+
// first. Where the documentation stops and the twin decides: the tab is at /account/api; the expirations offered are
|
|
15
|
+
// 30 days, 90 days and 1 year (the page says a key has one, and names none); a name the person already used, and a 38th key, answer the page again with a
|
|
16
|
+
// notice (422); the new key is shown once in the page's notice.
|
|
17
|
+
import { flowPage, Portal, PORTAL_CSS } from '@volter/world-ui';
|
|
18
|
+
import { makeApiKey } from "../semantics/index.js";
|
|
19
|
+
import { signedIn, toSignIn } from "./session.js";
|
|
20
|
+
const PATH = '/account/api';
|
|
21
|
+
const EXPIRATIONS = { '30d': 30, '90d': 90, '1y': 365 };
|
|
22
|
+
const MAX_KEYS = 37;
|
|
23
|
+
// Upstash's skin over the settings layout: its stone greys and green primary button
|
|
24
|
+
const SKIN = `
|
|
25
|
+
body { background: #ffffff; color: #1c1917; }
|
|
26
|
+
.portal-side { background: #fafaf9; border-right: 1px solid #e7e5e4; }
|
|
27
|
+
.portal-section h2 { border-color: #e7e5e4; color: #57534e; }
|
|
28
|
+
.portal-notice { background: #ecfdf5; border: 1px solid #a7f3d0; font-family: ui-monospace, monospace; word-break: break-all; }
|
|
29
|
+
.portal-button { background: #ffffff; border: 1px solid #d6d3d1; border-radius: 6px; padding: 6px 12px; }
|
|
30
|
+
.portal-primary { background: #00c98d; border-color: #00c98d; color: #0c0a09; }
|
|
31
|
+
`;
|
|
32
|
+
function page(ctx, email, notice, status = 200) {
|
|
33
|
+
const keys = ctx.rowsRaw('_api_key').filter((k) => k.email === email && k.deleted !== true);
|
|
34
|
+
return flowPage({
|
|
35
|
+
title: 'Developer API · Account · Upstash Console',
|
|
36
|
+
css: [PORTAL_CSS, SKIN],
|
|
37
|
+
status,
|
|
38
|
+
body: (_jsx(Portal, { merchant: `${email} · Account`, back: { href: '/', label: 'Back to console' }, ...(notice ? { notice } : {}), sections: [{
|
|
39
|
+
heading: 'Developer API',
|
|
40
|
+
empty: 'No API keys yet.',
|
|
41
|
+
items: keys.map((k) => ({
|
|
42
|
+
title: String(k.name),
|
|
43
|
+
detail: k.permission === 'read_only' ? 'Read Only' : 'Read/Write',
|
|
44
|
+
note: k.expires_at ? `Expires ${String(k.expires_at).slice(0, 10)}` : 'Never expires',
|
|
45
|
+
actions: [{ label: 'Delete', action: `${PATH}/delete`, tone: 'danger', fields: { name: String(k.name) } }],
|
|
46
|
+
})),
|
|
47
|
+
}], forms: [{
|
|
48
|
+
heading: 'Create API Key',
|
|
49
|
+
action: PATH,
|
|
50
|
+
fields: [
|
|
51
|
+
{ id: 'name', label: 'Name' },
|
|
52
|
+
{ id: 'permission', label: 'Permissions', options: [{ value: 'read_write', label: 'Read/Write' }, { value: 'read_only', label: 'Read Only' }] },
|
|
53
|
+
{ id: 'expiration', label: 'Expiration', options: [{ value: '30d', label: '30 days' }, { value: '90d', label: '90 days' }, { value: '1y', label: '1 year' }] },
|
|
54
|
+
],
|
|
55
|
+
submit: { label: 'Submit' },
|
|
56
|
+
}] })),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** Delete: the key stops working at once (key-gate.ts admits no deleted key); "When an API key is exposed ... or not
|
|
60
|
+
* being used anymore; you should delete it. You can delete the API keys in the `Account > Developer API` tab." */
|
|
61
|
+
async function deleteKey(ctx, email, name) {
|
|
62
|
+
const key = ctx.rowsRaw('_api_key').find((k) => k.email === email && k.name === name && k.deleted !== true);
|
|
63
|
+
if (!key)
|
|
64
|
+
return page(ctx, email, `You have no API key named ${name}.`, 422);
|
|
65
|
+
await ctx.record('_api_key', { deleted: true, deleted_at: ctx.occurredAt }, `apikey:${email}:${name}`);
|
|
66
|
+
return page(ctx, email, `API key ${name} deleted.`);
|
|
67
|
+
}
|
|
68
|
+
async function formOf(request) {
|
|
69
|
+
return Object.fromEntries(new URLSearchParams(await request.text()));
|
|
70
|
+
}
|
|
71
|
+
/** The Developer API tab, or undefined for any other request. `contextFor` gives the lane's semantics context. */
|
|
72
|
+
export async function developerApiPage(request, contextFor) {
|
|
73
|
+
const url = new URL(request.url);
|
|
74
|
+
const path = url.pathname.replace(/\/+$/, '');
|
|
75
|
+
if (path !== PATH && path !== `${PATH}/delete`)
|
|
76
|
+
return undefined;
|
|
77
|
+
const person = signedIn(await contextFor(new Request(request.url)), request);
|
|
78
|
+
if (!person)
|
|
79
|
+
return toSignIn(request);
|
|
80
|
+
if (request.method === 'GET' && path === PATH)
|
|
81
|
+
return page(await contextFor(new Request(request.url)), person.email);
|
|
82
|
+
if (request.method !== 'POST')
|
|
83
|
+
return undefined;
|
|
84
|
+
const form = await formOf(request);
|
|
85
|
+
const ctx = await contextFor(new Request(request.url));
|
|
86
|
+
if (path !== PATH)
|
|
87
|
+
return deleteKey(ctx, person.email, (form.name ?? '').trim());
|
|
88
|
+
const name = (form.name ?? '').trim();
|
|
89
|
+
const held = ctx.rowsRaw('_api_key').filter((k) => k.email === person.email && k.deleted !== true);
|
|
90
|
+
if (!name)
|
|
91
|
+
return page(ctx, person.email, 'An API key needs a name.', 422);
|
|
92
|
+
if (held.some((k) => k.name === name))
|
|
93
|
+
return page(ctx, person.email, `You already have an API key named ${name}.`, 422);
|
|
94
|
+
if (held.length >= MAX_KEYS)
|
|
95
|
+
return page(ctx, person.email, `You can create up to ${MAX_KEYS} API keys.`, 422);
|
|
96
|
+
const permission = form.permission === 'read_only' ? 'read_only' : 'read_write';
|
|
97
|
+
const days = form.expiration !== undefined && form.expiration in EXPIRATIONS ? EXPIRATIONS[form.expiration] : EXPIRATIONS['1y'];
|
|
98
|
+
const expiresAt = new Date(Date.parse(ctx.occurredAt) + days * 86_400_000).toISOString();
|
|
99
|
+
const { key } = await makeApiKey(ctx, person.email, name, permission, expiresAt);
|
|
100
|
+
return page(await contextFor(new Request(request.url)), person.email, `API key ${name} created: ${key} — copy it now: Upstash does not keep it.`);
|
|
101
|
+
}
|