@authyon/server 0.2.0-beta.20 → 0.2.0-beta.22

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.
@@ -171,6 +171,43 @@ interface IntrospectResult {
171
171
  token_type?: string;
172
172
  }
173
173
 
174
+ /**
175
+ * Builders for the tenant list's structured filters.
176
+ *
177
+ * The wire form of one filter is `<scope>.<key>:<op>[:<value>]`, repeated once
178
+ * per condition: `?filter=public.kind:in:reseller,partner&filter=custom.cnpj:eq:000`.
179
+ * Repeating the parameter ANDs the conditions and the comma inside `in` is the
180
+ * OR, which is the whole reason to filter server-side — the conditions are part
181
+ * of the SQL statement, so `total`, `pages` and `hasNext` describe the filtered
182
+ * set. A filter applied to a page after it arrives cannot do that.
183
+ *
184
+ * Scopes map to the three JSON documents a tenant carries: `public` →
185
+ * `publicMetadata`, `private` → `privateMetadata`, `custom` → `customFields`
186
+ * (the one the free-text `search` never covered).
187
+ *
188
+ * The rules below mirror the server's parser, so a typo raises `RangeError`
189
+ * here instead of coming back as a 400 `tenant.filter.invalid`.
190
+ */
191
+ /** One `filter` parameter, ready to send. */
192
+ type TenantFilterExpression = string;
193
+ /** Values the filters accept; numbers and booleans are compared as text, the
194
+ * way `jsonb ->> key` returns them. */
195
+ type TenantFilterValue = string | number | boolean;
196
+ declare const TENANT_FILTER_MAX_VALUES = 25;
197
+ declare const TENANT_FILTER_MAX_VALUE_LENGTH = 200;
198
+ declare const TENANT_FILTER_MAX_DEPTH = 3;
199
+ declare const tenantFilter: {
200
+ /** `public.kind:eq:reseller` — the key equals this value. */
201
+ eq(path: string, value: TenantFilterValue): TenantFilterExpression;
202
+ /** `public.kind:in:reseller,partner` — the key equals any of these (SQL IN). */
203
+ in(path: string, values: readonly TenantFilterValue[]): TenantFilterExpression;
204
+ /** `public.notes:like:nacional` — the key contains this text, case-insensitive.
205
+ * The server escapes the LIKE metacharacters, so `100%` means `100%`. */
206
+ like(path: string, text: string): TenantFilterExpression;
207
+ /** `custom.cnpj:exists` — the key is present, whatever its value. */
208
+ exists(path: string): TenantFilterExpression;
209
+ };
210
+
174
211
  /** GET/POST /env/tenants — confirmed against the live API. */
175
212
  interface Organization {
176
213
  id: string;
@@ -189,6 +226,55 @@ interface Organization {
189
226
  publicMetadata?: string;
190
227
  /** JSON-encoded metadata available only through administrative APIs. */
191
228
  privateMetadata?: string;
229
+ /**
230
+ * The tenant this one sits under, or `null` for a root tenant. With `depth`
231
+ * it is what lets a flat page be rendered as the tree it describes.
232
+ *
233
+ * Absent on an API older than 2026-10-31, where every tenant was a root.
234
+ */
235
+ parentTenantId?: string | null;
236
+ /** Distance from the root: `0` for a root tenant. */
237
+ depth?: number;
238
+ /**
239
+ * Direct sub-tenants. A tree view expands only where this is above zero,
240
+ * instead of fetching every node to find out it has no children.
241
+ */
242
+ childCount?: number;
243
+ /** Custom roles defined inside the tenant. Only `get()` returns it. */
244
+ roleCount?: number;
245
+ /**
246
+ * The chain above this tenant, root first, excluding itself — a breadcrumb
247
+ * without a request per level. Empty for a root tenant, and only `get()`
248
+ * returns it.
249
+ */
250
+ ancestors?: TenantAncestor[];
251
+ }
252
+ /** One step of the chain above a tenant, as `Organization.ancestors` carries it. */
253
+ interface TenantAncestor {
254
+ id: string;
255
+ name: string;
256
+ slug: string;
257
+ isDisabled: boolean;
258
+ }
259
+ /**
260
+ * What `environment.tenants.list()` accepts. `search` is the free-text half
261
+ * (one substring, ORed across name, slug, description and both metadata
262
+ * documents); `filter` is the structured half, ANDed, one entry per condition
263
+ * — see `tenantFilter` for the builders and the wire format.
264
+ */
265
+ interface TenantListOptions extends PaginationOptions {
266
+ search?: string;
267
+ filter?: TenantFilterExpression[];
268
+ /**
269
+ * Narrows the page to one level of the tree: a tenant id lists that tenant's
270
+ * direct sub-tenants, and `null` lists the roots. Omitted lists the
271
+ * environment's tenants regardless of depth — the behaviour from before the
272
+ * hierarchy existed, and still the right one for a flat picker.
273
+ *
274
+ * `search` and `filter` keep applying inside the level, so the envelope goes
275
+ * on describing what you asked for.
276
+ */
277
+ parentId?: string | null;
192
278
  }
193
279
  /** One of a user's tenant memberships, as embedded in `EnvironmentUser.tenantMemberships`. */
194
280
  interface TenantMembership {
@@ -336,6 +422,17 @@ interface CreateOrganizationInput {
336
422
  customFields?: Record<string, unknown>;
337
423
  publicMetadata?: Record<string, unknown>;
338
424
  privateMetadata?: Record<string, unknown>;
425
+ /**
426
+ * Creates the tenant underneath an existing one in the same environment.
427
+ * Omitted creates a root tenant.
428
+ *
429
+ * The environment's `maxTenantDepth` has to allow the level — it is `1` by
430
+ * default, which means roots only and refuses this with
431
+ * `tenant.depth.not_allowed` until an operator raises it. The new node counts
432
+ * against the plan's tenant cap like any other, so nesting cannot be used to
433
+ * get around it.
434
+ */
435
+ parentTenantId?: string;
339
436
  }
340
437
  interface UpdateOrganizationInput {
341
438
  name: string;
@@ -371,6 +468,12 @@ interface AuditEvent {
371
468
  environmentId?: string;
372
469
  actorUserId?: string | null;
373
470
  actorType?: string;
471
+ /**
472
+ * The address the event is recorded against — the vouched one when a machine
473
+ * credential named it. Where that differs from the socket peer, the peer is
474
+ * kept inside `payloadJson` as `peerIp`: which backend made the call is its
475
+ * own question, and the answer is not the caller's to choose.
476
+ */
374
477
  actorIp?: string;
375
478
  userAgent?: string;
376
479
  tenantId?: string | null;
@@ -465,8 +568,17 @@ interface AuthyonServerClientOptions {
465
568
  clientId?: string;
466
569
  clientSecret?: string;
467
570
  /**
468
- * Originating end-user IP to forward on every API call as `X-Forwarded-For`.
469
- * Only set this from infrastructure you trust; the SDK rejects comma-separated
571
+ * The end user's address, forwarded on every API call as
572
+ * `X-Authyon-Client-Ip` (and `X-Forwarded-For`, for your own proxies).
573
+ *
574
+ * Worth setting: a call from your backend has no browser in it, so without
575
+ * this every audit row Authyon writes for your integration names the
576
+ * container gateway, which identifies nobody. The API believes the header
577
+ * from a machine credential and records the address beside the event, where
578
+ * the console's IP filter can find it. It only ever changes the trail — no
579
+ * IP-keyed gate reads a vouched address.
580
+ *
581
+ * Only set it from infrastructure you trust; the SDK rejects comma-separated
470
582
  * or whitespace-containing values to avoid forwarded-header injection.
471
583
  */
472
584
  clientIp?: string;
@@ -787,10 +899,58 @@ interface ClientRequestContext {
787
899
  /** Real end-user IP, resolved only from infrastructure trusted by the application. */
788
900
  clientIp?: string;
789
901
  }
902
+ /**
903
+ * The header Authyon reads the real caller address from. `X-Forwarded-For` is
904
+ * not it: the API rewrites the socket peer from the edge proxy's chain and then
905
+ * resolves the client address from this header alone, so an address sent only
906
+ * in XFF is dropped before anything reads it.
907
+ */
908
+ declare const CLIENT_IP_HEADER = "X-Authyon-Client-Ip";
790
909
  /** Rejects header injection and values that are not a single IPv4 or IPv6 address. */
791
910
  declare function normalizeClientIp(value: string): string;
792
911
 
793
912
  type SafeJwtAlgorithm = "RS256" | "PS256" | "ES256" | "EdDSA";
913
+ /**
914
+ * The active tenant, as the `tenant` claim carries it. Present only on a
915
+ * session scoped to one — a token minted before any tenant switch has no
916
+ * tenant and no claim.
917
+ *
918
+ * Snake_case because these are wire names, not SDK ones: the claim is signed,
919
+ * and renaming its fields on the way in would hide what was actually asserted.
920
+ */
921
+ interface AuthyonTenantClaim {
922
+ id: string;
923
+ slug: string;
924
+ name: string;
925
+ /** The tenant above this one, or `null` at a root. */
926
+ parent_id?: string | null;
927
+ /**
928
+ * The materialised chain from the root down to this tenant. Signed, so a
929
+ * consumer can answer "is that tenant under mine?" with a prefix comparison
930
+ * and no round trip — which is the whole reason it is in the token.
931
+ */
932
+ path?: string;
933
+ /** Distance from the root: `0` at a root tenant. */
934
+ depth?: number;
935
+ /**
936
+ * Version of `path`. A tenant that gets moved bumps it, which is what makes
937
+ * the signed path safe to trust: a token minted before the move no longer
938
+ * matches and the API refuses it with `tenant.path_stale`, instead of
939
+ * letting a stale prefix keep answering yes.
940
+ */
941
+ pv?: number;
942
+ roles?: string[];
943
+ /** Effective permissions AT this tenant. */
944
+ permissions?: string[];
945
+ /**
946
+ * Set only when the session descended into this tenant from an ancestor,
947
+ * naming the node the permissions came from — the user holds no membership
948
+ * here. Treat it as borrowed authority if that matters to you (a banner, a
949
+ * tighter trail, a refusal); ignore it and you have a session scoped to a
950
+ * tenant, which is what it is. Absent on any other session.
951
+ */
952
+ via?: string | null;
953
+ }
794
954
  interface AuthyonJwtClaims extends JWTPayload {
795
955
  sub: string;
796
956
  iss: string;
@@ -802,6 +962,8 @@ interface AuthyonJwtClaims extends JWTPayload {
802
962
  roles?: string[];
803
963
  permissions?: string[];
804
964
  scope?: string;
965
+ /** The active tenant and the grants held inside it. Absent until a switch. */
966
+ tenant?: AuthyonTenantClaim;
805
967
  }
806
968
  interface VerifiedAccessToken {
807
969
  claims: AuthyonJwtClaims;
@@ -984,23 +1146,83 @@ declare class AuthyonServerClient {
984
1146
  * is found by the number, and `cnpj` alone finds every tenant carrying
985
1147
  * that key. `take` defaults to 25 and is capped at 100.
986
1148
  *
1149
+ * `filter` is the structured half, and the one to reach for when more
1150
+ * than one condition has to hold: each entry is a condition on a JSON
1151
+ * document, they are ANDed with each other and with `search`, and they
1152
+ * run inside the statement — so the envelope keeps describing the
1153
+ * filtered set and you can page through it. Build them with
1154
+ * `tenantFilter`:
1155
+ *
1156
+ * ```ts
1157
+ * const page = await authyon.environment.tenants.list({
1158
+ * filter: [
1159
+ * tenantFilter.in("public.kind", ["reseller", "partner"]),
1160
+ * tenantFilter.in("custom.cnpj", [a, b]),
1161
+ * ],
1162
+ * take: 100,
1163
+ * });
1164
+ * ```
1165
+ *
1166
+ * `parentId` narrows the page to one level of the tree — `children()`
1167
+ * and `roots()` below are the same call with that one option set.
1168
+ *
987
1169
  * Against an Authyon API older than 2026-10-21 this route ignored
988
- * skip/take/search and answered with a bare array.
1170
+ * skip/take/search and answered with a bare array; one older than
1171
+ * 2026-10-22 ignores `filter` and one older than 2026-10-31 ignores
1172
+ * `parentId`, which read as a page that is too wide rather than an error.
1173
+ */
1174
+ list: (params?: TenantListOptions) => Promise<Paged<Organization>>;
1175
+ /**
1176
+ * One page of a tenant's direct sub-tenants — `list()` with `parentId`.
1177
+ *
1178
+ * Only one level: a node's own children carry `childCount`, so a tree
1179
+ * view asks again where that is above zero instead of reading the whole
1180
+ * subtree to draw the first level of it.
1181
+ */
1182
+ children: (parentTenantId: string, params?: Omit<TenantListOptions, "parentId">) => Promise<Paged<Organization>>;
1183
+ /**
1184
+ * One page of the environment's root tenants — `list()` with no parent.
1185
+ *
1186
+ * This is the first level of the tree, and the difference from `list()`
1187
+ * is worth keeping straight: `list()` returns every tenant at every
1188
+ * depth, which is what a flat picker wants and what a tree view must not
1189
+ * start from.
1190
+ */
1191
+ roots: (params?: Omit<TenantListOptions, "parentId">) => Promise<Paged<Organization>>;
1192
+ /**
1193
+ * POST /env/tenants — create a tenant in the environment.
1194
+ *
1195
+ * `parentTenantId` nests it under an existing tenant. The environment's
1196
+ * `maxTenantDepth` has to allow the level — it ships as `1`, roots only,
1197
+ * so this answers `tenant.depth.not_allowed` until an operator raises it
1198
+ * in the console.
989
1199
  */
990
- list: (params?: {
991
- search?: string;
992
- } & PaginationOptions) => Promise<Paged<Organization>>;
993
- /** POST /env/tenants — create a tenant in the environment. */
994
1200
  create: (input: CreateOrganizationInput) => Promise<Organization>;
995
- /** GET /env/tenants/{tenantId} — fetch a tenant by id. */
1201
+ /**
1202
+ * GET /env/tenants/{tenantId} — fetch a tenant by id.
1203
+ *
1204
+ * Returns `ancestors` as well, the chain above it root first, so a
1205
+ * breadcrumb costs this one call rather than one per level.
1206
+ */
996
1207
  get: (tenantId: string) => Promise<Organization>;
997
1208
  /** PUT /env/tenants/{tenantId} — update a tenant. */
998
1209
  update: (tenantId: string, input: UpdateOrganizationInput) => Promise<Organization>;
999
1210
  /** PUT /env/tenants/{tenantId}/metadata - replace public and private metadata. */
1000
1211
  updateMetadata: (tenantId: string, input: Pick<CreateOrganizationInput, "publicMetadata" | "privateMetadata">) => Promise<void>;
1001
- /** POST /env/tenants/{tenantId}/disable — disable without removing tenant data. */
1212
+ /**
1213
+ * POST /env/tenants/{tenantId}/disable — disable without removing tenant
1214
+ * data.
1215
+ *
1216
+ * Disables the branch below it too. A sub-tenant left live under a
1217
+ * disabled parent would go on authenticating its users, which is the one
1218
+ * thing switching a tenant off is there to stop — so check `childCount`
1219
+ * before calling this on a node with children.
1220
+ */
1002
1221
  disable: (tenantId: string) => Promise<void>;
1003
- /** DELETE /env/tenants/{tenantId} — logically delete (disable) a tenant. */
1222
+ /**
1223
+ * DELETE /env/tenants/{tenantId} — logically delete (disable) a tenant,
1224
+ * and the branch below it. Same handler as `disable()`.
1225
+ */
1004
1226
  delete: (tenantId: string) => Promise<void>;
1005
1227
  members: {
1006
1228
  /**
@@ -1229,4 +1451,4 @@ declare class TenantScopedClient {
1229
1451
  /** Convenience factory: `const authyon = createClient({ clientId, clientSecret })`. */
1230
1452
  declare function createClient(options?: AuthyonServerClientOptions): AuthyonServerClient;
1231
1453
 
1232
- export { type RoleGrant as $, AuthyonServerClient as A, type HttpLogEvent as B, type ClientCredentials as C, type HttpLogger as D, type EnvironmentUser as E, FetchHttpAdapter as F, type InviteTenantMemberInput as G, type HttpAdapter as H, type IntrospectResult as I, type IssuePasswordResetInput as J, type JsonWebKeySet as K, JwksTokenVerifier as L, type JwksTokenVerifierOptions as M, LoggingHttpAdapter as N, type LoginActivity as O, type OpenIdConfiguration as P, type Organization as Q, type OtpCheckResult as R, type Paged as S, type PaginationOptions as T, type PasswordResetCompleted as U, type PasswordResetIssued as V, type PasswordResetPreview as W, type Permission as X, type PermissionsByRole as Y, type ReservedPermissions as Z, type Role as _, type HttpLoggerOptions as a, type RoleInput as a0, type RoleList as a1, type RolePermissions as a2, type SafeJwtAlgorithm as a3, type SecurityMethods as a4, type SubjectApprovals as a5, type SubjectSecurity as a6, type TenantClientValidationResult as a7, type TenantCredentialIssued as a8, type TenantCredentialSummary as a9, type TenantInvite as aa, type TenantInviteAccepted as ab, type TenantInviteInput as ac, type TenantInviteIssued as ad, type TenantInvitePreview as ae, type TenantInviteStatus as af, type TenantMember as ag, type TenantMemberPage as ah, type TenantMembership as ai, TenantScopedClient as aj, type TokenResult as ak, TokenVerificationError as al, type TokenVerificationErrorCode as am, type UpdateOrganizationInput as an, type User as ao, UserScopedClient as ap, type ValidateResult as aq, type VerifiedAccessToken as ar, createClient as as, normalizeClientIp as at, type AcceptTenantInviteInput as b, type AccessTokenSource as c, type Approval as d, type ApprovalAssurance as e, type ApprovalPayload as f, type ApprovalStatus as g, type ApprovalWebAuthnOptions as h, type AuditEvent as i, type AuthyonJwksDiscoveryOptions as j, type AuthyonJwtClaims as k, type AuthyonServerClientOptions as l, type ClientRequestContext as m, type ConfirmApprovalInput as n, type ConsumedApproval as o, type CreateApprovalInput as p, type CreateCredentialInput as q, type CreateOrganizationInput as r, type CreatePermissionInput as s, type CreateTenantCredentialInput as t, type CreateUserInput as u, type CredentialCreator as v, type CredentialDetail as w, type CredentialListOptions as x, type CredentialPermissionsInput as y, type HttpAdapterRequest as z };
1454
+ export { type ReservedPermissions as $, AuthyonServerClient as A, type CredentialPermissionsInput as B, CLIENT_IP_HEADER as C, type HttpAdapterRequest as D, type EnvironmentUser as E, FetchHttpAdapter as F, type HttpLogEvent as G, type HttpAdapter as H, type HttpLogger as I, JwksTokenVerifier as J, type IntrospectResult as K, type InviteTenantMemberInput as L, type IssuePasswordResetInput as M, type JsonWebKeySet as N, type JwksTokenVerifierOptions as O, LoggingHttpAdapter as P, type LoginActivity as Q, type OpenIdConfiguration as R, type Organization as S, type OtpCheckResult as T, type Paged as U, type PaginationOptions as V, type PasswordResetCompleted as W, type PasswordResetIssued as X, type PasswordResetPreview as Y, type Permission as Z, type PermissionsByRole as _, type HttpLoggerOptions as a, type Role as a0, type RoleGrant as a1, type RoleInput as a2, type RoleList as a3, type RolePermissions as a4, type SafeJwtAlgorithm as a5, type SecurityMethods as a6, type SubjectApprovals as a7, type SubjectSecurity as a8, TENANT_FILTER_MAX_DEPTH as a9, type VerifiedAccessToken as aA, createClient as aB, normalizeClientIp as aC, tenantFilter as aD, TENANT_FILTER_MAX_VALUES as aa, TENANT_FILTER_MAX_VALUE_LENGTH as ab, type TenantAncestor as ac, type TenantClientValidationResult as ad, type TenantCredentialIssued as ae, type TenantCredentialSummary as af, type TenantFilterExpression as ag, type TenantFilterValue as ah, type TenantInvite as ai, type TenantInviteAccepted as aj, type TenantInviteInput as ak, type TenantInviteIssued as al, type TenantInvitePreview as am, type TenantInviteStatus as an, type TenantListOptions as ao, type TenantMember as ap, type TenantMemberPage as aq, type TenantMembership as ar, TenantScopedClient as as, type TokenResult as at, TokenVerificationError as au, type TokenVerificationErrorCode as av, type UpdateOrganizationInput as aw, type User as ax, UserScopedClient as ay, type ValidateResult as az, type AcceptTenantInviteInput as b, type AccessTokenSource as c, type Approval as d, type ApprovalAssurance as e, type ApprovalPayload as f, type ApprovalStatus as g, type ApprovalWebAuthnOptions as h, type AuditEvent as i, type AuthyonJwksDiscoveryOptions as j, type AuthyonJwtClaims as k, type AuthyonServerClientOptions as l, type AuthyonTenantClaim as m, type ClientCredentials as n, type ClientRequestContext as o, type ConfirmApprovalInput as p, type ConsumedApproval as q, type CreateApprovalInput as r, type CreateCredentialInput as s, type CreateOrganizationInput as t, type CreatePermissionInput as u, type CreateTenantCredentialInput as v, type CreateUserInput as w, type CredentialCreator as x, type CredentialDetail as y, type CredentialListOptions as z };
package/dist/bff.cjs CHANGED
@@ -746,7 +746,14 @@ function appendQuery(path, params) {
746
746
  if (!params) return path;
747
747
  const query = new URLSearchParams();
748
748
  for (const [key, value2] of Object.entries(params)) {
749
- if (value2 !== void 0) query.set(key, String(value2));
749
+ if (value2 === void 0) continue;
750
+ if (Array.isArray(value2)) {
751
+ for (const item of value2) {
752
+ if (item !== void 0) query.append(key, String(item));
753
+ }
754
+ continue;
755
+ }
756
+ query.set(key, String(value2));
750
757
  }
751
758
  const value = query.toString();
752
759
  return value ? `${path}${path.includes("?") ? "&" : "?"}${value}` : path;
package/dist/bff.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { H as HttpAdapter, a as HttpLoggerOptions, A as AuthyonServerClient } from './authyonServerClient-UA7HdjtD.cjs';
1
+ import { H as HttpAdapter, a as HttpLoggerOptions, A as AuthyonServerClient } from './authyonServerClient-CW46xIRD.cjs';
2
2
  import 'jose';
3
3
 
4
4
  interface BffLoginInput {
package/dist/bff.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { H as HttpAdapter, a as HttpLoggerOptions, A as AuthyonServerClient } from './authyonServerClient-UA7HdjtD.js';
1
+ import { H as HttpAdapter, a as HttpLoggerOptions, A as AuthyonServerClient } from './authyonServerClient-CW46xIRD.js';
2
2
  import 'jose';
3
3
 
4
4
  interface BffLoginInput {
package/dist/bff.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  DEFAULT_BASE_URL,
4
4
  JsonHttpClient,
5
5
  createSharedTransport
6
- } from "./chunk-TDJNBD3Z.js";
6
+ } from "./chunk-SUL2HLRR.js";
7
7
  import "./chunk-77J4NURK.js";
8
8
 
9
9
  // src/bff/contracts.ts
@@ -298,7 +298,14 @@ function appendQuery(path, params) {
298
298
  if (!params) return path;
299
299
  const query = new URLSearchParams();
300
300
  for (const [key, value2] of Object.entries(params)) {
301
- if (value2 !== void 0) query.set(key, String(value2));
301
+ if (value2 === void 0) continue;
302
+ if (Array.isArray(value2)) {
303
+ for (const item of value2) {
304
+ if (item !== void 0) query.append(key, String(item));
305
+ }
306
+ continue;
307
+ }
308
+ query.set(key, String(value2));
302
309
  }
303
310
  const value = query.toString();
304
311
  return value ? `${path}${path.includes("?") ? "&" : "?"}${value}` : path;
@@ -1,7 +1,9 @@
1
1
  // ../../internal/core/http/clientIp.ts
2
+ var CLIENT_IP_HEADER = "X-Authyon-Client-Ip";
2
3
  function clientIpHeaders(context) {
3
4
  if (!context?.clientIp) return {};
4
- return { "X-Forwarded-For": normalizeClientIp(context.clientIp) };
5
+ const ip = normalizeClientIp(context.clientIp);
6
+ return { "X-Forwarded-For": ip, [CLIENT_IP_HEADER]: ip };
5
7
  }
6
8
  function normalizeClientIp(value) {
7
9
  let ip = value.trim();
@@ -222,12 +224,29 @@ function createAuthyonAbility(source = {}, options = {}) {
222
224
  return new AuthyonAbility(createAuthyonRules(source, options), options.detectSubjectType);
223
225
  }
224
226
  function hasPermission(source, requiredPermission) {
225
- const requirement = permissionToRule(requiredPermission);
226
- if (!requirement || typeof requirement.action !== "string" || typeof requirement.subject !== "string") {
227
- return false;
228
- }
227
+ const required = requiredPermission?.trim() ?? "";
228
+ if (!isWellFormedPermission(required)) return false;
229
229
  const permissionSource = isPermissionList(source) ? { permissions: source } : source;
230
- return createAuthyonAbility(permissionSource).can(requirement.action, requirement.subject);
230
+ const granted = [
231
+ ...permissionSource.permissions ?? [],
232
+ ...permissionSource.scope?.split(/\s+/).filter(Boolean) ?? []
233
+ ];
234
+ return granted.some((permission) => grants(permission.trim(), required));
235
+ }
236
+ function grants(granted, required) {
237
+ if (!isWellFormedPermission(granted)) return false;
238
+ if (isFullWildcard(granted)) return true;
239
+ const grantedSegments = granted.split(":");
240
+ const requiredSegments = required.split(":");
241
+ if (grantedSegments.length !== requiredSegments.length) return false;
242
+ return grantedSegments.every(
243
+ (segment, index) => segment === "*" || segment === requiredSegments[index]
244
+ );
245
+ }
246
+ function isWellFormedPermission(permission) {
247
+ if (!permission) return false;
248
+ const segments = permission.split(":");
249
+ return segments.length >= 2 && segments.every((segment) => segment.length > 0);
231
250
  }
232
251
  function hasPermissionGroup(source, group) {
233
252
  const hasAll = (group.allOf ?? []).every((permission) => hasPermission(source, permission));
@@ -242,13 +261,23 @@ function createAuthyonRules(source = {}, options = {}) {
242
261
  ...source.permissions ?? [],
243
262
  ...source.scope?.split(/\s+/).filter(Boolean) ?? []
244
263
  ]);
245
- const rules = [...permissions].map(permissionToRule).filter(isAbilityRule);
264
+ const rules = [...permissions].map(grantToRule).filter(isAbilityRule);
246
265
  for (const role of /* @__PURE__ */ new Set([...source.roles ?? [], ...options.roles ?? []])) {
247
266
  rules.push(...(options.roleRules?.[role] ?? []).map(cloneRule));
248
267
  }
249
268
  rules.push(...(options.rules ?? []).map(cloneRule));
250
269
  return rules;
251
270
  }
271
+ function grantToRule(permission) {
272
+ const normalized = permission.trim();
273
+ if (normalized && isFullWildcard(normalized)) {
274
+ return { action: "manage", subject: "all" };
275
+ }
276
+ return permissionToRule(normalized);
277
+ }
278
+ function isFullWildcard(permission) {
279
+ return permission.split(":").every((segment) => segment === "*");
280
+ }
252
281
  function permissionToRule(permission) {
253
282
  const normalized = permission.trim();
254
283
  if (!normalized) return null;
@@ -467,6 +496,7 @@ function unauthenticated() {
467
496
  }
468
497
 
469
498
  export {
499
+ CLIENT_IP_HEADER,
470
500
  clientIpHeaders,
471
501
  normalizeClientIp,
472
502
  TokenVerificationError,