nucleus-core-ts 0.9.728 → 0.9.730

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,31 @@
1
+ /**
2
+ * Pure, fail-closed authorization decision for the admin "create user" endpoint. Kept out of
3
+ * the route handler so the privilege-escalation guard can be unit-tested in isolation.
4
+ *
5
+ * Two boundaries must hold together:
6
+ * 1. Only a godmin, OR a caller holding one of `allowRoles`, may call the endpoint at all.
7
+ * 2. A caller who is NOT a godmin (admitted purely via `allowRoles`) must NOT be able to
8
+ * assign the godmin role — otherwise a delegated operator (e.g. "support") could mint a
9
+ * full super-admin, bypassing the isRbacTable godmin gate that protects generic
10
+ * user_roles writes. Only a godmin may assign the godmin role.
11
+ */
12
+ export type CreateUserAuthzDecision = {
13
+ allowed: true;
14
+ callerIsGodmin: boolean;
15
+ } | {
16
+ allowed: false;
17
+ status: 403;
18
+ reason: 'not_privileged' | 'godmin_assignment_forbidden';
19
+ };
20
+ export declare function decideCreateUserAuthz(params: {
21
+ /** The caller's DB is_god flag. */
22
+ callerIsGod: boolean;
23
+ /** Role NAMES the caller holds (from a user_roles⋈roles lookup). */
24
+ callerRoleNames: string[];
25
+ /** Role names permitted to call this endpoint (in addition to godmins). */
26
+ allowRoles: string[];
27
+ /** Role NAMES the caller is trying to assign to the new user. */
28
+ requestedRoleNames: string[];
29
+ /** The framework's godmin role name (GODMIN_ROLE_NAME). */
30
+ godminRoleName: string;
31
+ }): CreateUserAuthzDecision;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Pure, fail-closed decision helpers for authorizing `?with=` relation expansions on
3
+ * entity GET routes. Kept out of the route handler so they can be unit-tested in
4
+ * isolation (no DB, no Elysia). See index.ts for how they are wired into the handler.
5
+ */
6
+ /** The subset of an authorization check result the relation-expansion decision needs. */
7
+ export interface RelationAuthResult {
8
+ authorized: boolean;
9
+ scopeFilters?: Record<string, unknown>;
10
+ }
11
+ /** Outcome of the decision: whether to expand, and (when authorized + scoped) with which scope. */
12
+ export type RelationExpansionDecision = {
13
+ expand: false;
14
+ } | {
15
+ expand: true;
16
+ scope?: Record<string, unknown>;
17
+ };
18
+ /**
19
+ * Decide whether a requested `?with=<relation>` may be expanded, and with what ownership
20
+ * scope. Fail-closed by construction:
21
+ *
22
+ * - `!authEnabled` OR the relation is unmapped (`!targetTable`) → expand fully. There is no
23
+ * per-relation gate to apply (auth is off, or we could not resolve the relation's target
24
+ * entity), so this preserves the pre-per-relation-authz behavior.
25
+ *
26
+ * - `relAuth === null` → NO user context. This is the PUBLIC-parent-route case: on a public
27
+ * GET route the auth middleware never sets `x-user-id`, so the per-relation check has no
28
+ * caller to authorize. Expanding a NON-public target here would let a public parent entity
29
+ * transitively leak a DIFFERENT, non-public entity's full rows to an anonymous caller
30
+ * (e.g. `GET /comments/:id?with=author` exposing every `users` column). So expand ONLY when
31
+ * the TARGET entity is itself public-GET; otherwise DENY.
32
+ *
33
+ * - `relAuth.authorized === false` → DENY. The caller has no read claim on the target entity,
34
+ * so the relation must not be expanded.
35
+ *
36
+ * - `relAuth.authorized === true` → expand, carrying the target claim's OWN `self:` scope so a
37
+ * caller can only pull related rows they own (empty/absent scope → unscoped expand).
38
+ */
39
+ export declare function decideRelationExpansion(params: {
40
+ authEnabled: boolean;
41
+ targetTable: string | undefined;
42
+ relAuth: RelationAuthResult | null;
43
+ isTargetPublicGet: boolean;
44
+ }): RelationExpansionDecision;
45
+ /**
46
+ * Resolve a related table's drizzle column for a scope key, accepting snake_case or camelCase,
47
+ * using OWN-property lookups ONLY. A raw `fields[key]` reaches through the prototype chain: a
48
+ * stale/typo'd/malicious scope key that happens to be an `Object.prototype` member name
49
+ * (`constructor`, `toString`, `hasOwnProperty`, `__proto__`, …) would return a truthy INHERITED
50
+ * value, making the nested where-clause treat it as a real column and silently DROP the
51
+ * fail-closed `1 = 0` guard for an unknown scope column. Own-property lookup makes such keys
52
+ * resolve to `undefined`, so the guard fires. Mirrors `resolveCol` in the route handler.
53
+ */
54
+ export declare function resolveRelationScopeColumn(fields: Record<string, unknown>, key: string, toCamel: (s: string) => string): unknown | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nucleus-core-ts",
3
- "version": "0.9.728",
3
+ "version": "0.9.730",
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",