@elevasis/core 0.7.0 → 0.8.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/package.json +3 -3
- package/src/README.md +41 -41
- package/src/__tests__/publish.test.ts +18 -18
- package/src/__tests__/{template-foundations-compatibility.test.ts → template-core-compatibility.test.ts} +99 -99
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +1135 -1131
- package/src/_gen/__tests__/scaffold-contracts.test.ts +53 -53
- package/src/_gen/scaffold-contracts.ts +45 -45
- package/src/auth/multi-tenancy/credentials/README.md +38 -38
- package/src/auth/multi-tenancy/credentials/index.ts +6 -6
- package/src/auth/multi-tenancy/credentials/server/encryption.ts +39 -39
- package/src/auth/multi-tenancy/credentials/server/service.ts +60 -60
- package/src/auth/multi-tenancy/index.ts +17 -17
- package/src/auth/multi-tenancy/invitations/api-schemas.ts +107 -107
- package/src/auth/multi-tenancy/invitations/index.ts +37 -37
- package/src/auth/multi-tenancy/invitations/invitation.ts +86 -86
- package/src/auth/multi-tenancy/invitations/server/index.ts +25 -25
- package/src/auth/multi-tenancy/invitations/server/transforms.ts +24 -24
- package/src/auth/multi-tenancy/invitations/server/workos.ts +24 -24
- package/src/auth/multi-tenancy/invitations/supabase.ts +50 -50
- package/src/auth/multi-tenancy/memberships/api-schemas.ts +126 -126
- package/src/auth/multi-tenancy/memberships/index.ts +21 -21
- package/src/auth/multi-tenancy/memberships/membership.ts +138 -138
- package/src/auth/multi-tenancy/memberships/server/index.ts +15 -15
- package/src/auth/multi-tenancy/memberships/server/transforms.ts +32 -32
- package/src/auth/multi-tenancy/memberships/server/workos.ts +21 -21
- package/src/auth/multi-tenancy/memberships/supabase.ts +46 -46
- package/src/auth/multi-tenancy/organizations/api-schemas.ts +128 -128
- package/src/auth/multi-tenancy/organizations/index.ts +23 -23
- package/src/auth/multi-tenancy/organizations/organization.ts +24 -24
- package/src/auth/multi-tenancy/organizations/server/index.ts +10 -10
- package/src/auth/multi-tenancy/organizations/server/transforms.ts +35 -35
- package/src/auth/multi-tenancy/organizations/server/workos.ts +20 -20
- package/src/auth/multi-tenancy/types.ts +83 -83
- package/src/auth/multi-tenancy/users/api-schemas.ts +194 -194
- package/src/auth/multi-tenancy/users/index.ts +27 -27
- package/src/auth/multi-tenancy/users/server/index.ts +19 -19
- package/src/auth/multi-tenancy/users/server/transforms.ts +21 -21
- package/src/auth/multi-tenancy/users/server/workos.ts +16 -16
- package/src/auth/multi-tenancy/users/user.ts +65 -65
- package/src/business/README.md +52 -52
- package/src/business/__tests__/entities-published.test.ts +33 -33
- package/src/business/acquisition/api-schemas.ts +759 -759
- package/src/business/acquisition/index.ts +109 -109
- package/src/business/acquisition/types.ts +402 -402
- package/src/business/base-entities.test.ts +481 -481
- package/src/business/base-entities.ts +241 -241
- package/src/business/entities-published.ts +24 -24
- package/src/business/index.ts +15 -15
- package/src/business/pdf/browser/pdfmake-browser.ts +229 -229
- package/src/business/pdf/index.ts +10 -10
- package/src/business/pdf/server/index.ts +21 -21
- package/src/business/pdf/server/themes/default.ts +8 -8
- package/src/business/pdf/server/themes/index.ts +9 -9
- package/src/business/pdf/server/themes/types.ts +8 -8
- package/src/business/pdf/types.ts +272 -272
- package/src/business/projects/index.ts +2 -1
- package/src/business/projects/sse-events.ts +21 -0
- package/src/business/projects/types.ts +89 -89
- package/src/business/sales/api-schemas.ts +75 -75
- package/src/business/seo/__tests__/linking.test.ts +549 -549
- package/src/business/seo/__tests__/types.test.ts +404 -404
- package/src/business/seo/index.ts +2 -2
- package/src/business/seo/linking.ts +281 -281
- package/src/business/seo/types.ts +199 -199
- package/src/commands/queue/index.ts +3 -3
- package/src/commands/queue/schemas.test.ts +593 -593
- package/src/commands/queue/schemas.ts +125 -125
- package/src/commands/queue/sse-events.ts +61 -61
- package/src/commands/queue/types/action.ts +52 -52
- package/src/commands/queue/types/checkpoint.ts +44 -44
- package/src/commands/queue/types/index.ts +7 -7
- package/src/commands/queue/types/task.ts +116 -116
- package/src/commands/queue/types.ts +14 -14
- package/src/content/distribution-metadata.ts +61 -61
- package/src/content/index.ts +10 -10
- package/src/deployments/index.ts +22 -22
- package/src/execution/core/__tests__/archived-logs.test.ts +72 -72
- package/src/execution/core/index.ts +11 -11
- package/src/execution/core/runner-types.ts +80 -80
- package/src/execution/core/server/environment.ts +31 -31
- package/src/execution/core/sse-executions.ts +119 -119
- package/src/execution/core/types.ts +29 -29
- package/src/execution/engine/__tests__/fixtures/test-agents.ts +4 -4
- package/src/execution/engine/__tests__/timeout.test.ts +565 -565
- package/src/execution/engine/agent/__tests__/errors.test.ts +508 -508
- package/src/execution/engine/agent/actions/__tests__/processor.test.ts +531 -531
- package/src/execution/engine/agent/actions/executor.ts +205 -205
- package/src/execution/engine/agent/actions/navigate-knowledge-executor.ts +230 -230
- package/src/execution/engine/agent/actions/processor.ts +116 -116
- package/src/execution/engine/agent/actions/types.ts +70 -70
- package/src/execution/engine/agent/core/agent.ts +810 -810
- package/src/execution/engine/agent/core/types.ts +155 -155
- package/src/execution/engine/agent/errors.ts +251 -251
- package/src/execution/engine/agent/index.ts +78 -78
- package/src/execution/engine/agent/knowledge-map/types.ts +106 -106
- package/src/execution/engine/agent/knowledge-map/utils.ts +101 -101
- package/src/execution/engine/agent/memory/__tests__/manager.test.ts +754 -754
- package/src/execution/engine/agent/memory/domains.ts +99 -99
- package/src/execution/engine/agent/memory/manager.ts +365 -365
- package/src/execution/engine/agent/memory/processor.ts +66 -66
- package/src/execution/engine/agent/memory/types.ts +90 -90
- package/src/execution/engine/agent/memory/utils.ts +134 -134
- package/src/execution/engine/agent/observability/logging.ts +467 -467
- package/src/execution/engine/agent/observability/types.ts +64 -64
- package/src/execution/engine/agent/reasoning/adapters/agent-adapter-helpers.ts +349 -349
- package/src/execution/engine/agent/reasoning/processor.ts +92 -92
- package/src/execution/engine/agent/reasoning/prompt-sections/base-actions.ts +134 -134
- package/src/execution/engine/agent/reasoning/prompt-sections/completion.ts +49 -49
- package/src/execution/engine/agent/reasoning/prompt-sections/knowledge-map.ts +93 -93
- package/src/execution/engine/agent/reasoning/prompt-sections/memory.ts +65 -65
- package/src/execution/engine/agent/reasoning/prompt-sections/tools.ts +44 -44
- package/src/execution/engine/agent/reasoning/request-builder.ts +169 -169
- package/src/execution/engine/agent/reasoning/types.ts +18 -18
- package/src/execution/engine/base/errors.ts +118 -118
- package/src/execution/engine/base/index.ts +2 -2
- package/src/execution/engine/base/logging.ts +31 -31
- package/src/execution/engine/base/serialization.ts +324 -324
- package/src/execution/engine/base/types.ts +126 -126
- package/src/execution/engine/base/utils.ts +41 -41
- package/src/execution/engine/index.ts +434 -434
- package/src/execution/engine/interface/index.ts +1 -1
- package/src/execution/engine/interface/types.ts +62 -62
- package/src/execution/engine/llm/__tests__/model-info.test.ts +50 -50
- package/src/execution/engine/llm/__tests__/model-validation.test.ts +321 -321
- package/src/execution/engine/llm/__tests__/response-schema-validator.test.ts +115 -115
- package/src/execution/engine/llm/adapters/__tests__/adapter-factory.test.ts +375 -375
- package/src/execution/engine/llm/adapters/__tests__/anthropic-adapter.test.ts +463 -463
- package/src/execution/engine/llm/adapters/__tests__/anthropic.integration.test.ts +177 -177
- package/src/execution/engine/llm/adapters/__tests__/google-adapter.test.ts +722 -722
- package/src/execution/engine/llm/adapters/__tests__/google.integration.test.ts +376 -376
- package/src/execution/engine/llm/adapters/__tests__/openai-adapter.test.ts +551 -551
- package/src/execution/engine/llm/adapters/__tests__/openrouter-adapter.test.ts +563 -563
- package/src/execution/engine/llm/adapters/__tests__/openrouter.integration.test.ts +105 -105
- package/src/execution/engine/llm/adapters/__tests__/universal-adapter.test.ts +537 -537
- package/src/execution/engine/llm/adapters/circuit-breaker.ts +147 -147
- package/src/execution/engine/llm/adapters/index.ts +17 -17
- package/src/execution/engine/llm/adapters/mock-adapter.ts +116 -116
- package/src/execution/engine/llm/adapters/server/adapter-factory.ts +130 -130
- package/src/execution/engine/llm/adapters/server/anthropic.ts +137 -137
- package/src/execution/engine/llm/adapters/server/google.ts +283 -283
- package/src/execution/engine/llm/adapters/server/index.ts +12 -12
- package/src/execution/engine/llm/adapters/server/openai.ts +206 -206
- package/src/execution/engine/llm/adapters/server/openrouter.ts +235 -235
- package/src/execution/engine/llm/adapters/universal-adapter.ts +230 -230
- package/src/execution/engine/llm/errors.ts +186 -186
- package/src/execution/engine/llm/model-info.ts +332 -332
- package/src/execution/engine/llm/response-schema-validator.ts +113 -113
- package/src/execution/engine/llm/types.ts +86 -86
- package/src/execution/engine/test-utils/index.ts +6 -6
- package/src/execution/engine/test-utils/mocks.ts +56 -56
- package/src/execution/engine/tools/integration/base-integration-adapter.ts +50 -50
- package/src/execution/engine/tools/integration/index.ts +53 -53
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/anymailfinder-adapter.ts +73 -73
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/anymailfinder-tools.ts +209 -209
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/fetch/find-company-email/index.ts +82 -82
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/fetch/find-decision-maker-email/index.ts +122 -122
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/fetch/find-person-email/index.ts +89 -89
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/fetch/verify-email/index.ts +84 -84
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/index.ts +16 -16
- package/src/execution/engine/tools/integration/server/adapters/apify/__tests__/apify-run-actor.integration.test.ts +293 -293
- package/src/execution/engine/tools/integration/server/adapters/apify/apify-adapter.ts +100 -100
- package/src/execution/engine/tools/integration/server/adapters/apify/apify-tools.ts +217 -217
- package/src/execution/engine/tools/integration/server/adapters/apify/fetch/get-dataset-items/index.ts +92 -92
- package/src/execution/engine/tools/integration/server/adapters/apify/fetch/run-actor/index.ts +218 -218
- package/src/execution/engine/tools/integration/server/adapters/apify/fetch/start-actor/index.ts +87 -87
- package/src/execution/engine/tools/integration/server/adapters/apify/index.ts +11 -11
- package/src/execution/engine/tools/integration/server/adapters/attio/__tests__/attio-crud.integration.test.ts +361 -361
- package/src/execution/engine/tools/integration/server/adapters/attio/attio-adapter.ts +162 -162
- package/src/execution/engine/tools/integration/server/adapters/attio/attio-tools.ts +594 -594
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/create-attribute/index.ts +214 -214
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/create-note/index.ts +152 -152
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/create-record/index.ts +141 -141
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/delete-note/index.ts +86 -86
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/delete-record/index.ts +105 -105
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/get-record/index.ts +118 -118
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-attributes/index.ts +165 -165
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-notes/index.ts +96 -96
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-objects/index.ts +104 -104
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-records/index.ts +156 -156
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/update-attribute/index.ts +220 -220
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/update-record/index.ts +140 -140
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/utils/types.ts +146 -146
- package/src/execution/engine/tools/integration/server/adapters/attio/index.ts +31 -31
- package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-adapter.ts +210 -210
- package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-tools.ts +104 -104
- package/src/execution/engine/tools/integration/server/adapters/google-sheets/__tests__/google-sheets.integration.test.ts +261 -261
- package/src/execution/engine/tools/integration/server/adapters/google-sheets/google-sheets-adapter.ts +1189 -1189
- package/src/execution/engine/tools/integration/server/adapters/google-sheets/google-sheets-tools.ts +641 -641
- package/src/execution/engine/tools/integration/server/adapters/google-sheets/index.ts +18 -18
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/activate-campaign/index.ts +86 -86
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/add-to-campaign/__tests__/index.test.ts +289 -289
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/add-to-campaign/index.ts +154 -154
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/bulk-add-leads/__tests__/index.test.ts +325 -325
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/bulk-add-leads/index.ts +153 -153
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/bulk-delete-leads/index.ts +84 -84
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/create-campaign/index.ts +125 -125
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/create-inbox-test/index.ts +107 -107
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/delete-campaign/index.ts +85 -85
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-account-health/index.ts +91 -91
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-campaign/index.ts +92 -92
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-campaign-analytics/__tests__/index.test.ts +195 -195
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-campaign-analytics/index.ts +113 -113
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-daily-campaign-analytics/index.ts +104 -104
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-emails/index.ts +155 -155
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-step-analytics/__tests__/index.test.ts +196 -196
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-step-analytics/index.ts +102 -102
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/list-campaigns/__tests__/index.test.ts +189 -189
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/list-campaigns/index.ts +87 -87
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/list-leads/index.ts +112 -112
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/patch-lead/index.ts +76 -76
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/pause-campaign/index.ts +86 -86
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/remove-from-subsequence/index.ts +98 -98
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/send-reply/index.ts +126 -126
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/update-campaign/__tests__/index.test.ts +193 -193
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/update-campaign/index.ts +99 -99
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/update-interest-status/__tests__/index.test.ts +621 -621
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/update-interest-status/index.ts +125 -125
- package/src/execution/engine/tools/integration/server/adapters/instantly/index.ts +29 -29
- package/src/execution/engine/tools/integration/server/adapters/instantly/instantly-adapter.ts +178 -178
- package/src/execution/engine/tools/integration/server/adapters/instantly/instantly-tools.ts +1473 -1473
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/fetch/check-credits/index.ts +59 -59
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/fetch/verify-email/index.ts +102 -102
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/index.ts +17 -17
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/millionverifier-adapter.ts +80 -80
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/millionverifier-tools.ts +102 -102
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/get-email/index.ts +102 -102
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.ts +134 -134
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/utils/types.ts +75 -75
- package/src/execution/engine/tools/integration/server/adapters/resend/index.ts +27 -27
- package/src/execution/engine/tools/integration/server/adapters/resend/resend-adapter.ts +108 -108
- package/src/execution/engine/tools/integration/server/adapters/resend/resend-tools.ts +132 -132
- package/src/execution/engine/tools/integration/server/adapters/signature-api/fetch/create-envelope/index.ts +274 -274
- package/src/execution/engine/tools/integration/server/adapters/signature-api/fetch/download-document/index.ts +230 -230
- package/src/execution/engine/tools/integration/server/adapters/signature-api/fetch/get-envelope/index.ts +133 -133
- package/src/execution/engine/tools/integration/server/adapters/signature-api/fetch/void-envelope/index.ts +90 -90
- package/src/execution/engine/tools/integration/server/adapters/stripe/fetch/utils/types.ts +210 -210
- package/src/execution/engine/tools/integration/server/adapters/stripe/stripe-adapter.ts +517 -517
- package/src/execution/engine/tools/integration/server/adapters/stripe/stripe-tools.ts +309 -309
- package/src/execution/engine/tools/integration/server/adapters/tomba/fetch/domain-search/index.ts +133 -133
- package/src/execution/engine/tools/integration/server/adapters/tomba/fetch/email-finder/index.ts +122 -122
- package/src/execution/engine/tools/integration/server/adapters/tomba/fetch/email-verifier/index.ts +111 -111
- package/src/execution/engine/tools/integration/server/adapters/tomba/index.ts +11 -11
- package/src/execution/engine/tools/integration/server/adapters/tomba/tomba-adapter.ts +78 -78
- package/src/execution/engine/tools/integration/server/adapters/tomba/tomba-tools.ts +222 -222
- package/src/execution/engine/tools/integration/server/index.ts +61 -61
- package/src/execution/engine/tools/integration/service.ts +161 -161
- package/src/execution/engine/tools/integration/tool.ts +253 -253
- package/src/execution/engine/tools/integration/types/anymailfinder.ts +74 -74
- package/src/execution/engine/tools/integration/types/apify.ts +92 -92
- package/src/execution/engine/tools/integration/types/index.ts +19 -19
- package/src/execution/engine/tools/integration/types/instantly.ts +557 -557
- package/src/execution/engine/tools/integration/types/millionverifier.ts +56 -56
- package/src/execution/engine/tools/integration/types/stripe.ts +162 -162
- package/src/execution/engine/tools/integration/types/tomba.ts +94 -94
- package/src/execution/engine/tools/lead-service-types.ts +884 -884
- package/src/execution/engine/tools/llm/index.ts +11 -11
- package/src/execution/engine/tools/llm/server/index.ts +8 -8
- package/src/execution/engine/tools/llm/server/llm-call-tool.ts +118 -118
- package/src/execution/engine/tools/platform/__tests__/pdf.test.ts +441 -441
- package/src/execution/engine/tools/platform/acquisition/company-tools.ts +248 -248
- package/src/execution/engine/tools/platform/acquisition/contact-tools.ts +319 -319
- package/src/execution/engine/tools/platform/acquisition/index.ts +43 -43
- package/src/execution/engine/tools/platform/acquisition/list-tools.ts +148 -148
- package/src/execution/engine/tools/platform/acquisition/types.ts +260 -260
- package/src/execution/engine/tools/platform/email/index.ts +122 -122
- package/src/execution/engine/tools/platform/email/types.ts +96 -96
- package/src/execution/engine/tools/platform/index.ts +157 -157
- package/src/execution/engine/tools/platform/notification.ts +81 -81
- package/src/execution/engine/tools/platform/pdf/index.ts +110 -110
- package/src/execution/engine/tools/platform/pdf/types.ts +77 -77
- package/src/execution/engine/tools/platform/scheduler.ts +87 -87
- package/src/execution/engine/tools/platform/storage/index.ts +370 -370
- package/src/execution/engine/tools/platform/types.ts +148 -148
- package/src/execution/engine/tools/registry.ts +700 -699
- package/src/execution/engine/tools/tool-maps.ts +786 -786
- package/src/execution/engine/tools/types.ts +233 -233
- package/src/execution/engine/workflow/__tests__/errors.test.ts +139 -139
- package/src/execution/engine/workflow/errors.ts +63 -63
- package/src/execution/engine/workflow/helpers/index.ts +11 -11
- package/src/execution/engine/workflow/helpers/server/index.ts +8 -8
- package/src/execution/engine/workflow/helpers/server/llm-call.ts +93 -93
- package/src/execution/engine/workflow/index.ts +19 -19
- package/src/execution/engine/workflow/log-truncate.ts +26 -26
- package/src/execution/engine/workflow/logging.ts +191 -191
- package/src/execution/engine/workflow/types.ts +182 -182
- package/src/execution/engine/workflow/utils.ts +280 -280
- package/src/execution/engine/workflow/workflow.ts +168 -168
- package/src/execution/index.ts +3 -3
- package/src/execution/scheduler/__tests__/api-schemas.test.ts +733 -733
- package/src/execution/scheduler/__tests__/utils.test.ts +1009 -1009
- package/src/execution/scheduler/api-schemas.ts +296 -296
- package/src/execution/scheduler/index.ts +50 -50
- package/src/execution/scheduler/schemas.ts +264 -264
- package/src/execution/scheduler/types.ts +111 -111
- package/src/execution/scheduler/utils.ts +364 -364
- package/src/forms/index.ts +7 -7
- package/src/forms/schemas.ts +69 -69
- package/src/forms/types.ts +70 -70
- package/src/index.ts +71 -60
- package/src/integrations/credentials/__tests__/schemas.test.ts +82 -82
- package/src/integrations/credentials/__tests__/utils.test.ts +144 -144
- package/src/integrations/credentials/api-schemas.ts +143 -143
- package/src/integrations/credentials/index.ts +32 -32
- package/src/integrations/credentials/schemas.ts +164 -164
- package/src/integrations/credentials/utils.ts +59 -59
- package/src/integrations/oauth/__tests__/provider-registry.test.ts +59 -59
- package/src/integrations/oauth/api-schemas.ts +92 -92
- package/src/integrations/oauth/index.ts +19 -19
- package/src/integrations/oauth/provider-registry.ts +61 -61
- package/src/integrations/oauth/server/__tests__/refresh-concurrent.test.ts +183 -183
- package/src/integrations/oauth/server/__tests__/refresh.test.ts +577 -577
- package/src/integrations/oauth/server/credentials.ts +39 -39
- package/src/integrations/oauth/server/refresh.ts +214 -214
- package/src/integrations/oauth/types.ts +34 -34
- package/src/integrations/webhook-endpoints/__tests__/api-schemas.test.ts +318 -318
- package/src/integrations/webhook-endpoints/api-schemas.ts +102 -102
- package/src/integrations/webhook-endpoints/index.ts +28 -28
- package/src/integrations/webhook-endpoints/types.ts +51 -51
- package/src/operations/activities/api-schemas.ts +79 -79
- package/src/operations/activities/index.ts +9 -9
- package/src/operations/activities/sse-events.ts +30 -30
- package/src/operations/activities/types.ts +63 -63
- package/src/operations/debug-logs/client.ts +60 -60
- package/src/operations/debug-logs/debug-logger.ts +83 -83
- package/src/operations/debug-logs/index.ts +8 -8
- package/src/operations/debug-logs/server.ts +19 -19
- package/src/operations/debug-logs/types.ts +33 -33
- package/src/operations/index.ts +50 -50
- package/src/operations/notifications/api-schemas.ts +91 -91
- package/src/operations/notifications/index.ts +3 -3
- package/src/operations/notifications/sse-events.ts +21 -21
- package/src/operations/notifications/types.ts +47 -47
- package/src/operations/observability/__tests__/openrouter-cost-flow.test.ts +297 -297
- package/src/operations/observability/__tests__/utils.test.ts +54 -54
- package/src/operations/observability/ai-usage-collector.ts +64 -64
- package/src/operations/observability/index.ts +13 -13
- package/src/operations/observability/metrics-collector.ts +49 -49
- package/src/operations/observability/schemas.ts +39 -39
- package/src/operations/observability/types.ts +463 -463
- package/src/operations/observability/utils.ts +77 -77
- package/src/operations/sessions/__tests__/manager.test.ts +821 -821
- package/src/operations/sessions/index.ts +26 -26
- package/src/operations/sessions/server/manager.ts +90 -90
- package/src/operations/sessions/server/session.ts +180 -180
- package/src/operations/sessions/types.ts +98 -98
- package/src/operations/triggers/index.ts +12 -12
- package/src/operations/triggers/webhook/definitions/instantly-account-error.ts +44 -44
- package/src/operations/triggers/webhook/definitions/instantly-auto-reply-received.ts +51 -51
- package/src/operations/triggers/webhook/definitions/instantly-campaign-completed.ts +45 -45
- package/src/operations/triggers/webhook/definitions/instantly-email-bounced.ts +49 -49
- package/src/operations/triggers/webhook/definitions/instantly-lead-unsubscribed.ts +45 -45
- package/src/operations/triggers/webhook/definitions/instantly-reply-received.ts +54 -54
- package/src/operations/triggers/webhook/index.ts +35 -35
- package/src/operations/triggers/webhook/types.ts +74 -74
- package/src/organization-model/README.md +97 -97
- package/src/organization-model/__tests__/defaults.test.ts +175 -175
- package/src/organization-model/__tests__/domains/customers.test.ts +295 -295
- package/src/organization-model/__tests__/domains/goals.test.ts +479 -479
- package/src/organization-model/__tests__/domains/identity.test.ts +279 -279
- package/src/organization-model/__tests__/domains/navigation.test.ts +212 -212
- package/src/organization-model/__tests__/domains/offerings.test.ts +419 -419
- package/src/organization-model/__tests__/domains/operations.test.ts +203 -203
- package/src/organization-model/__tests__/domains/resource-mappings.test.ts +362 -362
- package/src/organization-model/__tests__/domains/roles.test.ts +347 -347
- package/src/organization-model/__tests__/domains/statuses.test.ts +243 -243
- package/src/organization-model/__tests__/foundation.test.ts +105 -105
- package/src/organization-model/__tests__/graph.test.ts +894 -894
- package/src/organization-model/__tests__/resolve.test.ts +690 -690
- package/src/organization-model/__tests__/schema.test.ts +407 -407
- package/src/organization-model/contracts.ts +14 -14
- package/src/organization-model/defaults.ts +148 -148
- package/src/organization-model/domains/branding.ts +22 -22
- package/src/organization-model/domains/customers.ts +75 -75
- package/src/organization-model/domains/features.ts +22 -22
- package/src/organization-model/domains/goals.ts +80 -80
- package/src/organization-model/domains/identity.ts +94 -94
- package/src/organization-model/domains/navigation.ts +391 -391
- package/src/organization-model/domains/offerings.ts +66 -66
- package/src/organization-model/domains/operations.ts +85 -85
- package/src/organization-model/domains/projects.ts +48 -48
- package/src/organization-model/domains/prospecting.ts +33 -33
- package/src/organization-model/domains/roles.ts +55 -55
- package/src/organization-model/domains/sales.ts +94 -94
- package/src/organization-model/domains/shared.ts +62 -62
- package/src/organization-model/domains/statuses.ts +130 -130
- package/src/organization-model/foundation.ts +97 -97
- package/src/organization-model/graph/build.ts +399 -399
- package/src/organization-model/graph/index.ts +4 -4
- package/src/organization-model/graph/schema.ts +48 -48
- package/src/organization-model/graph/types.ts +40 -40
- package/src/organization-model/index.ts +13 -13
- package/src/organization-model/organization-graph.mdx +272 -272
- package/src/organization-model/organization-model.mdx +320 -320
- package/src/organization-model/published.ts +85 -85
- package/src/organization-model/resolve.ts +66 -66
- package/src/organization-model/schema.ts +287 -287
- package/src/organization-model/types.ts +46 -46
- package/src/platform/api/index.ts +1 -1
- package/src/platform/api/types.ts +35 -35
- package/src/platform/constants/http.ts +37 -37
- package/src/platform/constants/index.ts +5 -5
- package/src/platform/constants/limits.ts +32 -32
- package/src/platform/constants/resilience.ts +51 -51
- package/src/platform/constants/timeouts.ts +20 -20
- package/src/platform/constants/versions.ts +3 -3
- package/src/platform/registry/__tests__/resource-registry-static.test.ts +347 -347
- package/src/platform/registry/__tests__/resource-registry.integration.test.ts +1028 -1028
- package/src/platform/registry/__tests__/resource-registry.list-executable.test.ts +393 -393
- package/src/platform/registry/__tests__/resource-registry.test.ts +2005 -2005
- package/src/platform/registry/__tests__/serialization.test.ts +1127 -1127
- package/src/platform/registry/command-view.ts +180 -180
- package/src/platform/registry/domains.ts +165 -165
- package/src/platform/registry/index.ts +93 -93
- package/src/platform/registry/reserved.ts +24 -24
- package/src/platform/registry/resource-metadata.ts +59 -59
- package/src/platform/registry/resource-registry.command-queue-groups.test.ts +129 -129
- package/src/platform/registry/resource-registry.ts +876 -876
- package/src/platform/registry/serialization.ts +273 -273
- package/src/platform/registry/serialized-types.ts +231 -231
- package/src/platform/registry/stats-types.ts +66 -66
- package/src/platform/registry/types.ts +404 -404
- package/src/platform/registry/validation.ts +513 -513
- package/src/platform/resilience/__tests__/rate-limiter.test.ts +471 -471
- package/src/platform/resilience/circuit-breaker.ts +164 -164
- package/src/platform/resilience/errors.ts +68 -68
- package/src/platform/resilience/http-error-mapper.ts +129 -129
- package/src/platform/resilience/index.ts +93 -93
- package/src/platform/resilience/rate-limiter-types.ts +46 -46
- package/src/platform/resilience/rate-limiter.ts +140 -140
- package/src/platform/resilience/retry.ts +89 -89
- package/src/platform/resilience/timeout.ts +63 -63
- package/src/platform/sse/events.ts +37 -34
- package/src/platform/sse/index.ts +7 -7
- package/src/platform/utils/__tests__/validation.test.ts +1083 -1083
- package/src/platform/utils/currency.ts +96 -96
- package/src/platform/utils/debounce.ts +52 -52
- package/src/platform/utils/error.ts +41 -41
- package/src/platform/utils/hmac.test.ts +97 -97
- package/src/platform/utils/index.ts +32 -32
- package/src/platform/utils/server/betterstack-logger.ts +210 -210
- package/src/platform/utils/server/hmac.ts +44 -44
- package/src/platform/utils/server/unsubscribe.ts +111 -111
- package/src/platform/utils/token-counter.ts +96 -96
- package/src/platform/utils/validation.ts +425 -425
- package/src/projects/api-schemas.ts +268 -268
- package/src/published.ts +1 -1
- package/src/reference/_generated/contracts.md +611 -607
- package/src/reference/glossary.md +105 -105
- package/src/requests/__tests__/api-schemas.test.ts +277 -277
- package/src/requests/api-schemas.ts +83 -83
- package/src/requests/index.ts +1 -1
- package/src/scaffold-registry/__tests__/index.test.ts +17 -0
- package/src/scaffold-registry/__tests__/schema.test.ts +329 -230
- package/src/scaffold-registry/index.ts +205 -189
- package/src/scaffold-registry/schema.ts +196 -128
- package/src/server.ts +272 -272
- package/src/supabase/database.types.ts +2719 -2719
- package/src/supabase/helpers.ts +20 -20
- package/src/supabase/index.ts +52 -52
- package/src/supabase/server/client.ts +58 -58
- package/src/test-utils/README.md +38 -38
- package/src/test-utils/browser-mocks.ts +54 -54
- package/src/test-utils/fixtures/api-keys.ts +52 -52
- package/src/test-utils/fixtures/index.ts +4 -4
- package/src/test-utils/fixtures/memberships.ts +80 -80
- package/src/test-utils/fixtures/organizations.ts +69 -69
- package/src/test-utils/fixtures/users.ts +79 -79
- package/src/test-utils/index.ts +11 -11
- package/src/test-utils/mocks/index.ts +2 -2
- package/src/test-utils/mocks/supabase.ts +142 -142
- package/src/test-utils/mocks/workos.ts +108 -108
- package/src/test-utils/rls/RLSTestContext.ts +556 -556
- package/src/test-utils/rls/index.ts +1 -1
|
@@ -1,471 +1,471 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
-
import { InMemoryRateLimiter, createRateLimiter } from '../rate-limiter'
|
|
3
|
-
|
|
4
|
-
describe('InMemoryRateLimiter', () => {
|
|
5
|
-
let limiter: InMemoryRateLimiter
|
|
6
|
-
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
limiter = new InMemoryRateLimiter(10, 60000) // 10 requests per minute
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
describe('constructor', () => {
|
|
12
|
-
it('creates limiter with default values', () => {
|
|
13
|
-
const defaultLimiter = new InMemoryRateLimiter()
|
|
14
|
-
|
|
15
|
-
expect(defaultLimiter).toBeInstanceOf(InMemoryRateLimiter)
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it('creates limiter with custom capacity', () => {
|
|
19
|
-
const customLimiter = new InMemoryRateLimiter(100, 60000)
|
|
20
|
-
|
|
21
|
-
expect(customLimiter).toBeInstanceOf(InMemoryRateLimiter)
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it('creates limiter with custom refill interval', () => {
|
|
25
|
-
const customLimiter = new InMemoryRateLimiter(10, 30000)
|
|
26
|
-
|
|
27
|
-
expect(customLimiter).toBeInstanceOf(InMemoryRateLimiter)
|
|
28
|
-
})
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
describe('allow', () => {
|
|
32
|
-
it('allows first request', async () => {
|
|
33
|
-
const allowed = await limiter.allow('org123:gmail')
|
|
34
|
-
|
|
35
|
-
expect(allowed).toBe(true)
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
it('allows requests up to capacity', async () => {
|
|
39
|
-
vi.useFakeTimers()
|
|
40
|
-
const key = 'org123:gmail'
|
|
41
|
-
|
|
42
|
-
// Should allow 10 requests
|
|
43
|
-
for (let i = 0; i < 10; i++) {
|
|
44
|
-
const allowed = await limiter.allow(key)
|
|
45
|
-
expect(allowed).toBe(true)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// 11th request should be denied
|
|
49
|
-
const denied = await limiter.allow(key)
|
|
50
|
-
expect(denied).toBe(false)
|
|
51
|
-
|
|
52
|
-
vi.useRealTimers()
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('denies requests after capacity exhausted', async () => {
|
|
56
|
-
vi.useFakeTimers()
|
|
57
|
-
const key = 'org123:gmail'
|
|
58
|
-
|
|
59
|
-
// Exhaust capacity
|
|
60
|
-
for (let i = 0; i < 10; i++) {
|
|
61
|
-
await limiter.allow(key)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Should deny
|
|
65
|
-
const denied = await limiter.allow(key)
|
|
66
|
-
expect(denied).toBe(false)
|
|
67
|
-
|
|
68
|
-
vi.useRealTimers()
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('tracks separate keys independently', async () => {
|
|
72
|
-
const key1 = 'org123:gmail'
|
|
73
|
-
const key2 = 'org456:slack'
|
|
74
|
-
|
|
75
|
-
// Exhaust key1
|
|
76
|
-
for (let i = 0; i < 10; i++) {
|
|
77
|
-
await limiter.allow(key1)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// key2 should still be allowed
|
|
81
|
-
const allowed = await limiter.allow(key2)
|
|
82
|
-
expect(allowed).toBe(true)
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('consumes token when allowing request', async () => {
|
|
86
|
-
const key = 'org123:gmail'
|
|
87
|
-
|
|
88
|
-
const remaining1 = await limiter.remaining(key)
|
|
89
|
-
await limiter.allow(key)
|
|
90
|
-
const remaining2 = await limiter.remaining(key)
|
|
91
|
-
|
|
92
|
-
expect(remaining2).toBe(remaining1 - 1)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it.skip('does not consume token when denying request', async () => {
|
|
96
|
-
// SKIPPED: Flaky test with timing issues - rate limiter window may refill between requests
|
|
97
|
-
const key = 'org123:gmail'
|
|
98
|
-
|
|
99
|
-
// Exhaust capacity
|
|
100
|
-
for (let i = 0; i < 10; i++) {
|
|
101
|
-
await limiter.allow(key)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const remaining1 = await limiter.remaining(key)
|
|
105
|
-
await limiter.allow(key) // Denied
|
|
106
|
-
const remaining2 = await limiter.remaining(key)
|
|
107
|
-
|
|
108
|
-
expect(remaining2).toBe(remaining1)
|
|
109
|
-
})
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
describe('remaining', () => {
|
|
113
|
-
it('returns capacity for new key', async () => {
|
|
114
|
-
const remaining = await limiter.remaining('org123:gmail')
|
|
115
|
-
|
|
116
|
-
expect(remaining).toBe(10)
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
it('returns decremented count after requests', async () => {
|
|
120
|
-
const key = 'org123:gmail'
|
|
121
|
-
|
|
122
|
-
await limiter.allow(key)
|
|
123
|
-
await limiter.allow(key)
|
|
124
|
-
await limiter.allow(key)
|
|
125
|
-
|
|
126
|
-
const remaining = await limiter.remaining(key)
|
|
127
|
-
expect(remaining).toBe(7)
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
it('returns 0 after capacity exhausted', async () => {
|
|
131
|
-
const key = 'org123:gmail'
|
|
132
|
-
|
|
133
|
-
// Exhaust capacity
|
|
134
|
-
for (let i = 0; i < 10; i++) {
|
|
135
|
-
await limiter.allow(key)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const remaining = await limiter.remaining(key)
|
|
139
|
-
expect(remaining).toBe(0)
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
it('returns integer (floors fractional tokens)', async () => {
|
|
143
|
-
const key = 'org123:gmail'
|
|
144
|
-
|
|
145
|
-
const remaining = await limiter.remaining(key)
|
|
146
|
-
|
|
147
|
-
expect(Number.isInteger(remaining)).toBe(true)
|
|
148
|
-
})
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
describe('reset', () => {
|
|
152
|
-
it('removes bucket for key', async () => {
|
|
153
|
-
const key = 'org123:gmail'
|
|
154
|
-
|
|
155
|
-
// Use some tokens
|
|
156
|
-
await limiter.allow(key)
|
|
157
|
-
await limiter.allow(key)
|
|
158
|
-
await limiter.allow(key)
|
|
159
|
-
|
|
160
|
-
const remaining1 = await limiter.remaining(key)
|
|
161
|
-
expect(remaining1).toBe(7)
|
|
162
|
-
|
|
163
|
-
// Reset
|
|
164
|
-
await limiter.reset(key)
|
|
165
|
-
|
|
166
|
-
// Should have full capacity again
|
|
167
|
-
const remaining2 = await limiter.remaining(key)
|
|
168
|
-
expect(remaining2).toBe(10)
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
it('does not affect other keys', async () => {
|
|
172
|
-
const key1 = 'org123:gmail'
|
|
173
|
-
const key2 = 'org456:slack'
|
|
174
|
-
|
|
175
|
-
await limiter.allow(key1)
|
|
176
|
-
await limiter.allow(key2)
|
|
177
|
-
await limiter.allow(key2)
|
|
178
|
-
|
|
179
|
-
await limiter.reset(key1)
|
|
180
|
-
|
|
181
|
-
const remaining1 = await limiter.remaining(key1)
|
|
182
|
-
const remaining2 = await limiter.remaining(key2)
|
|
183
|
-
|
|
184
|
-
expect(remaining1).toBe(10) // Reset
|
|
185
|
-
expect(remaining2).toBe(8) // Unchanged
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
it('handles resetting non-existent key', async () => {
|
|
189
|
-
await expect(limiter.reset('nonexistent')).resolves.toBeUndefined()
|
|
190
|
-
})
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
describe('refill behavior', () => {
|
|
194
|
-
let testLimiter: InMemoryRateLimiter
|
|
195
|
-
|
|
196
|
-
beforeEach(() => {
|
|
197
|
-
vi.useFakeTimers()
|
|
198
|
-
testLimiter = new InMemoryRateLimiter(10, 60000)
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
afterEach(() => {
|
|
202
|
-
vi.useRealTimers()
|
|
203
|
-
testLimiter.clearAll()
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
it('refills tokens after refill interval', async () => {
|
|
207
|
-
const key = 'org123:gmail'
|
|
208
|
-
|
|
209
|
-
// Exhaust capacity
|
|
210
|
-
for (let i = 0; i < 10; i++) {
|
|
211
|
-
await testLimiter.allow(key)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
expect(await testLimiter.remaining(key)).toBe(0)
|
|
215
|
-
|
|
216
|
-
// Advance time by refill interval
|
|
217
|
-
vi.advanceTimersByTime(60000)
|
|
218
|
-
|
|
219
|
-
// Should have full capacity again
|
|
220
|
-
expect(await testLimiter.remaining(key)).toBe(10)
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
it('partially refills tokens based on elapsed time', async () => {
|
|
224
|
-
const key = 'org123:gmail'
|
|
225
|
-
|
|
226
|
-
// Use all tokens
|
|
227
|
-
for (let i = 0; i < 10; i++) {
|
|
228
|
-
await testLimiter.allow(key)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
expect(await testLimiter.remaining(key)).toBe(0)
|
|
232
|
-
|
|
233
|
-
// Advance time by half interval (30 seconds)
|
|
234
|
-
vi.advanceTimersByTime(30000)
|
|
235
|
-
|
|
236
|
-
// Should have ~5 tokens (50% of capacity)
|
|
237
|
-
const remaining = await testLimiter.remaining(key)
|
|
238
|
-
expect(remaining).toBeGreaterThanOrEqual(4)
|
|
239
|
-
expect(remaining).toBeLessThanOrEqual(6)
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
it('caps refill at capacity', async () => {
|
|
243
|
-
const key = 'org123:gmail'
|
|
244
|
-
|
|
245
|
-
await testLimiter.allow(key)
|
|
246
|
-
|
|
247
|
-
// Advance time by 2x refill interval
|
|
248
|
-
vi.advanceTimersByTime(120000)
|
|
249
|
-
|
|
250
|
-
// Should not exceed capacity
|
|
251
|
-
expect(await testLimiter.remaining(key)).toBe(10)
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
it('refills linearly over time', async () => {
|
|
255
|
-
const key = 'org123:gmail'
|
|
256
|
-
|
|
257
|
-
// Use all tokens
|
|
258
|
-
for (let i = 0; i < 10; i++) {
|
|
259
|
-
await testLimiter.allow(key)
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Check refill at different intervals
|
|
263
|
-
const intervals = [6000, 12000, 18000, 24000, 30000]
|
|
264
|
-
const expectedTokens = [1, 2, 3, 4, 5]
|
|
265
|
-
|
|
266
|
-
for (let i = 0; i < intervals.length; i++) {
|
|
267
|
-
vi.advanceTimersByTime(intervals[i] - (i > 0 ? intervals[i - 1] : 0))
|
|
268
|
-
const remaining = await testLimiter.remaining(key)
|
|
269
|
-
|
|
270
|
-
// Allow some variance due to floating point
|
|
271
|
-
expect(remaining).toBeGreaterThanOrEqual(expectedTokens[i] - 1)
|
|
272
|
-
expect(remaining).toBeLessThanOrEqual(expectedTokens[i] + 1)
|
|
273
|
-
}
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
it('updates lastRefill timestamp on each check', async () => {
|
|
277
|
-
const key = 'org123:gmail'
|
|
278
|
-
|
|
279
|
-
await testLimiter.allow(key)
|
|
280
|
-
vi.advanceTimersByTime(10000)
|
|
281
|
-
await testLimiter.remaining(key)
|
|
282
|
-
vi.advanceTimersByTime(10000)
|
|
283
|
-
|
|
284
|
-
const remaining = await testLimiter.remaining(key)
|
|
285
|
-
|
|
286
|
-
// Should have refilled tokens
|
|
287
|
-
expect(remaining).toBeGreaterThan(9)
|
|
288
|
-
})
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
describe('clearAll', () => {
|
|
292
|
-
it('removes all buckets', async () => {
|
|
293
|
-
const key1 = 'org123:gmail'
|
|
294
|
-
const key2 = 'org456:slack'
|
|
295
|
-
|
|
296
|
-
// Use tokens
|
|
297
|
-
await limiter.allow(key1)
|
|
298
|
-
await limiter.allow(key2)
|
|
299
|
-
await limiter.allow(key2)
|
|
300
|
-
|
|
301
|
-
limiter.clearAll()
|
|
302
|
-
|
|
303
|
-
// Both should have full capacity again
|
|
304
|
-
expect(await limiter.remaining(key1)).toBe(10)
|
|
305
|
-
expect(await limiter.remaining(key2)).toBe(10)
|
|
306
|
-
})
|
|
307
|
-
|
|
308
|
-
it.skip('allows fresh start after clear', async () => {
|
|
309
|
-
// SKIPPED: Flaky test with timing issues - rate limiter window may refill between requests
|
|
310
|
-
const key = 'org123:gmail'
|
|
311
|
-
|
|
312
|
-
// Exhaust capacity
|
|
313
|
-
for (let i = 0; i < 10; i++) {
|
|
314
|
-
await limiter.allow(key)
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
expect(await limiter.allow(key)).toBe(false)
|
|
318
|
-
|
|
319
|
-
limiter.clearAll()
|
|
320
|
-
|
|
321
|
-
// Should allow requests again
|
|
322
|
-
expect(await limiter.allow(key)).toBe(true)
|
|
323
|
-
})
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
describe('concurrent requests', () => {
|
|
327
|
-
it.skip('handles concurrent requests for same key', async () => {
|
|
328
|
-
// SKIPPED: This test has a race condition due to non-atomic check-then-act
|
|
329
|
-
// in the InMemoryRateLimiter.allow() method. The test is flaky and can
|
|
330
|
-
// occasionally fail when concurrent requests interleave during token checks.
|
|
331
|
-
// The in-memory implementation is for development/testing only - production
|
|
332
|
-
// uses Redis with atomic operations.
|
|
333
|
-
const key = 'org123:gmail'
|
|
334
|
-
|
|
335
|
-
const promises = Array.from({ length: 20 }, () => limiter.allow(key))
|
|
336
|
-
const results = await Promise.all(promises)
|
|
337
|
-
|
|
338
|
-
const allowed = results.filter((r) => r).length
|
|
339
|
-
const denied = results.filter((r) => !r).length
|
|
340
|
-
|
|
341
|
-
expect(allowed).toBe(10) // Capacity
|
|
342
|
-
expect(denied).toBe(10) // Excess
|
|
343
|
-
})
|
|
344
|
-
|
|
345
|
-
it('handles concurrent requests for different keys', async () => {
|
|
346
|
-
const promises = [
|
|
347
|
-
...Array.from({ length: 5 }, () => limiter.allow('org123:gmail')),
|
|
348
|
-
...Array.from({ length: 5 }, () => limiter.allow('org456:slack'))
|
|
349
|
-
]
|
|
350
|
-
|
|
351
|
-
const results = await Promise.all(promises)
|
|
352
|
-
|
|
353
|
-
// All should be allowed (within capacity for each key)
|
|
354
|
-
expect(results.every((r) => r)).toBe(true)
|
|
355
|
-
})
|
|
356
|
-
})
|
|
357
|
-
|
|
358
|
-
describe('edge cases', () => {
|
|
359
|
-
it('handles fractional token refills correctly', async () => {
|
|
360
|
-
vi.useFakeTimers()
|
|
361
|
-
|
|
362
|
-
const key = 'org123:gmail'
|
|
363
|
-
|
|
364
|
-
// Use all tokens
|
|
365
|
-
for (let i = 0; i < 10; i++) {
|
|
366
|
-
await limiter.allow(key)
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Advance by 33% of interval (should add ~3.3 tokens)
|
|
370
|
-
vi.advanceTimersByTime(20000)
|
|
371
|
-
|
|
372
|
-
const remaining = await limiter.remaining(key)
|
|
373
|
-
|
|
374
|
-
// Should floor to integer tokens
|
|
375
|
-
expect(Number.isInteger(remaining)).toBe(true)
|
|
376
|
-
expect(remaining).toBeGreaterThanOrEqual(3)
|
|
377
|
-
expect(remaining).toBeLessThanOrEqual(4)
|
|
378
|
-
|
|
379
|
-
vi.useRealTimers()
|
|
380
|
-
})
|
|
381
|
-
})
|
|
382
|
-
})
|
|
383
|
-
|
|
384
|
-
describe('createRateLimiter', () => {
|
|
385
|
-
it('creates InMemoryRateLimiter instance', () => {
|
|
386
|
-
const limiter = createRateLimiter()
|
|
387
|
-
|
|
388
|
-
expect(limiter).toBeInstanceOf(InMemoryRateLimiter)
|
|
389
|
-
})
|
|
390
|
-
|
|
391
|
-
it('creates limiter with default config (300 req/min)', async () => {
|
|
392
|
-
vi.useFakeTimers()
|
|
393
|
-
const limiter = createRateLimiter()
|
|
394
|
-
|
|
395
|
-
// Should allow 300 requests
|
|
396
|
-
for (let i = 0; i < 300; i++) {
|
|
397
|
-
const allowed = await limiter.allow('test')
|
|
398
|
-
expect(allowed).toBe(true)
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// 301st should be denied
|
|
402
|
-
const denied = await limiter.allow('test')
|
|
403
|
-
expect(denied).toBe(false)
|
|
404
|
-
|
|
405
|
-
vi.useRealTimers()
|
|
406
|
-
})
|
|
407
|
-
|
|
408
|
-
it('creates independent limiter instances', async () => {
|
|
409
|
-
const limiter1 = createRateLimiter()
|
|
410
|
-
const limiter2 = createRateLimiter()
|
|
411
|
-
|
|
412
|
-
// Exhaust limiter1
|
|
413
|
-
for (let i = 0; i < 300; i++) {
|
|
414
|
-
await limiter1.allow('test')
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// limiter2 should still allow
|
|
418
|
-
const allowed = await limiter2.allow('test')
|
|
419
|
-
expect(allowed).toBe(true)
|
|
420
|
-
})
|
|
421
|
-
})
|
|
422
|
-
|
|
423
|
-
describe('Rate Limiter Integration', () => {
|
|
424
|
-
it('supports typical organization-integration pattern', async () => {
|
|
425
|
-
const limiter = createRateLimiter()
|
|
426
|
-
|
|
427
|
-
const key = 'org_acme_test:gmail'
|
|
428
|
-
|
|
429
|
-
// Make 50 requests
|
|
430
|
-
for (let i = 0; i < 50; i++) {
|
|
431
|
-
const allowed = await limiter.allow(key)
|
|
432
|
-
expect(allowed).toBe(true)
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// Check remaining (300 capacity - 50 used = 250)
|
|
436
|
-
const remaining = await limiter.remaining(key)
|
|
437
|
-
expect(remaining).toBe(250)
|
|
438
|
-
})
|
|
439
|
-
|
|
440
|
-
it('isolates different organizations', async () => {
|
|
441
|
-
const limiter = createRateLimiter()
|
|
442
|
-
|
|
443
|
-
const orgA = 'org_acme:gmail'
|
|
444
|
-
const orgB = 'org_beta:gmail'
|
|
445
|
-
|
|
446
|
-
// Exhaust org A
|
|
447
|
-
for (let i = 0; i < 300; i++) {
|
|
448
|
-
await limiter.allow(orgA)
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Org B should still be allowed
|
|
452
|
-
const allowed = await limiter.allow(orgB)
|
|
453
|
-
expect(allowed).toBe(true)
|
|
454
|
-
})
|
|
455
|
-
|
|
456
|
-
it('isolates different integrations', async () => {
|
|
457
|
-
const limiter = createRateLimiter()
|
|
458
|
-
|
|
459
|
-
const gmail = 'org_acme:gmail'
|
|
460
|
-
const slack = 'org_acme:slack'
|
|
461
|
-
|
|
462
|
-
// Exhaust Gmail
|
|
463
|
-
for (let i = 0; i < 300; i++) {
|
|
464
|
-
await limiter.allow(gmail)
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Slack should still be allowed
|
|
468
|
-
const allowed = await limiter.allow(slack)
|
|
469
|
-
expect(allowed).toBe(true)
|
|
470
|
-
})
|
|
471
|
-
})
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
+
import { InMemoryRateLimiter, createRateLimiter } from '../rate-limiter'
|
|
3
|
+
|
|
4
|
+
describe('InMemoryRateLimiter', () => {
|
|
5
|
+
let limiter: InMemoryRateLimiter
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
limiter = new InMemoryRateLimiter(10, 60000) // 10 requests per minute
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe('constructor', () => {
|
|
12
|
+
it('creates limiter with default values', () => {
|
|
13
|
+
const defaultLimiter = new InMemoryRateLimiter()
|
|
14
|
+
|
|
15
|
+
expect(defaultLimiter).toBeInstanceOf(InMemoryRateLimiter)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('creates limiter with custom capacity', () => {
|
|
19
|
+
const customLimiter = new InMemoryRateLimiter(100, 60000)
|
|
20
|
+
|
|
21
|
+
expect(customLimiter).toBeInstanceOf(InMemoryRateLimiter)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('creates limiter with custom refill interval', () => {
|
|
25
|
+
const customLimiter = new InMemoryRateLimiter(10, 30000)
|
|
26
|
+
|
|
27
|
+
expect(customLimiter).toBeInstanceOf(InMemoryRateLimiter)
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('allow', () => {
|
|
32
|
+
it('allows first request', async () => {
|
|
33
|
+
const allowed = await limiter.allow('org123:gmail')
|
|
34
|
+
|
|
35
|
+
expect(allowed).toBe(true)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('allows requests up to capacity', async () => {
|
|
39
|
+
vi.useFakeTimers()
|
|
40
|
+
const key = 'org123:gmail'
|
|
41
|
+
|
|
42
|
+
// Should allow 10 requests
|
|
43
|
+
for (let i = 0; i < 10; i++) {
|
|
44
|
+
const allowed = await limiter.allow(key)
|
|
45
|
+
expect(allowed).toBe(true)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 11th request should be denied
|
|
49
|
+
const denied = await limiter.allow(key)
|
|
50
|
+
expect(denied).toBe(false)
|
|
51
|
+
|
|
52
|
+
vi.useRealTimers()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('denies requests after capacity exhausted', async () => {
|
|
56
|
+
vi.useFakeTimers()
|
|
57
|
+
const key = 'org123:gmail'
|
|
58
|
+
|
|
59
|
+
// Exhaust capacity
|
|
60
|
+
for (let i = 0; i < 10; i++) {
|
|
61
|
+
await limiter.allow(key)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Should deny
|
|
65
|
+
const denied = await limiter.allow(key)
|
|
66
|
+
expect(denied).toBe(false)
|
|
67
|
+
|
|
68
|
+
vi.useRealTimers()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('tracks separate keys independently', async () => {
|
|
72
|
+
const key1 = 'org123:gmail'
|
|
73
|
+
const key2 = 'org456:slack'
|
|
74
|
+
|
|
75
|
+
// Exhaust key1
|
|
76
|
+
for (let i = 0; i < 10; i++) {
|
|
77
|
+
await limiter.allow(key1)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// key2 should still be allowed
|
|
81
|
+
const allowed = await limiter.allow(key2)
|
|
82
|
+
expect(allowed).toBe(true)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('consumes token when allowing request', async () => {
|
|
86
|
+
const key = 'org123:gmail'
|
|
87
|
+
|
|
88
|
+
const remaining1 = await limiter.remaining(key)
|
|
89
|
+
await limiter.allow(key)
|
|
90
|
+
const remaining2 = await limiter.remaining(key)
|
|
91
|
+
|
|
92
|
+
expect(remaining2).toBe(remaining1 - 1)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it.skip('does not consume token when denying request', async () => {
|
|
96
|
+
// SKIPPED: Flaky test with timing issues - rate limiter window may refill between requests
|
|
97
|
+
const key = 'org123:gmail'
|
|
98
|
+
|
|
99
|
+
// Exhaust capacity
|
|
100
|
+
for (let i = 0; i < 10; i++) {
|
|
101
|
+
await limiter.allow(key)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const remaining1 = await limiter.remaining(key)
|
|
105
|
+
await limiter.allow(key) // Denied
|
|
106
|
+
const remaining2 = await limiter.remaining(key)
|
|
107
|
+
|
|
108
|
+
expect(remaining2).toBe(remaining1)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('remaining', () => {
|
|
113
|
+
it('returns capacity for new key', async () => {
|
|
114
|
+
const remaining = await limiter.remaining('org123:gmail')
|
|
115
|
+
|
|
116
|
+
expect(remaining).toBe(10)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('returns decremented count after requests', async () => {
|
|
120
|
+
const key = 'org123:gmail'
|
|
121
|
+
|
|
122
|
+
await limiter.allow(key)
|
|
123
|
+
await limiter.allow(key)
|
|
124
|
+
await limiter.allow(key)
|
|
125
|
+
|
|
126
|
+
const remaining = await limiter.remaining(key)
|
|
127
|
+
expect(remaining).toBe(7)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('returns 0 after capacity exhausted', async () => {
|
|
131
|
+
const key = 'org123:gmail'
|
|
132
|
+
|
|
133
|
+
// Exhaust capacity
|
|
134
|
+
for (let i = 0; i < 10; i++) {
|
|
135
|
+
await limiter.allow(key)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const remaining = await limiter.remaining(key)
|
|
139
|
+
expect(remaining).toBe(0)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('returns integer (floors fractional tokens)', async () => {
|
|
143
|
+
const key = 'org123:gmail'
|
|
144
|
+
|
|
145
|
+
const remaining = await limiter.remaining(key)
|
|
146
|
+
|
|
147
|
+
expect(Number.isInteger(remaining)).toBe(true)
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
describe('reset', () => {
|
|
152
|
+
it('removes bucket for key', async () => {
|
|
153
|
+
const key = 'org123:gmail'
|
|
154
|
+
|
|
155
|
+
// Use some tokens
|
|
156
|
+
await limiter.allow(key)
|
|
157
|
+
await limiter.allow(key)
|
|
158
|
+
await limiter.allow(key)
|
|
159
|
+
|
|
160
|
+
const remaining1 = await limiter.remaining(key)
|
|
161
|
+
expect(remaining1).toBe(7)
|
|
162
|
+
|
|
163
|
+
// Reset
|
|
164
|
+
await limiter.reset(key)
|
|
165
|
+
|
|
166
|
+
// Should have full capacity again
|
|
167
|
+
const remaining2 = await limiter.remaining(key)
|
|
168
|
+
expect(remaining2).toBe(10)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('does not affect other keys', async () => {
|
|
172
|
+
const key1 = 'org123:gmail'
|
|
173
|
+
const key2 = 'org456:slack'
|
|
174
|
+
|
|
175
|
+
await limiter.allow(key1)
|
|
176
|
+
await limiter.allow(key2)
|
|
177
|
+
await limiter.allow(key2)
|
|
178
|
+
|
|
179
|
+
await limiter.reset(key1)
|
|
180
|
+
|
|
181
|
+
const remaining1 = await limiter.remaining(key1)
|
|
182
|
+
const remaining2 = await limiter.remaining(key2)
|
|
183
|
+
|
|
184
|
+
expect(remaining1).toBe(10) // Reset
|
|
185
|
+
expect(remaining2).toBe(8) // Unchanged
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('handles resetting non-existent key', async () => {
|
|
189
|
+
await expect(limiter.reset('nonexistent')).resolves.toBeUndefined()
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
describe('refill behavior', () => {
|
|
194
|
+
let testLimiter: InMemoryRateLimiter
|
|
195
|
+
|
|
196
|
+
beforeEach(() => {
|
|
197
|
+
vi.useFakeTimers()
|
|
198
|
+
testLimiter = new InMemoryRateLimiter(10, 60000)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
afterEach(() => {
|
|
202
|
+
vi.useRealTimers()
|
|
203
|
+
testLimiter.clearAll()
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('refills tokens after refill interval', async () => {
|
|
207
|
+
const key = 'org123:gmail'
|
|
208
|
+
|
|
209
|
+
// Exhaust capacity
|
|
210
|
+
for (let i = 0; i < 10; i++) {
|
|
211
|
+
await testLimiter.allow(key)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
expect(await testLimiter.remaining(key)).toBe(0)
|
|
215
|
+
|
|
216
|
+
// Advance time by refill interval
|
|
217
|
+
vi.advanceTimersByTime(60000)
|
|
218
|
+
|
|
219
|
+
// Should have full capacity again
|
|
220
|
+
expect(await testLimiter.remaining(key)).toBe(10)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('partially refills tokens based on elapsed time', async () => {
|
|
224
|
+
const key = 'org123:gmail'
|
|
225
|
+
|
|
226
|
+
// Use all tokens
|
|
227
|
+
for (let i = 0; i < 10; i++) {
|
|
228
|
+
await testLimiter.allow(key)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
expect(await testLimiter.remaining(key)).toBe(0)
|
|
232
|
+
|
|
233
|
+
// Advance time by half interval (30 seconds)
|
|
234
|
+
vi.advanceTimersByTime(30000)
|
|
235
|
+
|
|
236
|
+
// Should have ~5 tokens (50% of capacity)
|
|
237
|
+
const remaining = await testLimiter.remaining(key)
|
|
238
|
+
expect(remaining).toBeGreaterThanOrEqual(4)
|
|
239
|
+
expect(remaining).toBeLessThanOrEqual(6)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('caps refill at capacity', async () => {
|
|
243
|
+
const key = 'org123:gmail'
|
|
244
|
+
|
|
245
|
+
await testLimiter.allow(key)
|
|
246
|
+
|
|
247
|
+
// Advance time by 2x refill interval
|
|
248
|
+
vi.advanceTimersByTime(120000)
|
|
249
|
+
|
|
250
|
+
// Should not exceed capacity
|
|
251
|
+
expect(await testLimiter.remaining(key)).toBe(10)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('refills linearly over time', async () => {
|
|
255
|
+
const key = 'org123:gmail'
|
|
256
|
+
|
|
257
|
+
// Use all tokens
|
|
258
|
+
for (let i = 0; i < 10; i++) {
|
|
259
|
+
await testLimiter.allow(key)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Check refill at different intervals
|
|
263
|
+
const intervals = [6000, 12000, 18000, 24000, 30000]
|
|
264
|
+
const expectedTokens = [1, 2, 3, 4, 5]
|
|
265
|
+
|
|
266
|
+
for (let i = 0; i < intervals.length; i++) {
|
|
267
|
+
vi.advanceTimersByTime(intervals[i] - (i > 0 ? intervals[i - 1] : 0))
|
|
268
|
+
const remaining = await testLimiter.remaining(key)
|
|
269
|
+
|
|
270
|
+
// Allow some variance due to floating point
|
|
271
|
+
expect(remaining).toBeGreaterThanOrEqual(expectedTokens[i] - 1)
|
|
272
|
+
expect(remaining).toBeLessThanOrEqual(expectedTokens[i] + 1)
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('updates lastRefill timestamp on each check', async () => {
|
|
277
|
+
const key = 'org123:gmail'
|
|
278
|
+
|
|
279
|
+
await testLimiter.allow(key)
|
|
280
|
+
vi.advanceTimersByTime(10000)
|
|
281
|
+
await testLimiter.remaining(key)
|
|
282
|
+
vi.advanceTimersByTime(10000)
|
|
283
|
+
|
|
284
|
+
const remaining = await testLimiter.remaining(key)
|
|
285
|
+
|
|
286
|
+
// Should have refilled tokens
|
|
287
|
+
expect(remaining).toBeGreaterThan(9)
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
describe('clearAll', () => {
|
|
292
|
+
it('removes all buckets', async () => {
|
|
293
|
+
const key1 = 'org123:gmail'
|
|
294
|
+
const key2 = 'org456:slack'
|
|
295
|
+
|
|
296
|
+
// Use tokens
|
|
297
|
+
await limiter.allow(key1)
|
|
298
|
+
await limiter.allow(key2)
|
|
299
|
+
await limiter.allow(key2)
|
|
300
|
+
|
|
301
|
+
limiter.clearAll()
|
|
302
|
+
|
|
303
|
+
// Both should have full capacity again
|
|
304
|
+
expect(await limiter.remaining(key1)).toBe(10)
|
|
305
|
+
expect(await limiter.remaining(key2)).toBe(10)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it.skip('allows fresh start after clear', async () => {
|
|
309
|
+
// SKIPPED: Flaky test with timing issues - rate limiter window may refill between requests
|
|
310
|
+
const key = 'org123:gmail'
|
|
311
|
+
|
|
312
|
+
// Exhaust capacity
|
|
313
|
+
for (let i = 0; i < 10; i++) {
|
|
314
|
+
await limiter.allow(key)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
expect(await limiter.allow(key)).toBe(false)
|
|
318
|
+
|
|
319
|
+
limiter.clearAll()
|
|
320
|
+
|
|
321
|
+
// Should allow requests again
|
|
322
|
+
expect(await limiter.allow(key)).toBe(true)
|
|
323
|
+
})
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
describe('concurrent requests', () => {
|
|
327
|
+
it.skip('handles concurrent requests for same key', async () => {
|
|
328
|
+
// SKIPPED: This test has a race condition due to non-atomic check-then-act
|
|
329
|
+
// in the InMemoryRateLimiter.allow() method. The test is flaky and can
|
|
330
|
+
// occasionally fail when concurrent requests interleave during token checks.
|
|
331
|
+
// The in-memory implementation is for development/testing only - production
|
|
332
|
+
// uses Redis with atomic operations.
|
|
333
|
+
const key = 'org123:gmail'
|
|
334
|
+
|
|
335
|
+
const promises = Array.from({ length: 20 }, () => limiter.allow(key))
|
|
336
|
+
const results = await Promise.all(promises)
|
|
337
|
+
|
|
338
|
+
const allowed = results.filter((r) => r).length
|
|
339
|
+
const denied = results.filter((r) => !r).length
|
|
340
|
+
|
|
341
|
+
expect(allowed).toBe(10) // Capacity
|
|
342
|
+
expect(denied).toBe(10) // Excess
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('handles concurrent requests for different keys', async () => {
|
|
346
|
+
const promises = [
|
|
347
|
+
...Array.from({ length: 5 }, () => limiter.allow('org123:gmail')),
|
|
348
|
+
...Array.from({ length: 5 }, () => limiter.allow('org456:slack'))
|
|
349
|
+
]
|
|
350
|
+
|
|
351
|
+
const results = await Promise.all(promises)
|
|
352
|
+
|
|
353
|
+
// All should be allowed (within capacity for each key)
|
|
354
|
+
expect(results.every((r) => r)).toBe(true)
|
|
355
|
+
})
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
describe('edge cases', () => {
|
|
359
|
+
it('handles fractional token refills correctly', async () => {
|
|
360
|
+
vi.useFakeTimers()
|
|
361
|
+
|
|
362
|
+
const key = 'org123:gmail'
|
|
363
|
+
|
|
364
|
+
// Use all tokens
|
|
365
|
+
for (let i = 0; i < 10; i++) {
|
|
366
|
+
await limiter.allow(key)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Advance by 33% of interval (should add ~3.3 tokens)
|
|
370
|
+
vi.advanceTimersByTime(20000)
|
|
371
|
+
|
|
372
|
+
const remaining = await limiter.remaining(key)
|
|
373
|
+
|
|
374
|
+
// Should floor to integer tokens
|
|
375
|
+
expect(Number.isInteger(remaining)).toBe(true)
|
|
376
|
+
expect(remaining).toBeGreaterThanOrEqual(3)
|
|
377
|
+
expect(remaining).toBeLessThanOrEqual(4)
|
|
378
|
+
|
|
379
|
+
vi.useRealTimers()
|
|
380
|
+
})
|
|
381
|
+
})
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
describe('createRateLimiter', () => {
|
|
385
|
+
it('creates InMemoryRateLimiter instance', () => {
|
|
386
|
+
const limiter = createRateLimiter()
|
|
387
|
+
|
|
388
|
+
expect(limiter).toBeInstanceOf(InMemoryRateLimiter)
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it('creates limiter with default config (300 req/min)', async () => {
|
|
392
|
+
vi.useFakeTimers()
|
|
393
|
+
const limiter = createRateLimiter()
|
|
394
|
+
|
|
395
|
+
// Should allow 300 requests
|
|
396
|
+
for (let i = 0; i < 300; i++) {
|
|
397
|
+
const allowed = await limiter.allow('test')
|
|
398
|
+
expect(allowed).toBe(true)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// 301st should be denied
|
|
402
|
+
const denied = await limiter.allow('test')
|
|
403
|
+
expect(denied).toBe(false)
|
|
404
|
+
|
|
405
|
+
vi.useRealTimers()
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it('creates independent limiter instances', async () => {
|
|
409
|
+
const limiter1 = createRateLimiter()
|
|
410
|
+
const limiter2 = createRateLimiter()
|
|
411
|
+
|
|
412
|
+
// Exhaust limiter1
|
|
413
|
+
for (let i = 0; i < 300; i++) {
|
|
414
|
+
await limiter1.allow('test')
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// limiter2 should still allow
|
|
418
|
+
const allowed = await limiter2.allow('test')
|
|
419
|
+
expect(allowed).toBe(true)
|
|
420
|
+
})
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
describe('Rate Limiter Integration', () => {
|
|
424
|
+
it('supports typical organization-integration pattern', async () => {
|
|
425
|
+
const limiter = createRateLimiter()
|
|
426
|
+
|
|
427
|
+
const key = 'org_acme_test:gmail'
|
|
428
|
+
|
|
429
|
+
// Make 50 requests
|
|
430
|
+
for (let i = 0; i < 50; i++) {
|
|
431
|
+
const allowed = await limiter.allow(key)
|
|
432
|
+
expect(allowed).toBe(true)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Check remaining (300 capacity - 50 used = 250)
|
|
436
|
+
const remaining = await limiter.remaining(key)
|
|
437
|
+
expect(remaining).toBe(250)
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
it('isolates different organizations', async () => {
|
|
441
|
+
const limiter = createRateLimiter()
|
|
442
|
+
|
|
443
|
+
const orgA = 'org_acme:gmail'
|
|
444
|
+
const orgB = 'org_beta:gmail'
|
|
445
|
+
|
|
446
|
+
// Exhaust org A
|
|
447
|
+
for (let i = 0; i < 300; i++) {
|
|
448
|
+
await limiter.allow(orgA)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Org B should still be allowed
|
|
452
|
+
const allowed = await limiter.allow(orgB)
|
|
453
|
+
expect(allowed).toBe(true)
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
it('isolates different integrations', async () => {
|
|
457
|
+
const limiter = createRateLimiter()
|
|
458
|
+
|
|
459
|
+
const gmail = 'org_acme:gmail'
|
|
460
|
+
const slack = 'org_acme:slack'
|
|
461
|
+
|
|
462
|
+
// Exhaust Gmail
|
|
463
|
+
for (let i = 0; i < 300; i++) {
|
|
464
|
+
await limiter.allow(gmail)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Slack should still be allowed
|
|
468
|
+
const allowed = await limiter.allow(slack)
|
|
469
|
+
expect(allowed).toBe(true)
|
|
470
|
+
})
|
|
471
|
+
})
|