@authyon/auth 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.
- package/README.md +371 -362
- package/dist/{ability-BTcayvkM.d.cts → ability-DjEMCVjx.d.cts} +96 -8
- package/dist/{ability-BTcayvkM.d.ts → ability-DjEMCVjx.d.ts} +96 -8
- package/dist/{chunk-3XJY6BCI.js → chunk-IX6U6A72.js} +33 -6
- package/dist/index.cjs +125 -12
- package/dist/index.d.cts +53 -3
- package/dist/index.d.ts +53 -3
- package/dist/index.js +91 -7
- package/dist/react/index.cjs +11 -1
- package/dist/react/index.d.cts +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +1 -1
- package/package.json +71 -71
|
@@ -212,6 +212,17 @@ interface Organization {
|
|
|
212
212
|
customFields?: string;
|
|
213
213
|
/** JSON-encoded metadata visible to members of the organization. */
|
|
214
214
|
publicMetadata?: string;
|
|
215
|
+
/**
|
|
216
|
+
* `false` for an organization the user belongs to. `true` for one they may
|
|
217
|
+
* switch into from an ancestor without being a member — show it as borrowed
|
|
218
|
+
* rather than owned. Only `organization.list()` fills it.
|
|
219
|
+
*
|
|
220
|
+
* Ignoring the field still leaves you with a switchable organization, which
|
|
221
|
+
* is what it is. Absent on an API older than 2026-10-31.
|
|
222
|
+
*/
|
|
223
|
+
inherited?: boolean;
|
|
224
|
+
/** Distance from the root: `0` at a root organization. Indent the switcher by it. */
|
|
225
|
+
depth?: number;
|
|
215
226
|
}
|
|
216
227
|
/** POST /auth/tenants — creates an organization owned by the signed-in user. */
|
|
217
228
|
interface CreateOrganizationInput {
|
|
@@ -219,6 +230,16 @@ interface CreateOrganizationInput {
|
|
|
219
230
|
slug?: string;
|
|
220
231
|
description?: string;
|
|
221
232
|
customFields?: Record<string, unknown>;
|
|
233
|
+
/**
|
|
234
|
+
* Creates the organization underneath an existing one. Omitted creates a
|
|
235
|
+
* root organization, which is all this endpoint could do before the
|
|
236
|
+
* hierarchy existed.
|
|
237
|
+
*
|
|
238
|
+
* The caller has to belong to the parent (or to an organization above it)
|
|
239
|
+
* and hold `subtenants:tree:create` there, and the environment's
|
|
240
|
+
* `maxTenantDepth` has to allow the level — it ships as `1`, roots only.
|
|
241
|
+
*/
|
|
242
|
+
parentTenantId?: string;
|
|
222
243
|
}
|
|
223
244
|
/** GET /auth/tenants/{organizationId}/members — confirmed against the live API. */
|
|
224
245
|
interface OrganizationMember {
|
|
@@ -517,7 +538,13 @@ interface AuthyonClientOptions {
|
|
|
517
538
|
/** Publishable environment key (`pk_live_...` / `pk_test_...`). */
|
|
518
539
|
envKey: string;
|
|
519
540
|
/**
|
|
520
|
-
* Originating client IP to forward on every API call as
|
|
541
|
+
* Originating client IP to forward on every API call as
|
|
542
|
+
* `X-Authyon-Client-Ip` (and `X-Forwarded-For`, for your own proxies).
|
|
543
|
+
*
|
|
544
|
+
* Authyon honours it only from a trusted BFF — a socket peer in the API's
|
|
545
|
+
* `TRUSTED_BFF_IPS`, or a request carrying `TRUSTED_BFF_SECRET`, which this
|
|
546
|
+
* SDK does not send. Never set it in the browser: the connection already
|
|
547
|
+
* carries the address.
|
|
521
548
|
* Browsers cannot reliably discover their public IP by themselves; set this
|
|
522
549
|
* only when your application receives it from trusted infrastructure.
|
|
523
550
|
*/
|
|
@@ -583,7 +610,21 @@ declare class AuthyonClient {
|
|
|
583
610
|
*/
|
|
584
611
|
private hydrateUser;
|
|
585
612
|
private request;
|
|
586
|
-
/**
|
|
613
|
+
/**
|
|
614
|
+
* POST /auth/register — creates a new user (rate-limited to 20/hour per IP).
|
|
615
|
+
*
|
|
616
|
+
* An environment may close public sign-up, and then this route answers as
|
|
617
|
+
* though it were not mapped: **404 with no body**, so the error carries
|
|
618
|
+
* status 404 and the code `unknown`. The SDK does not relabel it, because a
|
|
619
|
+
* wrong `baseUrl` looks exactly the same from here and guessing would send
|
|
620
|
+
* you after the wrong bug — check the environment's setting in the console
|
|
621
|
+
* and the URL you configured. Accounts in a closed environment are created
|
|
622
|
+
* by your backend instead, through `environment.users.create()` in
|
|
623
|
+
* `@authyon/server`, which already needs the `users:write` scope.
|
|
624
|
+
*
|
|
625
|
+
* It gates creation only. Everyone who already has an account signs in as
|
|
626
|
+
* before, and magic link and social sign-up keep their own switches.
|
|
627
|
+
*/
|
|
587
628
|
register(params: RegisterInput): Promise<{
|
|
588
629
|
id: string;
|
|
589
630
|
}>;
|
|
@@ -672,7 +713,15 @@ declare class AuthyonClient {
|
|
|
672
713
|
confirmPasswordReset: (token: string, newPassword: string) => Promise<PasswordResetCompleted>;
|
|
673
714
|
};
|
|
674
715
|
readonly organization: {
|
|
675
|
-
/**
|
|
716
|
+
/**
|
|
717
|
+
* GET /auth/tenants — filterable list of organization memberships.
|
|
718
|
+
*
|
|
719
|
+
* Also lists the organizations below them that the user may switch into
|
|
720
|
+
* without being a member, marked `inherited` — only the ones that actually
|
|
721
|
+
* carry `subtenants:tree:switch`, so the switcher never offers an
|
|
722
|
+
* organization the switch would then refuse. `depth` indents the list into
|
|
723
|
+
* the tree it describes.
|
|
724
|
+
*/
|
|
676
725
|
list: (params?: {
|
|
677
726
|
search?: string;
|
|
678
727
|
} & PaginationOptions) => Promise<Organization[]>;
|
|
@@ -680,16 +729,41 @@ declare class AuthyonClient {
|
|
|
680
729
|
* POST /auth/tenants — creates an organization owned by the signed-in
|
|
681
730
|
* user (only available when self-service organization creation is
|
|
682
731
|
* enabled for the environment).
|
|
732
|
+
*
|
|
733
|
+
* `parentTenantId` nests it under one the user already belongs to, which
|
|
734
|
+
* needs `subtenants:tree:create` there. The creator lands as the effective
|
|
735
|
+
* owner either way, with `tenants:settings:manage`,
|
|
736
|
+
* `tenants:members:invite` and `tenants:members:remove` granted directly
|
|
737
|
+
* inside the new organization.
|
|
683
738
|
*/
|
|
684
739
|
create: (params?: CreateOrganizationInput) => Promise<Organization>;
|
|
685
740
|
/** GET /auth/tenants/{organizationId} — fetch one of the user's organizations by id. */
|
|
686
741
|
get: (organizationId: string) => Promise<Organization>;
|
|
687
742
|
/**
|
|
688
743
|
* PATCH /auth/tenants/{organizationId} — renames the organization.
|
|
689
|
-
*
|
|
744
|
+
*
|
|
745
|
+
* Requires the `tenants:settings:manage` custom permission on it, or
|
|
746
|
+
* `subtenants:tree:manage` on an organization above it. The value was
|
|
747
|
+
* spelled `tenants:manage` before 2026-10-31 — two segments, which the API
|
|
748
|
+
* could not store, so the endpoint refused everyone who had been granted
|
|
749
|
+
* it.
|
|
690
750
|
*/
|
|
691
751
|
rename: (organizationId: string, name: string) => Promise<Organization>;
|
|
692
|
-
/**
|
|
752
|
+
/**
|
|
753
|
+
* POST /auth/switch-tenant — issues a fresh token scoped to the new
|
|
754
|
+
* organization.
|
|
755
|
+
*
|
|
756
|
+
* Membership is enough for one of the user's own organizations. Descending
|
|
757
|
+
* into one below it takes `subtenants:tree:switch` at the organization they
|
|
758
|
+
* belong to — deliberately not the same permission as administering a
|
|
759
|
+
* sub-organization from the outside, since operating AS it is the larger of
|
|
760
|
+
* the two powers. The grants then travel from the node the user belongs to,
|
|
761
|
+
* and the token says so in its `tenant.via`.
|
|
762
|
+
*
|
|
763
|
+
* Refusals: `tenant.outside_scope` (no path down to it),
|
|
764
|
+
* `tenant.ancestor_disabled`, and `tenant.path_stale` when the tree moved
|
|
765
|
+
* under a session — sign-in again rather than retrying.
|
|
766
|
+
*/
|
|
693
767
|
switch: (organizationSlug: string) => Promise<Session>;
|
|
694
768
|
/** The organization the current session is scoped to, from the cached session — no network call. */
|
|
695
769
|
current: () => Organization | null;
|
|
@@ -918,11 +992,25 @@ declare class AuthyonAbilityBuilder {
|
|
|
918
992
|
}
|
|
919
993
|
/** Creates an ability from Authyon permissions, OAuth scope and optional role rules. */
|
|
920
994
|
declare function createAuthyonAbility(source?: AuthyonPermissionSource, options?: AuthyonAbilityOptions): AuthyonAbility;
|
|
921
|
-
/**
|
|
995
|
+
/**
|
|
996
|
+
* Whether a source holds an Authyon permission, by Authyon's own rules: the
|
|
997
|
+
* values are compared segment by segment, `*` stands for any one segment, and
|
|
998
|
+
* two values of different lengths never match — except an all-wildcard grant,
|
|
999
|
+
* which is everything. OAuth scopes count as permissions.
|
|
1000
|
+
*
|
|
1001
|
+
* Not the same question as {@link AuthyonAbility.can}. An ability is CASL, so
|
|
1002
|
+
* the action `manage` there means "every action on this subject", which is a
|
|
1003
|
+
* useful thing to author by hand and a wrong thing to infer from a permission:
|
|
1004
|
+
* `subtenants:tree:manage` and `subtenants:tree:switch` are deliberately
|
|
1005
|
+
* separate grants on the API side — administering a sub-tenant from above and
|
|
1006
|
+
* operating as it are different powers — and reading the first as the second
|
|
1007
|
+
* would have this answer yes to a call the API then refuses. Ask this one
|
|
1008
|
+
* about anything Authyon will be enforcing.
|
|
1009
|
+
*/
|
|
922
1010
|
declare function hasPermission(source: AuthyonPermissionSource | readonly string[], requiredPermission: string): boolean;
|
|
923
1011
|
/**
|
|
924
|
-
* Checks a permission group
|
|
925
|
-
* Empty or omitted groups do not add authorization requirements.
|
|
1012
|
+
* Checks a permission group with {@link hasPermission}, so the same rules as
|
|
1013
|
+
* the API. Empty or omitted groups do not add authorization requirements.
|
|
926
1014
|
*/
|
|
927
1015
|
declare function hasPermissionGroup(source: AuthyonPermissionSource | readonly string[], group: PermissionGroup): boolean;
|
|
928
1016
|
/** Converts Authyon's `subject:action` permission strings to authorization rules. */
|
|
@@ -212,6 +212,17 @@ interface Organization {
|
|
|
212
212
|
customFields?: string;
|
|
213
213
|
/** JSON-encoded metadata visible to members of the organization. */
|
|
214
214
|
publicMetadata?: string;
|
|
215
|
+
/**
|
|
216
|
+
* `false` for an organization the user belongs to. `true` for one they may
|
|
217
|
+
* switch into from an ancestor without being a member — show it as borrowed
|
|
218
|
+
* rather than owned. Only `organization.list()` fills it.
|
|
219
|
+
*
|
|
220
|
+
* Ignoring the field still leaves you with a switchable organization, which
|
|
221
|
+
* is what it is. Absent on an API older than 2026-10-31.
|
|
222
|
+
*/
|
|
223
|
+
inherited?: boolean;
|
|
224
|
+
/** Distance from the root: `0` at a root organization. Indent the switcher by it. */
|
|
225
|
+
depth?: number;
|
|
215
226
|
}
|
|
216
227
|
/** POST /auth/tenants — creates an organization owned by the signed-in user. */
|
|
217
228
|
interface CreateOrganizationInput {
|
|
@@ -219,6 +230,16 @@ interface CreateOrganizationInput {
|
|
|
219
230
|
slug?: string;
|
|
220
231
|
description?: string;
|
|
221
232
|
customFields?: Record<string, unknown>;
|
|
233
|
+
/**
|
|
234
|
+
* Creates the organization underneath an existing one. Omitted creates a
|
|
235
|
+
* root organization, which is all this endpoint could do before the
|
|
236
|
+
* hierarchy existed.
|
|
237
|
+
*
|
|
238
|
+
* The caller has to belong to the parent (or to an organization above it)
|
|
239
|
+
* and hold `subtenants:tree:create` there, and the environment's
|
|
240
|
+
* `maxTenantDepth` has to allow the level — it ships as `1`, roots only.
|
|
241
|
+
*/
|
|
242
|
+
parentTenantId?: string;
|
|
222
243
|
}
|
|
223
244
|
/** GET /auth/tenants/{organizationId}/members — confirmed against the live API. */
|
|
224
245
|
interface OrganizationMember {
|
|
@@ -517,7 +538,13 @@ interface AuthyonClientOptions {
|
|
|
517
538
|
/** Publishable environment key (`pk_live_...` / `pk_test_...`). */
|
|
518
539
|
envKey: string;
|
|
519
540
|
/**
|
|
520
|
-
* Originating client IP to forward on every API call as
|
|
541
|
+
* Originating client IP to forward on every API call as
|
|
542
|
+
* `X-Authyon-Client-Ip` (and `X-Forwarded-For`, for your own proxies).
|
|
543
|
+
*
|
|
544
|
+
* Authyon honours it only from a trusted BFF — a socket peer in the API's
|
|
545
|
+
* `TRUSTED_BFF_IPS`, or a request carrying `TRUSTED_BFF_SECRET`, which this
|
|
546
|
+
* SDK does not send. Never set it in the browser: the connection already
|
|
547
|
+
* carries the address.
|
|
521
548
|
* Browsers cannot reliably discover their public IP by themselves; set this
|
|
522
549
|
* only when your application receives it from trusted infrastructure.
|
|
523
550
|
*/
|
|
@@ -583,7 +610,21 @@ declare class AuthyonClient {
|
|
|
583
610
|
*/
|
|
584
611
|
private hydrateUser;
|
|
585
612
|
private request;
|
|
586
|
-
/**
|
|
613
|
+
/**
|
|
614
|
+
* POST /auth/register — creates a new user (rate-limited to 20/hour per IP).
|
|
615
|
+
*
|
|
616
|
+
* An environment may close public sign-up, and then this route answers as
|
|
617
|
+
* though it were not mapped: **404 with no body**, so the error carries
|
|
618
|
+
* status 404 and the code `unknown`. The SDK does not relabel it, because a
|
|
619
|
+
* wrong `baseUrl` looks exactly the same from here and guessing would send
|
|
620
|
+
* you after the wrong bug — check the environment's setting in the console
|
|
621
|
+
* and the URL you configured. Accounts in a closed environment are created
|
|
622
|
+
* by your backend instead, through `environment.users.create()` in
|
|
623
|
+
* `@authyon/server`, which already needs the `users:write` scope.
|
|
624
|
+
*
|
|
625
|
+
* It gates creation only. Everyone who already has an account signs in as
|
|
626
|
+
* before, and magic link and social sign-up keep their own switches.
|
|
627
|
+
*/
|
|
587
628
|
register(params: RegisterInput): Promise<{
|
|
588
629
|
id: string;
|
|
589
630
|
}>;
|
|
@@ -672,7 +713,15 @@ declare class AuthyonClient {
|
|
|
672
713
|
confirmPasswordReset: (token: string, newPassword: string) => Promise<PasswordResetCompleted>;
|
|
673
714
|
};
|
|
674
715
|
readonly organization: {
|
|
675
|
-
/**
|
|
716
|
+
/**
|
|
717
|
+
* GET /auth/tenants — filterable list of organization memberships.
|
|
718
|
+
*
|
|
719
|
+
* Also lists the organizations below them that the user may switch into
|
|
720
|
+
* without being a member, marked `inherited` — only the ones that actually
|
|
721
|
+
* carry `subtenants:tree:switch`, so the switcher never offers an
|
|
722
|
+
* organization the switch would then refuse. `depth` indents the list into
|
|
723
|
+
* the tree it describes.
|
|
724
|
+
*/
|
|
676
725
|
list: (params?: {
|
|
677
726
|
search?: string;
|
|
678
727
|
} & PaginationOptions) => Promise<Organization[]>;
|
|
@@ -680,16 +729,41 @@ declare class AuthyonClient {
|
|
|
680
729
|
* POST /auth/tenants — creates an organization owned by the signed-in
|
|
681
730
|
* user (only available when self-service organization creation is
|
|
682
731
|
* enabled for the environment).
|
|
732
|
+
*
|
|
733
|
+
* `parentTenantId` nests it under one the user already belongs to, which
|
|
734
|
+
* needs `subtenants:tree:create` there. The creator lands as the effective
|
|
735
|
+
* owner either way, with `tenants:settings:manage`,
|
|
736
|
+
* `tenants:members:invite` and `tenants:members:remove` granted directly
|
|
737
|
+
* inside the new organization.
|
|
683
738
|
*/
|
|
684
739
|
create: (params?: CreateOrganizationInput) => Promise<Organization>;
|
|
685
740
|
/** GET /auth/tenants/{organizationId} — fetch one of the user's organizations by id. */
|
|
686
741
|
get: (organizationId: string) => Promise<Organization>;
|
|
687
742
|
/**
|
|
688
743
|
* PATCH /auth/tenants/{organizationId} — renames the organization.
|
|
689
|
-
*
|
|
744
|
+
*
|
|
745
|
+
* Requires the `tenants:settings:manage` custom permission on it, or
|
|
746
|
+
* `subtenants:tree:manage` on an organization above it. The value was
|
|
747
|
+
* spelled `tenants:manage` before 2026-10-31 — two segments, which the API
|
|
748
|
+
* could not store, so the endpoint refused everyone who had been granted
|
|
749
|
+
* it.
|
|
690
750
|
*/
|
|
691
751
|
rename: (organizationId: string, name: string) => Promise<Organization>;
|
|
692
|
-
/**
|
|
752
|
+
/**
|
|
753
|
+
* POST /auth/switch-tenant — issues a fresh token scoped to the new
|
|
754
|
+
* organization.
|
|
755
|
+
*
|
|
756
|
+
* Membership is enough for one of the user's own organizations. Descending
|
|
757
|
+
* into one below it takes `subtenants:tree:switch` at the organization they
|
|
758
|
+
* belong to — deliberately not the same permission as administering a
|
|
759
|
+
* sub-organization from the outside, since operating AS it is the larger of
|
|
760
|
+
* the two powers. The grants then travel from the node the user belongs to,
|
|
761
|
+
* and the token says so in its `tenant.via`.
|
|
762
|
+
*
|
|
763
|
+
* Refusals: `tenant.outside_scope` (no path down to it),
|
|
764
|
+
* `tenant.ancestor_disabled`, and `tenant.path_stale` when the tree moved
|
|
765
|
+
* under a session — sign-in again rather than retrying.
|
|
766
|
+
*/
|
|
693
767
|
switch: (organizationSlug: string) => Promise<Session>;
|
|
694
768
|
/** The organization the current session is scoped to, from the cached session — no network call. */
|
|
695
769
|
current: () => Organization | null;
|
|
@@ -918,11 +992,25 @@ declare class AuthyonAbilityBuilder {
|
|
|
918
992
|
}
|
|
919
993
|
/** Creates an ability from Authyon permissions, OAuth scope and optional role rules. */
|
|
920
994
|
declare function createAuthyonAbility(source?: AuthyonPermissionSource, options?: AuthyonAbilityOptions): AuthyonAbility;
|
|
921
|
-
/**
|
|
995
|
+
/**
|
|
996
|
+
* Whether a source holds an Authyon permission, by Authyon's own rules: the
|
|
997
|
+
* values are compared segment by segment, `*` stands for any one segment, and
|
|
998
|
+
* two values of different lengths never match — except an all-wildcard grant,
|
|
999
|
+
* which is everything. OAuth scopes count as permissions.
|
|
1000
|
+
*
|
|
1001
|
+
* Not the same question as {@link AuthyonAbility.can}. An ability is CASL, so
|
|
1002
|
+
* the action `manage` there means "every action on this subject", which is a
|
|
1003
|
+
* useful thing to author by hand and a wrong thing to infer from a permission:
|
|
1004
|
+
* `subtenants:tree:manage` and `subtenants:tree:switch` are deliberately
|
|
1005
|
+
* separate grants on the API side — administering a sub-tenant from above and
|
|
1006
|
+
* operating as it are different powers — and reading the first as the second
|
|
1007
|
+
* would have this answer yes to a call the API then refuses. Ask this one
|
|
1008
|
+
* about anything Authyon will be enforcing.
|
|
1009
|
+
*/
|
|
922
1010
|
declare function hasPermission(source: AuthyonPermissionSource | readonly string[], requiredPermission: string): boolean;
|
|
923
1011
|
/**
|
|
924
|
-
* Checks a permission group
|
|
925
|
-
* Empty or omitted groups do not add authorization requirements.
|
|
1012
|
+
* Checks a permission group with {@link hasPermission}, so the same rules as
|
|
1013
|
+
* the API. Empty or omitted groups do not add authorization requirements.
|
|
926
1014
|
*/
|
|
927
1015
|
declare function hasPermissionGroup(source: AuthyonPermissionSource | readonly string[], group: PermissionGroup): boolean;
|
|
928
1016
|
/** Converts Authyon's `subject:action` permission strings to authorization rules. */
|
|
@@ -194,12 +194,29 @@ function createAuthyonAbility(source = {}, options = {}) {
|
|
|
194
194
|
return new AuthyonAbility(createAuthyonRules(source, options), options.detectSubjectType);
|
|
195
195
|
}
|
|
196
196
|
function hasPermission(source, requiredPermission) {
|
|
197
|
-
const
|
|
198
|
-
if (!
|
|
199
|
-
return false;
|
|
200
|
-
}
|
|
197
|
+
const required = requiredPermission?.trim() ?? "";
|
|
198
|
+
if (!isWellFormedPermission(required)) return false;
|
|
201
199
|
const permissionSource = isPermissionList(source) ? { permissions: source } : source;
|
|
202
|
-
|
|
200
|
+
const granted = [
|
|
201
|
+
...permissionSource.permissions ?? [],
|
|
202
|
+
...permissionSource.scope?.split(/\s+/).filter(Boolean) ?? []
|
|
203
|
+
];
|
|
204
|
+
return granted.some((permission) => grants(permission.trim(), required));
|
|
205
|
+
}
|
|
206
|
+
function grants(granted, required) {
|
|
207
|
+
if (!isWellFormedPermission(granted)) return false;
|
|
208
|
+
if (isFullWildcard(granted)) return true;
|
|
209
|
+
const grantedSegments = granted.split(":");
|
|
210
|
+
const requiredSegments = required.split(":");
|
|
211
|
+
if (grantedSegments.length !== requiredSegments.length) return false;
|
|
212
|
+
return grantedSegments.every(
|
|
213
|
+
(segment, index) => segment === "*" || segment === requiredSegments[index]
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
function isWellFormedPermission(permission) {
|
|
217
|
+
if (!permission) return false;
|
|
218
|
+
const segments = permission.split(":");
|
|
219
|
+
return segments.length >= 2 && segments.every((segment) => segment.length > 0);
|
|
203
220
|
}
|
|
204
221
|
function hasPermissionGroup(source, group) {
|
|
205
222
|
const hasAll = (group.allOf ?? []).every((permission) => hasPermission(source, permission));
|
|
@@ -214,13 +231,23 @@ function createAuthyonRules(source = {}, options = {}) {
|
|
|
214
231
|
...source.permissions ?? [],
|
|
215
232
|
...source.scope?.split(/\s+/).filter(Boolean) ?? []
|
|
216
233
|
]);
|
|
217
|
-
const rules = [...permissions].map(
|
|
234
|
+
const rules = [...permissions].map(grantToRule).filter(isAbilityRule);
|
|
218
235
|
for (const role of /* @__PURE__ */ new Set([...source.roles ?? [], ...options.roles ?? []])) {
|
|
219
236
|
rules.push(...(options.roleRules?.[role] ?? []).map(cloneRule));
|
|
220
237
|
}
|
|
221
238
|
rules.push(...(options.rules ?? []).map(cloneRule));
|
|
222
239
|
return rules;
|
|
223
240
|
}
|
|
241
|
+
function grantToRule(permission) {
|
|
242
|
+
const normalized = permission.trim();
|
|
243
|
+
if (normalized && isFullWildcard(normalized)) {
|
|
244
|
+
return { action: "manage", subject: "all" };
|
|
245
|
+
}
|
|
246
|
+
return permissionToRule(normalized);
|
|
247
|
+
}
|
|
248
|
+
function isFullWildcard(permission) {
|
|
249
|
+
return permission.split(":").every((segment) => segment === "*");
|
|
250
|
+
}
|
|
224
251
|
function permissionToRule(permission) {
|
|
225
252
|
const normalized = permission.trim();
|
|
226
253
|
if (!normalized) return null;
|
package/dist/index.cjs
CHANGED
|
@@ -29,6 +29,8 @@ __export(index_exports, {
|
|
|
29
29
|
ErrorCodes: () => ErrorCodes,
|
|
30
30
|
FetchHttpAdapter: () => FetchHttpAdapter,
|
|
31
31
|
LoggingHttpAdapter: () => LoggingHttpAdapter,
|
|
32
|
+
SubtenantPermissions: () => SubtenantPermissions,
|
|
33
|
+
TenantPermissions: () => TenantPermissions,
|
|
32
34
|
createAuthyonAbility: () => createAuthyonAbility,
|
|
33
35
|
createAuthyonRules: () => createAuthyonRules,
|
|
34
36
|
createClient: () => createClient,
|
|
@@ -269,7 +271,14 @@ function appendQuery(path2, params) {
|
|
|
269
271
|
if (!params) return path2;
|
|
270
272
|
const query = new URLSearchParams();
|
|
271
273
|
for (const [key, value2] of Object.entries(params)) {
|
|
272
|
-
if (value2
|
|
274
|
+
if (value2 === void 0) continue;
|
|
275
|
+
if (Array.isArray(value2)) {
|
|
276
|
+
for (const item of value2) {
|
|
277
|
+
if (item !== void 0) query.append(key, String(item));
|
|
278
|
+
}
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
query.set(key, String(value2));
|
|
273
282
|
}
|
|
274
283
|
const value = query.toString();
|
|
275
284
|
return value ? `${path2}${path2.includes("?") ? "&" : "?"}${value}` : path2;
|
|
@@ -467,9 +476,11 @@ var JsonHttpClient = class {
|
|
|
467
476
|
};
|
|
468
477
|
|
|
469
478
|
// ../../internal/core/http/clientIp.ts
|
|
479
|
+
var CLIENT_IP_HEADER = "X-Authyon-Client-Ip";
|
|
470
480
|
function clientIpHeaders(context) {
|
|
471
481
|
if (!context?.clientIp) return {};
|
|
472
|
-
|
|
482
|
+
const ip = normalizeClientIp(context.clientIp);
|
|
483
|
+
return { "X-Forwarded-For": ip, [CLIENT_IP_HEADER]: ip };
|
|
473
484
|
}
|
|
474
485
|
function normalizeClientIp(value) {
|
|
475
486
|
let ip = value.trim();
|
|
@@ -598,26 +609,59 @@ var AuthyonClient = class {
|
|
|
598
609
|
};
|
|
599
610
|
// ── Organization ─────────────────────────────────────────────────────────
|
|
600
611
|
this.organization = {
|
|
601
|
-
/**
|
|
612
|
+
/**
|
|
613
|
+
* GET /auth/tenants — filterable list of organization memberships.
|
|
614
|
+
*
|
|
615
|
+
* Also lists the organizations below them that the user may switch into
|
|
616
|
+
* without being a member, marked `inherited` — only the ones that actually
|
|
617
|
+
* carry `subtenants:tree:switch`, so the switcher never offers an
|
|
618
|
+
* organization the switch would then refuse. `depth` indents the list into
|
|
619
|
+
* the tree it describes.
|
|
620
|
+
*/
|
|
602
621
|
list: (params = {}) => this.request(appendQuery("/auth/tenants", params), { bearer: true }),
|
|
603
622
|
/**
|
|
604
623
|
* POST /auth/tenants — creates an organization owned by the signed-in
|
|
605
624
|
* user (only available when self-service organization creation is
|
|
606
625
|
* enabled for the environment).
|
|
626
|
+
*
|
|
627
|
+
* `parentTenantId` nests it under one the user already belongs to, which
|
|
628
|
+
* needs `subtenants:tree:create` there. The creator lands as the effective
|
|
629
|
+
* owner either way, with `tenants:settings:manage`,
|
|
630
|
+
* `tenants:members:invite` and `tenants:members:remove` granted directly
|
|
631
|
+
* inside the new organization.
|
|
607
632
|
*/
|
|
608
633
|
create: (params = {}) => this.request("/auth/tenants", { method: "POST", bearer: true, body: params }),
|
|
609
634
|
/** GET /auth/tenants/{organizationId} — fetch one of the user's organizations by id. */
|
|
610
635
|
get: (organizationId) => this.request(`/auth/tenants/${encodeURIComponent(organizationId)}`, { bearer: true }),
|
|
611
636
|
/**
|
|
612
637
|
* PATCH /auth/tenants/{organizationId} — renames the organization.
|
|
613
|
-
*
|
|
638
|
+
*
|
|
639
|
+
* Requires the `tenants:settings:manage` custom permission on it, or
|
|
640
|
+
* `subtenants:tree:manage` on an organization above it. The value was
|
|
641
|
+
* spelled `tenants:manage` before 2026-10-31 — two segments, which the API
|
|
642
|
+
* could not store, so the endpoint refused everyone who had been granted
|
|
643
|
+
* it.
|
|
614
644
|
*/
|
|
615
645
|
rename: (organizationId, name) => this.request(`/auth/tenants/${encodeURIComponent(organizationId)}`, {
|
|
616
646
|
method: "PATCH",
|
|
617
647
|
bearer: true,
|
|
618
648
|
body: { name }
|
|
619
649
|
}),
|
|
620
|
-
/**
|
|
650
|
+
/**
|
|
651
|
+
* POST /auth/switch-tenant — issues a fresh token scoped to the new
|
|
652
|
+
* organization.
|
|
653
|
+
*
|
|
654
|
+
* Membership is enough for one of the user's own organizations. Descending
|
|
655
|
+
* into one below it takes `subtenants:tree:switch` at the organization they
|
|
656
|
+
* belong to — deliberately not the same permission as administering a
|
|
657
|
+
* sub-organization from the outside, since operating AS it is the larger of
|
|
658
|
+
* the two powers. The grants then travel from the node the user belongs to,
|
|
659
|
+
* and the token says so in its `tenant.via`.
|
|
660
|
+
*
|
|
661
|
+
* Refusals: `tenant.outside_scope` (no path down to it),
|
|
662
|
+
* `tenant.ancestor_disabled`, and `tenant.path_stale` when the tree moved
|
|
663
|
+
* under a session — sign-in again rather than retrying.
|
|
664
|
+
*/
|
|
621
665
|
switch: (organizationSlug) => this.request("/auth/switch-tenant", {
|
|
622
666
|
method: "POST",
|
|
623
667
|
bearer: true,
|
|
@@ -931,7 +975,21 @@ var AuthyonClient = class {
|
|
|
931
975
|
return this.http.parse(response);
|
|
932
976
|
}
|
|
933
977
|
// ── Auth flows ───────────────────────────────────────────────────────────
|
|
934
|
-
/**
|
|
978
|
+
/**
|
|
979
|
+
* POST /auth/register — creates a new user (rate-limited to 20/hour per IP).
|
|
980
|
+
*
|
|
981
|
+
* An environment may close public sign-up, and then this route answers as
|
|
982
|
+
* though it were not mapped: **404 with no body**, so the error carries
|
|
983
|
+
* status 404 and the code `unknown`. The SDK does not relabel it, because a
|
|
984
|
+
* wrong `baseUrl` looks exactly the same from here and guessing would send
|
|
985
|
+
* you after the wrong bug — check the environment's setting in the console
|
|
986
|
+
* and the URL you configured. Accounts in a closed environment are created
|
|
987
|
+
* by your backend instead, through `environment.users.create()` in
|
|
988
|
+
* `@authyon/server`, which already needs the `users:write` scope.
|
|
989
|
+
*
|
|
990
|
+
* It gates creation only. Everyone who already has an account signs in as
|
|
991
|
+
* before, and magic link and social sign-up keep their own switches.
|
|
992
|
+
*/
|
|
935
993
|
async register(params) {
|
|
936
994
|
return this.request("/auth/register", { method: "POST", body: params });
|
|
937
995
|
}
|
|
@@ -1288,12 +1346,29 @@ function createAuthyonAbility(source = {}, options = {}) {
|
|
|
1288
1346
|
return new AuthyonAbility(createAuthyonRules(source, options), options.detectSubjectType);
|
|
1289
1347
|
}
|
|
1290
1348
|
function hasPermission(source, requiredPermission) {
|
|
1291
|
-
const
|
|
1292
|
-
if (!
|
|
1293
|
-
return false;
|
|
1294
|
-
}
|
|
1349
|
+
const required = requiredPermission?.trim() ?? "";
|
|
1350
|
+
if (!isWellFormedPermission(required)) return false;
|
|
1295
1351
|
const permissionSource = isPermissionList(source) ? { permissions: source } : source;
|
|
1296
|
-
|
|
1352
|
+
const granted = [
|
|
1353
|
+
...permissionSource.permissions ?? [],
|
|
1354
|
+
...permissionSource.scope?.split(/\s+/).filter(Boolean) ?? []
|
|
1355
|
+
];
|
|
1356
|
+
return granted.some((permission) => grants(permission.trim(), required));
|
|
1357
|
+
}
|
|
1358
|
+
function grants(granted, required) {
|
|
1359
|
+
if (!isWellFormedPermission(granted)) return false;
|
|
1360
|
+
if (isFullWildcard(granted)) return true;
|
|
1361
|
+
const grantedSegments = granted.split(":");
|
|
1362
|
+
const requiredSegments = required.split(":");
|
|
1363
|
+
if (grantedSegments.length !== requiredSegments.length) return false;
|
|
1364
|
+
return grantedSegments.every(
|
|
1365
|
+
(segment, index) => segment === "*" || segment === requiredSegments[index]
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
function isWellFormedPermission(permission) {
|
|
1369
|
+
if (!permission) return false;
|
|
1370
|
+
const segments = permission.split(":");
|
|
1371
|
+
return segments.length >= 2 && segments.every((segment) => segment.length > 0);
|
|
1297
1372
|
}
|
|
1298
1373
|
function hasPermissionGroup(source, group) {
|
|
1299
1374
|
const hasAll = (group.allOf ?? []).every((permission) => hasPermission(source, permission));
|
|
@@ -1308,13 +1383,23 @@ function createAuthyonRules(source = {}, options = {}) {
|
|
|
1308
1383
|
...source.permissions ?? [],
|
|
1309
1384
|
...source.scope?.split(/\s+/).filter(Boolean) ?? []
|
|
1310
1385
|
]);
|
|
1311
|
-
const rules = [...permissions].map(
|
|
1386
|
+
const rules = [...permissions].map(grantToRule).filter(isAbilityRule);
|
|
1312
1387
|
for (const role of /* @__PURE__ */ new Set([...source.roles ?? [], ...options.roles ?? []])) {
|
|
1313
1388
|
rules.push(...(options.roleRules?.[role] ?? []).map(cloneRule));
|
|
1314
1389
|
}
|
|
1315
1390
|
rules.push(...(options.rules ?? []).map(cloneRule));
|
|
1316
1391
|
return rules;
|
|
1317
1392
|
}
|
|
1393
|
+
function grantToRule(permission) {
|
|
1394
|
+
const normalized = permission.trim();
|
|
1395
|
+
if (normalized && isFullWildcard(normalized)) {
|
|
1396
|
+
return { action: "manage", subject: "all" };
|
|
1397
|
+
}
|
|
1398
|
+
return permissionToRule(normalized);
|
|
1399
|
+
}
|
|
1400
|
+
function isFullWildcard(permission) {
|
|
1401
|
+
return permission.split(":").every((segment) => segment === "*");
|
|
1402
|
+
}
|
|
1318
1403
|
function permissionToRule(permission) {
|
|
1319
1404
|
const normalized = permission.trim();
|
|
1320
1405
|
if (!normalized) return null;
|
|
@@ -1411,6 +1496,32 @@ function isRecord(value) {
|
|
|
1411
1496
|
function isAbilityRule(value) {
|
|
1412
1497
|
return value !== null;
|
|
1413
1498
|
}
|
|
1499
|
+
|
|
1500
|
+
// ../../internal/core/authorization/tenantPermissions.ts
|
|
1501
|
+
var TenantPermissions = {
|
|
1502
|
+
/** Rename the tenant and change its settings. */
|
|
1503
|
+
SettingsManage: "tenants:settings:manage",
|
|
1504
|
+
/** Invite an address to the tenant. */
|
|
1505
|
+
MembersInvite: "tenants:members:invite",
|
|
1506
|
+
/** Remove a member from the tenant. */
|
|
1507
|
+
MembersRemove: "tenants:members:remove"
|
|
1508
|
+
};
|
|
1509
|
+
var SubtenantPermissions = {
|
|
1510
|
+
/** Read any tenant below one you belong to. */
|
|
1511
|
+
TreeRead: "subtenants:tree:read",
|
|
1512
|
+
/** Rename a tenant below yours, and add or remove its members. */
|
|
1513
|
+
TreeManage: "subtenants:tree:manage",
|
|
1514
|
+
/** Create a tenant underneath one you belong to. */
|
|
1515
|
+
TreeCreate: "subtenants:tree:create",
|
|
1516
|
+
/**
|
|
1517
|
+
* Make a tenant below yours the ACTIVE tenant of the session — acting inside
|
|
1518
|
+
* it rather than on it from outside. Deliberately not the same permission as
|
|
1519
|
+
* {@link SubtenantPermissions.TreeManage}: administering a sub-tenant through
|
|
1520
|
+
* the management endpoints and operating as that sub-tenant are different
|
|
1521
|
+
* powers, and granting the first should not hand over the second.
|
|
1522
|
+
*/
|
|
1523
|
+
TreeSwitch: "subtenants:tree:switch"
|
|
1524
|
+
};
|
|
1414
1525
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1415
1526
|
0 && (module.exports = {
|
|
1416
1527
|
AuthyonAbility,
|
|
@@ -1422,6 +1533,8 @@ function isAbilityRule(value) {
|
|
|
1422
1533
|
ErrorCodes,
|
|
1423
1534
|
FetchHttpAdapter,
|
|
1424
1535
|
LoggingHttpAdapter,
|
|
1536
|
+
SubtenantPermissions,
|
|
1537
|
+
TenantPermissions,
|
|
1425
1538
|
createAuthyonAbility,
|
|
1426
1539
|
createAuthyonRules,
|
|
1427
1540
|
createClient,
|