diff --git a/.changeset/rate-limit-storage-subpath-export.md b/.changeset/rate-limit-storage-subpath-export.md new file mode 100644 index 0000000000..664bdff101 --- /dev/null +++ b/.changeset/rate-limit-storage-subpath-export.md @@ -0,0 +1,54 @@ +--- +"@objectstack/plugin-auth": minor +"@objectstack/runtime": patch +"@objectstack/service-sms": patch +--- + +feat(plugin-auth): the fixed-window counter gets its own `./rate-limit-storage` entry (#6040) + +`rate-limit-storage.ts` is the repo's ONE fixed-window counter — +`incrementFixedWindow` / `createLazyCounterStore` / `InProcessCounterStore`, +ADR-0069 D2 — and #4790's cross-reference asks later arrivals to reuse it +rather than write a third copy. They did, and from outside auth: +`@objectstack/runtime` counts inbound requests and endpoint policy through it, +and `@objectstack/service-sms` counts its daily SMS budget through it (#2814). + +`@objectstack/plugin-auth` published exactly one entry, `"."`, whose `export *` +chain takes **value** imports on `better-auth/adapters` +(`objectql-adapter.ts`) and `@better-auth/core/db` (`backfill-account-issuer.ts`). +Value imports are evaluated eagerly, so reaching those ~90 lines of counting +loaded `better-auth` + `@better-auth/{core,oauth-provider,scim,sso}` + `jose` + +`@noble/hashes` + `@objectstack/rest` + `@objectstack/platform-objects` first. +Measured against the built package: `require('@objectstack/plugin-auth')` puts +109 modules in `require.cache`; the counter needs one. + +So the counter is now published on its own: + +```ts +// before — 109 modules, the whole better-auth family +import { incrementFixedWindow } from '@objectstack/plugin-auth'; +// after — 1 module, 3.7 KB +import { incrementFixedWindow } from '@objectstack/plugin-auth/rate-limit-storage'; +``` + +`tsup` emits the second entry with `splitting: false`, so it is a self-contained +bundle rather than a nominal split: `dist/rate-limit-storage.mjs` is 3.71 KB +against `dist/index.mjs`'s 330.28 KB, contains zero top-level imports and zero +occurrences of the string `better-auth`. The one better-auth reference that +survives is `import type { BetterAuthRateLimitStorage }`, which is erased at +build and costs a consumer nothing at runtime. + +**Nothing is removed.** The root still re-exports every one of these symbols, so +existing `@objectstack/plugin-auth` imports keep working unchanged — this is a +new entry point, which is why it is `minor` rather than breaking. The `patch` on +`runtime` and `service-sms` is the import-specifier switch in those packages; +their behaviour is identical. + +`src/rate-limit-storage-isolation.test.ts` pins the invariant from both sides, +in the shape `packages/types/src/node-isolation.test.ts` (#4700) established for +the `./node` split: it walks the real import graph from the subpath entry and +fails on any better-auth **value** import or any undeclared external package, +it fails if a consumer reaches the counter through the package root again, and +it fails if the root ever *stops* pulling better-auth eagerly — because at that +point the split stopped buying anything and deserves re-measuring rather than a +suite that passes for the wrong reason. diff --git a/packages/plugins/plugin-auth/package.json b/packages/plugins/plugin-auth/package.json index 28ffb424e5..8aaabc33fa 100644 --- a/packages/plugins/plugin-auth/package.json +++ b/packages/plugins/plugin-auth/package.json @@ -8,13 +8,18 @@ "types": "dist/index.d.ts", "exports": { ".": { + "types": "./dist/index.d.ts", "import": "./dist/index.mjs", - "require": "./dist/index.js", - "types": "./dist/index.d.ts" + "require": "./dist/index.js" + }, + "./rate-limit-storage": { + "types": "./dist/rate-limit-storage.d.ts", + "import": "./dist/rate-limit-storage.mjs", + "require": "./dist/rate-limit-storage.js" } }, "scripts": { - "build": "tsup --config ../../../tsup.config.ts", + "build": "tsup", "test": "vitest run", "typecheck": "tsc --noEmit" }, diff --git a/packages/plugins/plugin-auth/src/rate-limit-storage-isolation.test.ts b/packages/plugins/plugin-auth/src/rate-limit-storage-isolation.test.ts new file mode 100644 index 0000000000..9e7d85e361 --- /dev/null +++ b/packages/plugins/plugin-auth/src/rate-limit-storage-isolation.test.ts @@ -0,0 +1,306 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * #6040 — the `./rate-limit-storage` subpath entry must stay free of the + * better-auth family at RUNTIME. + * + * `rate-limit-storage.ts` is the repo's one fixed-window counter (ADR-0069 D2, + * #4790). Three packages outside auth count through it — `@objectstack/runtime` + * (`security/inbound-rate-limit.ts`, `endpoint-policy.ts`, + * `dispatcher-plugin.ts`) and `@objectstack/service-sms` (`sms-plugin.ts`, + * `sms-daily-quota.ts`) — and until #6040 they could only reach it through the + * package root, whose `export *` chain takes **value** imports on + * `better-auth/adapters` and `@better-auth/core/db`. Importing 90 lines of + * counting therefore eagerly evaluated `better-auth` + + * `@better-auth/{core,oauth-provider,scim,sso}` + `jose` + `@noble/hashes` + + * `@objectstack/rest` + `@objectstack/platform-objects`. + * + * That is now a packaging invariant, and an invariant nobody checks is a + * comment. The regression it guards is silent: someone adds one convenient + * import to this module — `@objectstack/rest` for an error type, a better-auth + * helper, anything — every existing test still passes, the counter still + * counts, and the whole family quietly comes back into `service-sms`'s load + * graph. Nothing in the build, the type-check or the unit suites can see it. + * + * So this walks the real import graph from `src/rate-limit-storage.ts` and + * pins the external surface it is allowed to reach. + * + * Deliberately a SOURCE-level scan rather than a probe of `dist/`. A gate that + * reads build output passes or fails by local build state, which is exactly the + * boundary `scripts/check-published-files.mjs` states for itself ("it does not + * exist in a fresh checkout … a gate that passes or fails by accident is worse + * than one with a stated boundary"). The dist-level reading is real but + * one-shot, and it is recorded in the PR: after `pnpm --filter + * @objectstack/plugin-auth build`, `dist/rate-limit-storage.mjs` is 3.71 KB + * against `dist/index.mjs`'s 330.28 KB, and a `node -e "import(…)"` subprocess + * loads zero better-auth modules. + * + * Same shape as `packages/types/src/node-isolation.test.ts` (#4700), which pins + * the identical class of invariant for the `./node` subpath. + */ + +import { describe, it, expect } from 'vitest'; +import { readFileSync, existsSync, readdirSync } from 'node:fs'; +import { dirname, join, relative } from 'node:path'; + +/** + * This package is CJS-typed (no `"type": "module"` — it publishes + * `dist/index.js` as CommonJS), so `module: NodeNext` forbids `import.meta` + * here. Walk up from the CWD instead, which works wherever vitest is invoked + * from. + */ +function findUp(predicate: (dir: string) => boolean, what: string): string { + let dir = process.cwd(); + for (;;) { + if (predicate(dir)) return dir; + const parent = dirname(dir); + if (parent === dir) throw new Error(`could not locate ${what}`); + dir = parent; + } +} + +const PKG = findUp((dir) => { + const manifest = join(dir, 'package.json'); + if (!existsSync(manifest)) return false; + const { name } = JSON.parse(readFileSync(manifest, 'utf8')) as { name?: string }; + return name === '@objectstack/plugin-auth'; +}, 'the @objectstack/plugin-auth package root'); + +const REPO = findUp( + (dir) => existsSync(join(dir, 'pnpm-workspace.yaml')), + 'the workspace root (pnpm-workspace.yaml)', +); + +const SRC = join(PKG, 'src'); + +/** + * Strip comments before scanning. The distinction this file turns on — a + * `import type` versus a value `import` of the same specifier — is invisible to + * a raw-text regex the moment a doc comment quotes an import line, and this + * module's own header quotes several. Handles `//`, block comments and the + * three string forms so a `'http://…'` literal is not mistaken for a comment. + */ +function stripComments(src: string): string { + let out = ''; + let i = 0; + while (i < src.length) { + const c = src[i]!; + const next = src[i + 1]; + if (c === '/' && next === '/') { + while (i < src.length && src[i] !== '\n') i++; + continue; + } + if (c === '/' && next === '*') { + i += 2; + while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) i++; + i += 2; + continue; + } + if (c === "'" || c === '"' || c === '`') { + out += c; + i++; + while (i < src.length && src[i] !== c) { + if (src[i] === '\\') { + out += src[i]! + (src[i + 1] ?? ''); + i += 2; + continue; + } + out += src[i]; + i++; + } + out += c; + i++; + continue; + } + out += c; + i++; + } + return out; +} + +interface Ref { + spec: string; + /** `import type … from` / `export type … from` — erased at build, costs nothing at runtime. */ + typeOnly: boolean; +} + +/** `import|export … from 'x'`, bare `import 'x'`, and `await import('x')`. */ +const FROM = /(?:^|[\n;}])\s*(?:import|export)\b([^'"]*?)\bfrom\s*['"]([^'"]+)['"]/g; +const SIDE_EFFECT = /(?:^|[\n;}])\s*import\s*['"]([^'"]+)['"]/g; +const DYNAMIC = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g; + +function refsOf(file: string): Ref[] { + const src = stripComments(readFileSync(file, 'utf8')); + const out: Ref[] = []; + for (const m of src.matchAll(FROM)) { + out.push({ spec: m[2]!, typeOnly: /^\s*type\b/.test(m[1]!) }); + } + // A side-effect import and a dynamic import are always value loads. + for (const m of src.matchAll(SIDE_EFFECT)) out.push({ spec: m[1]!, typeOnly: false }); + for (const m of src.matchAll(DYNAMIC)) out.push({ spec: m[1]!, typeOnly: false }); + return out; +} + +/** Resolve a relative TS import (`./x.js` → `src/x.ts`). */ +function resolveRelative(fromFile: string, spec: string): string | undefined { + const base = join(dirname(fromFile), spec); + for (const cand of [base.replace(/\.js$/, '.ts'), `${base}.ts`, join(base, 'index.ts')]) { + if (existsSync(cand)) return cand; + } + return undefined; +} + +/** + * Every source file reachable from an entry, following relative imports — + * type-only relative hops included, because a `.ts` file reached only by a type + * import can still carry value imports of its own. + */ +function reachableFrom(entry: string): Map { + const seen = new Map(); + const queue = [entry]; + while (queue.length > 0) { + const file = queue.shift()!; + if (seen.has(file)) continue; + const refs = refsOf(file); + seen.set(file, refs); + for (const ref of refs) { + if (!ref.spec.startsWith('.')) continue; + const next = resolveRelative(file, ref.spec); + if (next) queue.push(next); + } + } + return seen; +} + +const isBetterAuth = (spec: string): boolean => + spec === 'better-auth' || + spec.startsWith('better-auth/') || + spec === '@better-auth/core' || + spec.startsWith('@better-auth/'); + +/** + * The complete set of packages `./rate-limit-storage` may reach, and how. + * + * An allowlist rather than a better-auth denylist on purpose: the expensive + * import is not necessarily spelled `better-auth`. `@objectstack/rest` and + * `@objectstack/platform-objects` both drag the family in transitively, and a + * denylist would wave either through. Anything new here is a deliberate + * decision that has to be written down — including its runtime cost. + */ +const ALLOWED_EXTERNAL: ReadonlyArray<{ spec: string; typeOnly: boolean }> = [ + // Type-only: `BetterAuthRateLimitStorage` is the shape + // `createLazyCacheRateLimitStorage` returns for better-auth's + // `rateLimit.customStorage`. `import type` is erased at build, so it costs a + // consumer nothing at runtime — which is the whole reason this entry can be + // split out while still typing better-auth's seam. + { spec: '@better-auth/core', typeOnly: true }, +]; + +describe('@objectstack/plugin-auth — ./rate-limit-storage stays free of better-auth (#6040)', () => { + it('nothing reachable from the subpath entry VALUE-imports better-auth', () => { + const graph = reachableFrom(join(SRC, 'rate-limit-storage.ts')); + const offenders: string[] = []; + for (const [file, refs] of graph) { + for (const ref of refs) { + if (isBetterAuth(ref.spec) && !ref.typeOnly) { + offenders.push(`${relative(PKG, file)} -> ${ref.spec}`); + } + } + } + expect( + offenders, + 'The ./rate-limit-storage entry exists so @objectstack/runtime and ' + + '@objectstack/service-sms can use the ~90-line fixed-window counter without ' + + 'eagerly loading better-auth + @better-auth/* + jose + @noble/hashes. A value ' + + 'import (anything but `import type`) re-couples them. Keep better-auth types ' + + 'type-only, and put anything that needs the runtime in a root-only module.', + ).toEqual([]); + }); + + it('the subpath entry reaches exactly the external packages it declares', () => { + const graph = reachableFrom(join(SRC, 'rate-limit-storage.ts')); + const actual = new Map(); + for (const refs of graph.values()) { + for (const ref of refs) { + if (ref.spec.startsWith('.') || ref.spec.startsWith('node:')) continue; + // A specifier imported both ways is a value import. + actual.set(ref.spec, (actual.get(ref.spec) ?? true) && ref.typeOnly); + } + } + const sort = (a: { spec: string }, b: { spec: string }): number => a.spec.localeCompare(b.spec); + expect( + [...actual].map(([spec, typeOnly]) => ({ spec, typeOnly })).sort(sort), + 'A new external import on this entry is a new runtime dependency for every ' + + 'consumer of the subpath — including the transitive better-auth pull that ' + + '@objectstack/rest and @objectstack/platform-objects carry. Add it to ' + + 'ALLOWED_EXTERNAL only with its cost written down.', + ).toEqual([...ALLOWED_EXTERNAL].sort(sort)); + }); + + it('the ROOT entry really does value-import better-auth — otherwise this suite proves nothing', () => { + // Guards against the vacuous pass: if the root ever stopped pulling + // better-auth eagerly, the two cases above would go green for the wrong + // reason and the subpath split would look justified when it no longer was. + const graph = reachableFrom(join(SRC, 'index.ts')); + const rootValueImports = new Set(); + for (const refs of graph.values()) { + for (const ref of refs) if (isBetterAuth(ref.spec) && !ref.typeOnly) rootValueImports.add(ref.spec); + } + expect( + [...rootValueImports].sort(), + 'The root no longer eagerly loads better-auth. That is good news, but it means ' + + 'the ./rate-limit-storage split may have stopped buying anything — re-measure ' + + 'before trusting the cases above.', + ).toContain('better-auth/adapters'); + }); + + it('package.json publishes the ./rate-limit-storage subpath', () => { + const pkg = JSON.parse(readFileSync(join(PKG, 'package.json'), 'utf8')) as { + exports: Record>; + files?: string[]; + }; + expect(Object.keys(pkg.exports)).toContain('./rate-limit-storage'); + expect(pkg.exports['./rate-limit-storage']).toEqual({ + // `types` first: conditions are first-match-wins, so a `types` key behind + // `import`/`require` is only ever read by accident. 84 of the 85 exports + // conditions on main are written this way. + types: './dist/rate-limit-storage.d.ts', + import: './dist/rate-limit-storage.mjs', + require: './dist/rate-limit-storage.js', + }); + // `check:published-files` SUFFICIENT covers this too; pinned here so the + // entry cannot be declared and then left out of the published whitelist. + expect(pkg.files).toContain('dist'); + }); + + it('no cross-package consumer reaches the counter through the package ROOT', () => { + // The other half of the invariant. Switching an import back to + // `@objectstack/plugin-auth` costs nothing visible — it type-checks, it + // tests green, it just silently reinstates the whole better-auth load for + // that package. Scanned by directory rather than by filename so moving a + // consumer file does not quietly retire the check. + const COUNTER_SYMBOLS = /\b(incrementFixedWindow|createLazyCounterStore|InProcessCounterStore|CounterStore|FixedWindowCount|LazyCounterStoreOptions)\b/; + const roots = ['packages/runtime/src', 'packages/services/service-sms/src']; + const offenders: string[] = []; + for (const root of roots) { + const abs = join(REPO, root); + expect(existsSync(abs), `${root} — consumer directory moved; re-point this check`).toBe(true); + for (const entry of readdirSync(abs, { recursive: true, withFileTypes: true })) { + if (!entry.isFile() || !entry.name.endsWith('.ts')) continue; + const file = join(entry.parentPath, entry.name); + for (const m of stripComments(readFileSync(file, 'utf8')).matchAll(FROM)) { + if (m[2] !== '@objectstack/plugin-auth') continue; + if (COUNTER_SYMBOLS.test(m[1]!)) { + offenders.push(`${relative(REPO, file)} -> ${m[1]!.trim()} from the package root`); + } + } + } + } + expect( + offenders, + 'Import the counter from "@objectstack/plugin-auth/rate-limit-storage". Reaching ' + + 'it through the package root pulls better-auth + @better-auth/* + jose + ' + + '@noble/hashes into this package for ~90 lines of counting (#6040).', + ).toEqual([]); + }); +}); diff --git a/packages/plugins/plugin-auth/tsup.config.ts b/packages/plugins/plugin-auth/tsup.config.ts new file mode 100644 index 0000000000..ee7f3ee9e0 --- /dev/null +++ b/packages/plugins/plugin-auth/tsup.config.ts @@ -0,0 +1,45 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { defineConfig } from 'tsup'; + +/** + * Two entries, deliberately (#6040). + * + * `src/index.ts` is the full plugin: `export *` over ~20 modules, several of + * which take a **value** import on the better-auth family + * (`objectql-adapter.ts` → `better-auth/adapters`, `backfill-account-issuer.ts` + * → `@better-auth/core/db`). Loading the root therefore eagerly evaluates + * `better-auth` + `@better-auth/{core,oauth-provider,scim,sso}` + `jose` + + * `@noble/hashes` + `@objectstack/rest` + `@objectstack/platform-objects`. + * + * `src/rate-limit-storage.ts` is the ~90-line fixed-window counter + * (`incrementFixedWindow` / `createLazyCounterStore` / `InProcessCounterStore`, + * ADR-0069 D2). It is the repo's ONE fixed-window counter and #4790 asks later + * arrivals to reuse it rather than write a third copy — which they did, from + * outside auth: `@objectstack/runtime` (`security/inbound-rate-limit.ts`, + * `endpoint-policy.ts`, `dispatcher-plugin.ts`) and `@objectstack/service-sms` + * (`sms-plugin.ts`, `sms-daily-quota.ts`). With only a `"."` export those + * consumers had to reach the counter through the root and took the whole + * better-auth family with it. The `./rate-limit-storage` subpath is the entry + * they import instead. + * + * `splitting: false` is what makes the isolation real rather than nominal: each + * entry is emitted as a self-contained bundle, so nothing the root pulls in can + * reach the counter entry through a shared chunk. + * `rate-limit-storage-isolation.test.ts` pins the source-level half of the same + * invariant. + * + * (Identical shape to `packages/types/tsup.config.ts` and + * `packages/core/tsup.config.ts`, which ship `./node` and `./logger` this way. + * The only reason this file exists at all — rather than the shared + * `../../../tsup.config.ts` — is the second entry.) + */ +export default defineConfig({ + entry: ['src/index.ts', 'src/rate-limit-storage.ts'], + splitting: false, + sourcemap: true, + clean: true, + dts: !process.env.OS_SKIP_DTS, + format: ['esm', 'cjs'], + target: 'es2020', +}); diff --git a/packages/runtime/src/api-endpoint-step.test.ts b/packages/runtime/src/api-endpoint-step.test.ts index 06d9ad3d88..4b3b171952 100644 --- a/packages/runtime/src/api-endpoint-step.test.ts +++ b/packages/runtime/src/api-endpoint-step.test.ts @@ -20,7 +20,7 @@ import { describe, it, expect } from 'vitest'; import { ApiEndpointSchema, type ApiEndpoint } from '@objectstack/spec/api'; import type { ApiEndpointMatch } from '@objectstack/spec/contracts'; -import type { CounterStore } from '@objectstack/plugin-auth'; +import type { CounterStore } from '@objectstack/plugin-auth/rate-limit-storage'; import { APP_ENDPOINT_SEGMENT, diff --git a/packages/runtime/src/dispatcher-plugin.ts b/packages/runtime/src/dispatcher-plugin.ts index 9085c1f08c..d68830db31 100644 --- a/packages/runtime/src/dispatcher-plugin.ts +++ b/packages/runtime/src/dispatcher-plugin.ts @@ -4,7 +4,7 @@ import { Plugin, PluginContext, IHttpServer } from '@objectstack/core'; import { looksLikeInternalErrorLeak, declaresServerFault, INTERNAL_ERROR_MESSAGE } from '@objectstack/types'; import { DispatcherErrorCode } from '@objectstack/spec/api'; import type { IAuthService, IMetadataService } from '@objectstack/spec/contracts'; -import type { CounterStore } from '@objectstack/plugin-auth'; +import type { CounterStore } from '@objectstack/plugin-auth/rate-limit-storage'; import { HttpDispatcher, HttpDispatcherResult, type HttpProtocolContext } from './http-dispatcher.js'; import { isServiceServeable } from './service-serveable.js'; import { validationFailureDetails, VALIDATION_FAILED_STATUS } from './validation-failure.js'; diff --git a/packages/runtime/src/endpoint-policy.test.ts b/packages/runtime/src/endpoint-policy.test.ts index f3bd052a3b..8fe10a071a 100644 --- a/packages/runtime/src/endpoint-policy.test.ts +++ b/packages/runtime/src/endpoint-policy.test.ts @@ -17,7 +17,7 @@ import { describe, it, expect } from 'vitest'; import { ApiEndpointSchema, type ApiEndpoint } from '@objectstack/spec/api'; -import type { CounterStore } from '@objectstack/plugin-auth'; +import type { CounterStore } from '@objectstack/plugin-auth/rate-limit-storage'; import { applyEndpointPolicies, diff --git a/packages/runtime/src/endpoint-policy.ts b/packages/runtime/src/endpoint-policy.ts index 1c25bdb409..55412c7397 100644 --- a/packages/runtime/src/endpoint-policy.ts +++ b/packages/runtime/src/endpoint-policy.ts @@ -64,7 +64,7 @@ import { ANONYMOUS_DENY_STATUS, shouldDenyAnonymous, } from '@objectstack/core'; -import { createLazyCounterStore, type CounterStore } from '@objectstack/plugin-auth'; +import { createLazyCounterStore, type CounterStore } from '@objectstack/plugin-auth/rate-limit-storage'; import type { ApiEndpoint } from '@objectstack/spec/api'; import { apiErrorResponse, type ApiErrorEnvelope } from './error-envelope.js'; diff --git a/packages/runtime/src/security/inbound-rate-limit.test.ts b/packages/runtime/src/security/inbound-rate-limit.test.ts index 014e78aaf3..2dc5d7904e 100644 --- a/packages/runtime/src/security/inbound-rate-limit.test.ts +++ b/packages/runtime/src/security/inbound-rate-limit.test.ts @@ -19,7 +19,7 @@ import { resolveRateLimitKey, SharedTokenBucketLimiter, } from './inbound-rate-limit.js'; -import type { CounterStore } from '@objectstack/plugin-auth'; +import type { CounterStore } from '@objectstack/plugin-auth/rate-limit-storage'; /** An in-test counter store that also records whether it was ever consulted. */ function memoryStore() { diff --git a/packages/runtime/src/security/inbound-rate-limit.ts b/packages/runtime/src/security/inbound-rate-limit.ts index 6fc0564f41..975b538b27 100644 --- a/packages/runtime/src/security/inbound-rate-limit.ts +++ b/packages/runtime/src/security/inbound-rate-limit.ts @@ -36,7 +36,7 @@ */ import type { Middleware, IHttpRequest, IHttpResponse } from '@objectstack/core'; -import { createLazyCounterStore, type CounterStore } from '@objectstack/plugin-auth'; +import { createLazyCounterStore, type CounterStore } from '@objectstack/plugin-auth/rate-limit-storage'; import { buildApiError } from '../error-envelope.js'; import { diff --git a/packages/services/service-sms/src/sms-daily-quota.test.ts b/packages/services/service-sms/src/sms-daily-quota.test.ts index 4d9ab0673f..44b47a89d6 100644 --- a/packages/services/service-sms/src/sms-daily-quota.test.ts +++ b/packages/services/service-sms/src/sms-daily-quota.test.ts @@ -8,7 +8,7 @@ import { secondsUntilNextUtcMidnight, utcDayStamp, } from './sms-daily-quota.js'; -import type { CounterStore } from '@objectstack/plugin-auth'; +import type { CounterStore } from '@objectstack/plugin-auth/rate-limit-storage'; /** A memory counter store with the same tolerance the cache adapters have. */ function memoryStore(): CounterStore & { entries: Map } { diff --git a/packages/services/service-sms/src/sms-daily-quota.ts b/packages/services/service-sms/src/sms-daily-quota.ts index 9313749320..1d2d444e72 100644 --- a/packages/services/service-sms/src/sms-daily-quota.ts +++ b/packages/services/service-sms/src/sms-daily-quota.ts @@ -70,7 +70,7 @@ import { InProcessCounterStore, incrementFixedWindow, type CounterStore, -} from '@objectstack/plugin-auth'; +} from '@objectstack/plugin-auth/rate-limit-storage'; /** * The error code a quota-refused send answers with, as the `CODE: message` diff --git a/packages/services/service-sms/src/sms-plugin.ts b/packages/services/service-sms/src/sms-plugin.ts index f4e8513be9..498a07313d 100644 --- a/packages/services/service-sms/src/sms-plugin.ts +++ b/packages/services/service-sms/src/sms-plugin.ts @@ -2,7 +2,7 @@ import type { Plugin, PluginContext } from '@objectstack/core'; import type { ISmsTransport } from '@objectstack/spec/contracts'; -import { createLazyCounterStore, type CounterStore } from '@objectstack/plugin-auth'; +import { createLazyCounterStore, type CounterStore } from '@objectstack/plugin-auth/rate-limit-storage'; import { SmsService, LogSmsTransport, maskPhoneNumber, normalizeSmsRecipient } from './sms-service.js'; import { SmsDailyQuota } from './sms-daily-quota.js'; import { makeSmsTransport, type SmsProviderTag } from './transports/index.js';