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.
- package/dist/index.js +8 -8
- package/dist/src/ElysiaPlugin/routes/auth/login/deviceInfo.d.ts +25 -0
- package/dist/src/ElysiaPlugin/routes/auth/login/deviceInfo.test.d.ts +1 -0
- package/dist/src/ElysiaPlugin/routes/auth/login/utils.d.ts +14 -1
- package/dist/src/ElysiaPlugin/routes/authorization/consume.test.d.ts +1 -0
- package/dist/src/ElysiaPlugin/routes/authorization/index.d.ts +12 -0
- package/dist/src/Services/Auth/SessionStore/types.d.ts +3 -0
- package/dist/src/Services/Authorization/Quota/canonical.test.d.ts +1 -0
- package/dist/src/Services/Authorization/Quota/index.d.ts +1 -1
- package/dist/src/Services/Authorization/Quota/types.d.ts +13 -1
- package/dist/src/Services/Authorization/Quota/windows.d.ts +22 -0
- package/dist/src/Utils/headerList.d.ts +28 -0
- package/dist/src/Utils/headerList.test.d.ts +1 -0
- package/dist/src/types.d.ts +19 -0
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
-
|
|
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;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
|
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 {};
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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.
|
|
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",
|