Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,82 @@
# Token match — `@suaveplan/api-auth-bearer-static`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`findTokenIndex` performs a constant-time lookup of a presented bearer
token against a fixed table, returning the matching entry's index or `-1`
if no entry matches. The function is the security boundary of the package:
every other code path in the resolver assumes the match is timing-safe so
an attacker cannot binary-search the token table by measuring response
latency.
The implementation walks every entry, calling `constantTimeEqual` from
`@suaveplan/crypto-utils` once per row. The match-tracking variable is
written only on the first match; subsequent matches are ignored. Crucially
the loop does not break on first match — exiting early would leak the
position of the matching token via wall-clock timing.
## Features
- Wraps `@suaveplan/crypto-utils` `constantTimeEqual` over UTF-8 byte
views.
- Iterates the full table for every call — no early exit, no short-circuit
on `presented === entry.token`.
- Returns the first matching index or `-1`.
- Handles `undefined` entries (defensive guard for sparse arrays).
## Usage: Basic
```ts
import { findTokenIndex } from "@suaveplan/api-auth-bearer-static/constant-time";
const table = [
{ token: "T1" },
{ token: "T2" },
];
const index = findTokenIndex("T2", table);
// index === 1
```
## Usage: Advanced
```ts
import { findTokenIndex } from "@suaveplan/api-auth-bearer-static/constant-time";
interface TokenEntry {
readonly token: string;
readonly principalId: string;
}
const table: ReadonlyArray<TokenEntry> = [
{ token: "service-cron", principalId: "p1" },
{ token: "service-admin", principalId: "p2" },
];
function lookup(presented: string): TokenEntry | null {
const idx = findTokenIndex(presented, table);
return idx === -1 ? null : (table[idx] ?? null);
}
```
## API Reference
- `findTokenIndex(presented: string, table: ReadonlyArray<{ token: string }>): number`
— returns the index of the first entry whose `token` matches
`presented`, or `-1` if no entry matches.
## Implementation Notes
`constantTimeEqual` from `@suaveplan/crypto-utils` walks every byte of the
two `Uint8Array` views and accumulates differences into an integer via
bitwise OR; the function returns whether the accumulator stayed zero. The
length-mismatch fast path is documented as not secret (token lengths are
not secret in this adapter's threat model — the table is operator-supplied
config). The `TextEncoder` round-trip ensures we compare bytes, not
surrogate-pair code units; otherwise an attacker could compare a multi-
byte presented token against a single-byte entry and learn from the
length-mismatch shortcut whether their candidate had the right length.

View File

@@ -0,0 +1,94 @@
# Error codes — `@suaveplan/api-auth-bearer-static`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The error-codes module defines the stable `AUTH_BEARER_STATIC.*` namespace
that every public failure path in this package surfaces. Codes are produced
by `defineCodes` from `@suaveplan/error` so the registry yields a frozen
record of fully-qualified strings (`AUTH_BEARER_STATIC.NO_TOKEN`,
`AUTH_BEARER_STATIC.UNKNOWN_TOKEN`, `AUTH_BEARER_STATIC.MTLS_REQUIRED`,
`AUTH_BEARER_STATIC.INVALID_OPTIONS`) along with a `BearerStaticAuthError`
subclass that lifts each code into a real `Error` instance.
This module is the contract a caller branches on. Catching a generic
`SuaveplanError` and reading `.code` is the supported failure-handling
pattern; the concrete subclass exists to make `instanceof` narrow the code
union for downstream code that needs the stronger typing.
## Features
- Frozen four-entry code registry keyed by reason name.
- `BearerStaticAuthError` extends `SuaveplanError<BearerStaticAuthErrorCode>`
— `instanceof` narrows `.code`.
- Stable namespace string (`AUTH_BEARER_STATIC`) safe to log and persist.
- Every code exercised by at least one production code path (registry-vs-
source conformance is asserted by the spec-conformance test).
## Usage: Basic
```ts
import {
AUTH_BEARER_STATIC_CODES,
BearerStaticAuthError,
} from "@suaveplan/api-auth-bearer-static/error-codes";
try {
// factory call that may throw on bad options
throw new BearerStaticAuthError(
AUTH_BEARER_STATIC_CODES.INVALID_OPTIONS,
"tokens table is empty"
);
} catch (e) {
if (e instanceof BearerStaticAuthError) {
// e.code narrows to BearerStaticAuthErrorCode
console.error(e.code, e.message);
}
}
```
## Usage: Advanced
```ts
import {
AUTH_BEARER_STATIC_CODES,
BearerStaticAuthError,
} from "@suaveplan/api-auth-bearer-static/error-codes";
function isInvalidOptions(err: unknown): err is BearerStaticAuthError {
return (
err instanceof BearerStaticAuthError &&
err.code === AUTH_BEARER_STATIC_CODES.INVALID_OPTIONS
);
}
// Treat invalid options as a fatal config error; bubble everything else.
try {
// build resolver
} catch (e) {
if (isInvalidOptions(e)) {
process.exit(78); // EX_CONFIG
}
throw e;
}
```
## API Reference
- `AUTH_BEARER_STATIC_CODES` — `Readonly<Record<Reason,
"AUTH_BEARER_STATIC.<Reason>">>`. Frozen at module load.
- `BearerStaticAuthErrorCode` — string-literal union of every value.
- `class BearerStaticAuthError extends SuaveplanError<BearerStaticAuthErrorCode>`
— constructor `(code, messageKey, options?)` matches the
`SuaveplanError` shape.
## Implementation Notes
The registry is constructed with `defineCodes("AUTH_BEARER_STATIC", [...])`.
`defineCodes` returns a `Readonly` record whose key set is the reason names
and whose values are the fully-qualified `"AUTH_BEARER_STATIC.<reason>"`
strings — referenced via `AUTH_BEARER_STATIC_CODES.NO_TOKEN` rather than the
raw string literal so a typo at the call site fails type-check.

View File

@@ -0,0 +1,106 @@
# Direct factory — `@suaveplan/api-auth-bearer-static`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`createBearerStaticResolver` is the direct-surface entry point of the
package. It accepts validated `BearerStaticResolverOptions`, builds a
`SessionResolver` over the fixed token table, and returns a resolver
implementation suitable for placing inside a `SessionResolverChain` from
`@suaveplan/api-auth`. The resolver matches the documented chain
semantics: an unknown token, missing header, or tripped mTLS gate all
return `null` so the chain advances to the next entry.
The resolver is stateless beyond the disposed flag — there is no DB
connection, no JWKS cache, no refresh loop. Disposal flips the flag and
subsequent calls return `null` rather than throwing.
## Features
- Stateless per-request resolution; safe under concurrent fanout.
- Constant-time token match via `findTokenIndex`.
- Optional per-entry mTLS gate via `checkMtlsGate`.
- Configurable `headerName` and prefix (`Bearer` by default).
- Case-insensitive scheme match per RFC 7235.
- OTel counter emission for every of the four outcomes.
## Usage: Basic
```ts
import { createBearerStaticResolver } from "@suaveplan/api-auth-bearer-static";
const resolver = createBearerStaticResolver({
tokens: [
{
token: "service-cron-2026",
principal: {
id: "svc_cron",
scopes: ["jobs:run"],
claims: { kind: "service" },
},
},
],
});
const req = new Request("https://api/x", {
headers: { Authorization: "Bearer service-cron-2026" },
});
const principal = await resolver.resolve(req);
// principal.id === "svc_cron"
```
## Usage: Advanced
```ts
import { createBearerStaticResolver } from "@suaveplan/api-auth-bearer-static";
import { SessionResolverChain } from "@suaveplan/api-auth";
import { createJwtResolver } from "@suaveplan/api-auth-jwt"; // hypothetical sibling
// Chained — service tokens checked first, JWT users fall through after.
const chain = new SessionResolverChain([
createBearerStaticResolver({
tokens: [
{
token: "admin-2026",
principal: {
id: "svc_admin",
scopes: ["admin:*"],
claims: {},
},
mtlsRequired: true,
},
],
headerName: "X-Service-Auth",
prefix: "Token",
}),
createJwtResolver({ issuer: "https://idp/..." }),
]);
const result = await chain.resolve(request);
```
## API Reference
- `createBearerStaticResolver(options: BearerStaticResolverOptions): SessionResolver`
Returned resolver:
- `resolve(request: Request): Promise<Principal | null>`
- `dispose(): Promise<void>` — idempotent.
## Implementation Notes
`extractToken` accepts the header value, trims, splits on the first space,
and lower-cases the scheme for the prefix comparison. Empty credentials
after the prefix are rejected. The implementation reads the configured
header by exact name (the Web `Headers` API normalises case on lookup, so
`Authorization` and `authorization` both work). When the table contains
the same token twice with conflicting principals, `findTokenIndex` returns
the first index — operator misconfiguration the schema cannot prevent.
TypeScript cannot narrow `table[idx]` to non-undefined even though
`findTokenIndex` only ever returns `-1` or a valid in-range index; a type
assertion encodes that contract at the call site without adding a runtime
guard the test would have to exercise.

View File

@@ -0,0 +1,79 @@
# mTLS gate — `@suaveplan/api-auth-bearer-static`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The mTLS gate enforces an optional second factor on individual token table
entries. When operators mark an entry with `mtlsRequired: true`, the
resolver MUST refuse to surface the entry's Principal unless the host
listener attached a verified peer certificate to the inbound request via
`request.tls.peerCertificate`. This guards the highest-privilege service
tokens (e.g. internal admin API) against credential leakage — the bearer
token alone is necessary but no longer sufficient.
The adapter does NOT establish TLS itself. The host listener (Bun's
TLS-enabled server, Node's `https`, a Cloudflare-Worker TLS terminator
ahead of the worker, etc.) is responsible for completing the TLS handshake
and surfacing the verified subject as `request.tls.peerCertificate`. This
module just inspects what was attached.
## Features
- Pure function — no I/O, no state.
- Defensive — accepts any `Request`-shaped input even when the `tls` field
was not attached (returns `undefined`).
- Treats `peerCertificate: null` identically to absent.
- `mtlsRequired === false` short-circuits without inspecting the request.
## Usage: Basic
```ts
import { checkMtlsGate } from "@suaveplan/api-auth-bearer-static/mtls";
const request = new Request("https://example/test");
const allowed = checkMtlsGate(request, true);
// allowed === false — no peerCertificate attached
```
## Usage: Advanced
```ts
import {
checkMtlsGate,
extractPeerCertificate,
} from "@suaveplan/api-auth-bearer-static/mtls";
// Host listener pattern — wrap the inbound request before handing off to
// the auth chain.
const withTls = Object.assign(request, {
tls: { peerCertificate: parsedX509Subject },
});
const cert = extractPeerCertificate(withTls);
// cert === parsedX509Subject
const allowed = checkMtlsGate(withTls, true);
// allowed === true
```
## API Reference
- `interface RequestTlsInfo { peerCertificate?: unknown }`
- `extractPeerCertificate(request: Request): unknown | undefined`
- `checkMtlsGate(request: Request, mtlsRequired: boolean): boolean`
## Implementation Notes
The `Request` Web type has no standardised `tls` field. Both Node's
undici-based `Request` and Bun's polyfill ignore unknown properties on
construction, so the host pattern is to attach `tls` as a plain property
after the Request is built (or to wrap it in a Proxy / subclass). The
extractor reads through that property with a single defensive type cast
and returns `undefined` for any shape that does not satisfy the
`{ peerCertificate: <truthy> }` invariant. The opaque certificate itself
is not parsed here — that is the host's job, and a future
`api-auth-mtls` adapter (out of scope for Stage 4) will own subject
extraction.

View File

@@ -0,0 +1,96 @@
# Options — `@suaveplan/api-auth-bearer-static`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The options module owns the public construction surface of every resolver
in this package. It defines the `BearerStaticResolverOptions` interface
(plus its row type `BearerStaticTokenEntry`), the Zod schema that mirrors
the interface, and a thin parser that turns operator-supplied input into a
validated value or throws `AUTH_BEARER_STATIC.INVALID_OPTIONS`.
Validation runs once at factory construction. Hot-path resolve calls never
re-validate — by then the table is structurally trusted. Token rotation is
intentionally out of scope: operators rotate by reloading host config,
which re-runs the factory and re-runs validation.
## Features
- `tokens` is required and non-empty (`.min(1)`).
- Every entry validates token (non-empty string), `principal` shape (id +
scopes + claims, optional tenantId), and optional `mtlsRequired` flag.
- `headerName` and `prefix` are optional with documented defaults
(`Authorization` / `Bearer`).
- Failure surfaces a typed `BearerStaticAuthError` carrying the original
`ZodError` as `cause` — full diagnostic trail is preserved.
## Usage: Basic
```ts
import { parseBearerStaticResolverOptions } from "@suaveplan/api-auth-bearer-static/options";
const opts = parseBearerStaticResolverOptions({
tokens: [
{
token: "service-cron-2026",
principal: {
id: "svc_cron",
scopes: ["jobs:run"],
claims: { kind: "service" },
},
},
],
});
```
## Usage: Advanced
```ts
import {
parseBearerStaticResolverOptions,
bearerStaticResolverOptionsSchema,
} from "@suaveplan/api-auth-bearer-static/options";
import { AUTH_BEARER_STATIC_CODES } from "@suaveplan/api-auth-bearer-static/error-codes";
// Validate without constructing — useful for config UIs that show errors
// inline before committing changes.
const result = bearerStaticResolverOptionsSchema.safeParse(userInput);
if (!result.success) {
renderInlineErrors(result.error);
}
// Or rely on the throwing parser when integrated into a startup pipeline:
try {
const opts = parseBearerStaticResolverOptions(userInput);
return opts;
} catch (e) {
if (
e &&
typeof e === "object" &&
"code" in e &&
e.code === AUTH_BEARER_STATIC_CODES.INVALID_OPTIONS
) {
crashWithConfigExitCode();
}
throw e;
}
```
## API Reference
- `interface BearerStaticTokenEntry { token: string; principal: Principal; mtlsRequired?: boolean }`
- `interface BearerStaticResolverOptions { tokens: ReadonlyArray<BearerStaticTokenEntry>; headerName?: string; prefix?: string }`
- `bearerStaticResolverOptionsSchema: z.ZodType<BearerStaticResolverOptions>`
- `parseBearerStaticResolverOptions(raw: unknown): BearerStaticResolverOptions`
## Implementation Notes
The schema's `principal` block deliberately accepts strings rather than
`PrincipalId` / `Scope` brands. Brands are erased at runtime and the host
trusts operator-supplied config to be well-formed; rejecting strings that
look fine at runtime would be developer-hostile noise. Downstream code
treats the returned shape as a `Principal` because the structural fields
match.

View File

@@ -0,0 +1,110 @@
# Worker-proxy factory — `@suaveplan/api-auth-bearer-static`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The worker-proxy surface is the second of the two Stage-4 surfaces this
adapter ships (`§5.2a`). It lets the host install the bearer-static
resolver in the main process while the actual token table lives inside an
isolated worker. The host's `SessionResolverChain` calls
`resolve(request)` exactly as for the direct surface; under the hood, the
proxy marshals the request across a `MessagePort` using
`@suaveplan/api-worker-rpc`, the worker's resolver runs, and the
`Principal | null` decision is returned to the host.
The proxy intentionally keeps the token table out of the host's address
space — useful when the host is sandboxed and the worker holds privileged
credentials, or when operators want a single auth daemon serving multiple
host workers.
## Features
- Bidirectional RPC over a `MessagePort` (or any `PortLike`) — no
process / address-space assumptions.
- Idempotent `dispose()` — coalesces multiple calls and closes the
underlying RPC client exactly once.
- Header set, request method, URL, and (optional) `peerCertificate` cross
the boundary; opaque body and abort signal are out of scope (resolution
does not need them).
- Server-side mount via `serveBearerStaticResolverOnPort` — owns the real
resolver and the token table.
- Passes the same `runAuthSessionConformance` harness as the direct
surface.
## Usage: Basic
```ts
import {
createBearerStaticResolverWorkerProxy,
serveBearerStaticResolverOnPort,
} from "@suaveplan/api-auth-bearer-static/proxy";
import { createInProcessPortPair } from "@suaveplan/api-worker-rpc";
const { clientPort, serverPort } = createInProcessPortPair();
// Worker side — owns the real table.
const server = serveBearerStaticResolverOnPort(serverPort, {
tokens: [
{
token: "T1",
principal: { id: "svc", scopes: [], claims: {} },
},
],
});
// Host side — sees only a proxy.
const proxy = createBearerStaticResolverWorkerProxy(clientPort);
const request = new Request("https://api/x", {
headers: { Authorization: "Bearer T1" },
});
const principal = await proxy.resolve(request);
await proxy.dispose();
server.close("done");
```
## Usage: Advanced
```ts
import { Worker, MessageChannel } from "node:worker_threads";
import {
createBearerStaticResolverWorkerProxy,
} from "@suaveplan/api-auth-bearer-static/proxy";
// Real Node worker boundary. The worker script imports
// `serveBearerStaticResolverOnPort` and calls it against its own port.
const { port1, port2 } = new MessageChannel();
const worker = new Worker("./auth-worker.mjs", { transferList: [port2] });
const resolver = createBearerStaticResolverWorkerProxy(port1 as never);
// Place in chain alongside direct adapters — host treats both uniformly.
import { SessionResolverChain } from "@suaveplan/api-auth";
const chain = new SessionResolverChain([resolver]);
```
## API Reference
- `serveBearerStaticResolverOnPort(port: PortLike, options: BearerStaticResolverOptions): { close(reason?: string): void }`
— mount the server-side handlers.
- `createBearerStaticResolverWorkerProxy(port: PortLike): SessionResolver`
— host-side proxy.
## Implementation Notes
The marshalled envelope is `{url, method, headers, peerCertificate?}` —
the minimal subset of `Request` that the bearer-static decision actually
needs. `peerCertificate` is structured-clone-friendly because the host
attaches whatever shape the listener surfaced; tests pass plain objects.
The proxy never sends the request body, because token resolution does not
consult it. Errors thrown by the server-side resolver (e.g. an unexpected
runtime fault) propagate as the standard `WORKER_RPC` error envelope and
surface in the host as a thrown `WorkerRpcError` — the host's resolver
chain treats this as a fail-closed `RESOLVER_THREW` per the documented
`api-auth` chain semantics. `dispose()` calls the remote `dispose` first
to drain the server-side state, then closes the local client so the
underlying port can be garbage-collected.

View File

@@ -0,0 +1,71 @@
# Security scheme contribution — `@suaveplan/api-auth-bearer-static`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module builds the OpenAPI 3.1 `components.securitySchemes`
contribution the adapter publishes to the host's `AuthAdapter` registry.
The generated entry declares an `http` / `bearer` scheme so OpenAPI tooling
(Stoplight, ReadMe, Postman import, etc.) and the host's emitter render
the canonical "Authorize" affordance.
The contribution is content-only: it does NOT register routes, mount
middleware, or affect runtime behaviour. It exists solely so the OpenAPI
document the host emits is correct and discoverable.
## Features
- OAS 3.1 compliant — emits the `http` discriminator with `scheme: bearer`.
- `name` defaults to `bearerStaticAuth` and is overridable at construction.
- Optional `bearerFormat` and `description` fields pass through verbatim.
- Validated against the `OpenApiSecuritySchemeSchema` Zod schema published
by `@suaveplan/api-auth/security-scheme` — if this module ever drifts,
the conformance tests in `api-auth` fail.
## Usage: Basic
```ts
import { securitySchemeContribution } from "@suaveplan/api-auth-bearer-static/security-scheme";
const contrib = securitySchemeContribution();
// { name: "bearerStaticAuth",
// scheme: { type: "http", scheme: "bearer" } }
```
## Usage: Advanced
```ts
import { securitySchemeContribution } from "@suaveplan/api-auth-bearer-static/security-scheme";
import { validateSecurityScheme } from "@suaveplan/api-auth";
// Custom name + bearerFormat for an admin-only deployment that publishes
// two static-token adapters (cron + admin) side by side under distinct
// scheme keys.
const adminScheme = securitySchemeContribution({
name: "bearerStaticAdminAuth",
description: "Static admin token. mTLS-gated. Rotate via host config reload.",
bearerFormat: "opaque",
});
// Defensive — confirm the contribution still passes the api-auth schema.
validateSecurityScheme(adminScheme);
```
## API Reference
- `interface SecuritySchemeContributionOptions { name?: string; description?: string; bearerFormat?: string }`
- `securitySchemeContribution(options?: SecuritySchemeContributionOptions): SecuritySchemeContribution`
— pure factory; the result is structurally a
`SecuritySchemeContribution` from `@suaveplan/api-auth`.
## Implementation Notes
`bearerFormat` is purely informational per OAS — values like `JWT`,
`opaque`, or vendor-specific strings appear in some doc tooling but never
affect runtime validation. The contribution intentionally omits the
optional fields when undefined rather than emitting `undefined` values,
which would round-trip through `JSON.stringify` as missing keys but
trigger noisy diffs in machine-comparison tooling.

View File

@@ -0,0 +1,76 @@
# OTel instruments — `@suaveplan/api-auth-bearer-static`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The telemetry module owns the single OpenTelemetry counter this package
emits — `api.auth.bearer_static_match_total` — partitioned by the
documented `outcome` attribute. Every call to a resolver's `resolve()`
method increments this counter exactly once with one of four label values.
The instrument is bound lazily against `metrics.getMeter(...)` so the
package is safe to import on the cold path before the host installs a
`MeterProvider`. Resetting the cached bundle (via
`resetBearerStaticInstruments`) is required in test setup to bind against
a freshly-installed in-memory exporter.
## Features
- Single counter — `api.auth.bearer_static_match_total`.
- Four `outcome` label values: `match`, `no-token`, `unknown-token`,
`mtls-required`.
- Lazy meter resolution — module load does not trigger an OTel side
effect.
- Idempotent reset hook for test isolation.
## Usage: Basic
```ts
import { recordMatchOutcome } from "@suaveplan/api-auth-bearer-static/src/telemetry";
// Inside a resolver's resolve() body:
recordMatchOutcome("unknown-token");
```
## Usage: Advanced
```ts
import {
getBearerStaticInstruments,
resetBearerStaticInstruments,
} from "@suaveplan/api-auth-bearer-static/src/telemetry";
// In a vitest setup file that has installed an in-memory MeterProvider
// upstream:
beforeEach(() => {
resetBearerStaticInstruments();
});
it("emits the outcome attribute we expect", async () => {
const inst = getBearerStaticInstruments();
inst.matchTotal.add(1, { outcome: "match" });
// ...then flush + assert on the exporter
});
```
## API Reference
- `interface BearerStaticInstruments { matchTotal: Counter }`
- `type BearerStaticMatchOutcome = "match" | "no-token" | "unknown-token" | "mtls-required"`
- `getBearerStaticInstruments(): BearerStaticInstruments`
- `resetBearerStaticInstruments(): void`
- `recordMatchOutcome(outcome: BearerStaticMatchOutcome): void`
## Implementation Notes
The instrument name uses dotted notation per OpenTelemetry semantic
conventions; the attribute key is `outcome` rather than the host-wide
`auth.outcome` since this counter is adapter-scoped and dashboards aggregate
across adapters by metric name, not attribute. The `description` field is
human-readable only — OTel collectors do not interpret it. The cached bundle
exists per process and is intentionally global; resolvers do not own
instruments because every concrete resolver in a chain would otherwise
double-publish the same counter under the same scope.