nucleus-core-ts 0.9.714 → 0.9.716

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.
@@ -0,0 +1,25 @@
1
+ import type { DeviceInfo } from 'src/Services/Auth/SessionStore/types';
2
+ import { type ClientHints } from './utils';
3
+ export type { ClientHints } from './utils';
4
+ /**
5
+ * Extract IP + User-Agent + Client-Hints from a request, using the same header precedence as
6
+ * password login. Any auth path that has the `Request` can produce a consistent device context.
7
+ */
8
+ export declare function deviceContextFromRequest(request: Request): {
9
+ ipAddress: string;
10
+ userAgent: string;
11
+ clientHints: ClientHints;
12
+ };
13
+ /**
14
+ * Idempotent device-info normalizer — the single place device fields are guaranteed.
15
+ *
16
+ * If the caller already resolved browser + OS + deviceName (password login, OAuth, the
17
+ * deliberate impersonation "Admin" labels) the object is returned unchanged. Otherwise the
18
+ * User-Agent is parsed (with optional Client-Hints) and merged, and a non-null `deviceName`
19
+ * is always produced. This is what fixes "Unknown on Unknown": paths that pass only a raw
20
+ * `{ ipAddress, userAgent }` (register / webauthn / magic-link) now get the same parsing as
21
+ * login — and because `saveSessionToDb` calls this, EVERY path (incl. any future one) is covered.
22
+ */
23
+ export declare function ensureDeviceInfo(info: Partial<DeviceInfo> & {
24
+ ipAddress: string;
25
+ }, clientHints?: ClientHints): DeviceInfo;
@@ -1,6 +1,19 @@
1
1
  export declare function verifyPassword(plainPassword: string, hashedPassword: string): Promise<boolean>;
2
2
  export declare function isValidEmail(email: string): boolean;
3
- export declare function parseUserAgentForLogin(userAgent: string, ipAddress: string, deviceHint?: string): {
3
+ /** Client Hints headers (`Sec-CH-UA*`) used to enrich a reduced/frozen User-Agent. */
4
+ export interface ClientHints {
5
+ uaHeader?: string;
6
+ platformHeader?: string;
7
+ mobileHeader?: string;
8
+ }
9
+ /**
10
+ * Last-resort human label from a UA we couldn't classify — the first meaningful
11
+ * `Product/Version` token (ignoring the ubiquitous `Mozilla/5.0`), else the first alpha
12
+ * word (a bespoke client name). Generic: no product allow-list, works for any consumer.
13
+ * Returns undefined for empty/placeholder UAs so the caller can fall back to "Unknown Device".
14
+ */
15
+ export declare function labelFromRawUserAgent(userAgent: string): string | undefined;
16
+ export declare function parseUserAgentForLogin(userAgent: string, ipAddress: string, deviceHint?: string, clientHints?: ClientHints): {
4
17
  deviceName: string;
5
18
  deviceType: "unknown" | "desktop" | "mobile" | "tablet";
6
19
  browserName: string | undefined;
@@ -29,6 +29,18 @@ export interface AuthorizationRoutesConfig {
29
29
  key: string;
30
30
  value: number;
31
31
  }>>;
32
+ /**
33
+ * Prefix for canonical (metric-keyed) usage counters written by `/consume` and read by a
34
+ * quota that omits `keyTemplate`. Default `'quota'`. Must match the spender's prefix so
35
+ * metering keys and read keys agree across the deployment.
36
+ */
37
+ canonicalPrefix?: string;
38
+ /**
39
+ * Optional allowlist of metric names `/consume` will record. When set, any metric NOT in
40
+ * the list is ignored (bounds Redis key cardinality against arbitrary metric names).
41
+ * Undefined = meter every metric present in the usage payload (default, fully generic).
42
+ */
43
+ allowedMetrics?: string[];
32
44
  }
33
45
  /**
34
46
  * Endpoint-discovery admin + manifest routes.
@@ -9,6 +9,9 @@ export type DeviceInfo = {
9
9
  ipAddress: string;
10
10
  userAgent?: string;
11
11
  deviceHint?: string;
12
+ /** Optional geo enrichment (set by the login path from the IP when available). */
13
+ locationCountry?: string;
14
+ locationCity?: string;
12
15
  };
13
16
  export type SessionRecord = {
14
17
  id: string;
@@ -2,4 +2,4 @@ export { aggregateLimit, evaluateQuota, parseQuotaPolicy } from './evaluate';
2
2
  export { type ResolvedQuota, resolveUserQuotas } from './resolve';
3
3
  export { resolveSuppressedClaims } from './suppress';
4
4
  export type { CounterReader, QuotaAggregate, QuotaContext, QuotaEvaluation, QuotaOp, QuotaPolicy, QuotaWindow, } from './types';
5
- export { bucketDatesForWindow, buildIncrementKeys, buildQuotaKeys } from './windows';
5
+ export { bucketDatesForWindow, buildCanonicalIncrementKeys, buildIncrementKeys, buildQuotaKeys, canonicalKeyTemplate, DEFAULT_CANONICAL_PREFIX, sanitizeMetric, } from './windows';
@@ -29,8 +29,14 @@ export interface QuotaPolicy {
29
29
  * Bucketed windows (rolling / calendar_day) substitute `{date}` (yyyy-mm-dd, UTC) once
30
30
  * per bucket. `total` uses the template as-is (must not contain `{date}`).
31
31
  * e.g. `usage:{tenant}:{userId}:tokens:{date}` or `usage:{tenant}:{userId}:tokens:total`.
32
+ *
33
+ * OPTIONAL: omit it to INHERIT the canonical per-metric template
34
+ * `<prefix>:{tenant}:{userId}:<metric>:{date}` (see canonicalKeyTemplate). Omitting is
35
+ * preferred for new quotas — it makes the keys this quota READS identical to the keys the
36
+ * `/consume` metering endpoint WRITES for the same metric, so a quota assigned mid-window
37
+ * already reflects the usage recorded before it existed.
32
38
  */
33
- keyTemplate: string;
39
+ keyTemplate?: string;
34
40
  /** Comparison for a HARD cap. `lte`: usage may reach the limit; `lt`: must stay below. Default `lte`. */
35
41
  op?: QuotaOp;
36
42
  /** Multi-role limit resolution. Default `max` (tier model: the most generous role wins). */
@@ -51,6 +57,12 @@ export interface QuotaContext {
51
57
  userId: string;
52
58
  tenant?: string;
53
59
  now: Date;
60
+ /**
61
+ * Prefix used to derive the canonical counter template when a policy omits `keyTemplate`.
62
+ * Must match the prefix the spender meters under (default `'quota'`). Set from
63
+ * `authorization.quota.canonicalPrefix` so read keys == metering keys across the deployment.
64
+ */
65
+ canonicalPrefix?: string;
54
66
  }
55
67
  export interface QuotaEvaluation {
56
68
  metric: string;
@@ -1,4 +1,26 @@
1
1
  import type { QuotaContext, QuotaPolicy, QuotaWindow } from './types';
2
+ /** Default prefix for canonical (metric-keyed) usage counters. Project-overridable via config. */
3
+ export declare const DEFAULT_CANONICAL_PREFIX = "quota";
4
+ /** A metric goes into a Redis key segment: strip `:`/whitespace so it can't split the key. */
5
+ export declare function sanitizeMetric(metric: string): string;
6
+ /**
7
+ * The canonical keyTemplate for a metric: `<prefix>:{tenant}:{userId}:<metric>:{date}`.
8
+ * A policy that OMITS `keyTemplate` inherits this at build time, so the keys metering WRITES
9
+ * (buildCanonicalIncrementKeys / buildIncrementKeys) are identical to the keys a quota READS
10
+ * (buildQuotaKeys). `{date}` collapses to `total` for total-windows via the build fns below.
11
+ * This is the load-bearing decoupling: usage is recorded per (tenant,user,metric) regardless
12
+ * of whether any quota claim exists, and a later-assigned quota finds the pre-existing usage.
13
+ */
14
+ export declare function canonicalKeyTemplate(metric: string, prefix?: string): string;
15
+ /**
16
+ * The canonical daily + total metering keys for one metric — INDEPENDENT of any policy.
17
+ * Used by `/consume` to ALWAYS record usage even when the user holds no quota claim. The
18
+ * daily bucket self-expires (~40d, outliving a rolling-30 window); the total key is permanent.
19
+ */
20
+ export declare function buildCanonicalIncrementKeys(metric: string, ctx: QuotaContext, prefix?: string): Array<{
21
+ key: string;
22
+ ttlSeconds?: number;
23
+ }>;
2
24
  /**
3
25
  * The yyyy-mm-dd daily buckets a window spans, most-recent first, or `null` for `total`
4
26
  * (which is a single cumulative counter with no date bucket). Rolling windows include today.
@@ -0,0 +1,28 @@
1
+ /**
2
+ * HTTP-header-safe encoding for lists of possibly-Unicode values (role names,
3
+ * claim actions) carried in the `x-user-roles` / `x-user-claims` headers.
4
+ *
5
+ * HTTP header values must be ISO-8859-1 (latin1). Role names are user-defined and
6
+ * may contain arbitrary Unicode — e.g. a Turkish role "Günlük Kısıtlı Deneme"
7
+ * contains `ı`/`ş`/`ğ` which are outside latin1. Passing such a value to
8
+ * `Headers.set` throws ("Header 'x-user-roles' has invalid value") and 500s EVERY
9
+ * request for any user holding that role — which silently breaks login, entity
10
+ * access, and impersonation enter/exit for that user.
11
+ *
12
+ * Per-item percent-encoding fixes this generically:
13
+ * - ASCII values (`godmin`, `user-free`, `llm.conversations.list`) are left
14
+ * byte-identical, so readers that don't decode still match them correctly.
15
+ * - Unicode values become transport-safe and round-trip losslessly via decode.
16
+ *
17
+ * The comma separator is preserved (encodeURIComponent never emits `,`), so
18
+ * existing `header.split(',')` readers keep working.
19
+ */
20
+ /** Encode a list of role/claim names into a header-safe comma-joined string. */
21
+ export declare function encodeHeaderList(items: string[]): string;
22
+ /**
23
+ * Inverse of {@link encodeHeaderList}: split a header value and restore the
24
+ * original (possibly Unicode) items. Tolerant of legacy/plain values —
25
+ * `decodeURIComponent` is the identity for un-encoded ASCII, and a malformed
26
+ * percent-sequence is returned verbatim rather than throwing.
27
+ */
28
+ export declare function decodeHeaderList(header: string | null | undefined): string[];
@@ -0,0 +1 @@
1
+ export {};
@@ -749,6 +749,25 @@ export interface NucleusConfigOptions {
749
749
  */
750
750
  token?: string;
751
751
  };
752
+ /**
753
+ * Usage-quota metering settings. Metering is decoupled from quota existence: the
754
+ * `/authorization/quotas/consume` endpoint records usage into canonical
755
+ * `<prefix>:{tenant}:{userId}:<metric>:{date|total}` counters for EVERY reported metric,
756
+ * so usage accumulates even when a user holds no quota claim, and a quota assigned later
757
+ * (which omits `keyTemplate`) reads those same counters.
758
+ */
759
+ quota?: {
760
+ /**
761
+ * Prefix for the canonical usage counters. Default `'quota'`. Must match the prefix the
762
+ * spender meters under so read keys and metering keys agree.
763
+ */
764
+ canonicalPrefix?: string;
765
+ /**
766
+ * Optional allowlist of metric names `/consume` will record. When set, metrics not in
767
+ * the list are ignored (bounds Redis key cardinality). Undefined = meter every metric.
768
+ */
769
+ allowedMetrics?: string[];
770
+ };
752
771
  };
753
772
  audit?: {
754
773
  enabled?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nucleus-core-ts",
3
- "version": "0.9.714",
3
+ "version": "0.9.716",
4
4
  "description": "Production-ready, enterprise-grade TypeScript framework for building multi-tenant APIs",
5
5
  "author": "Hidayet Can Özcan <hidayetcan@gmail.com>",
6
6
  "license": "SEE LICENSE IN LICENSE",