@authyon/auth 0.2.0-beta.21 → 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 +117 -11
- package/dist/index.d.cts +53 -3
- package/dist/index.d.ts +53 -3
- package/dist/index.js +83 -6
- 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,
|
|
@@ -474,9 +476,11 @@ var JsonHttpClient = class {
|
|
|
474
476
|
};
|
|
475
477
|
|
|
476
478
|
// ../../internal/core/http/clientIp.ts
|
|
479
|
+
var CLIENT_IP_HEADER = "X-Authyon-Client-Ip";
|
|
477
480
|
function clientIpHeaders(context) {
|
|
478
481
|
if (!context?.clientIp) return {};
|
|
479
|
-
|
|
482
|
+
const ip = normalizeClientIp(context.clientIp);
|
|
483
|
+
return { "X-Forwarded-For": ip, [CLIENT_IP_HEADER]: ip };
|
|
480
484
|
}
|
|
481
485
|
function normalizeClientIp(value) {
|
|
482
486
|
let ip = value.trim();
|
|
@@ -605,26 +609,59 @@ var AuthyonClient = class {
|
|
|
605
609
|
};
|
|
606
610
|
// ── Organization ─────────────────────────────────────────────────────────
|
|
607
611
|
this.organization = {
|
|
608
|
-
/**
|
|
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
|
+
*/
|
|
609
621
|
list: (params = {}) => this.request(appendQuery("/auth/tenants", params), { bearer: true }),
|
|
610
622
|
/**
|
|
611
623
|
* POST /auth/tenants — creates an organization owned by the signed-in
|
|
612
624
|
* user (only available when self-service organization creation is
|
|
613
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.
|
|
614
632
|
*/
|
|
615
633
|
create: (params = {}) => this.request("/auth/tenants", { method: "POST", bearer: true, body: params }),
|
|
616
634
|
/** GET /auth/tenants/{organizationId} — fetch one of the user's organizations by id. */
|
|
617
635
|
get: (organizationId) => this.request(`/auth/tenants/${encodeURIComponent(organizationId)}`, { bearer: true }),
|
|
618
636
|
/**
|
|
619
637
|
* PATCH /auth/tenants/{organizationId} — renames the organization.
|
|
620
|
-
*
|
|
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.
|
|
621
644
|
*/
|
|
622
645
|
rename: (organizationId, name) => this.request(`/auth/tenants/${encodeURIComponent(organizationId)}`, {
|
|
623
646
|
method: "PATCH",
|
|
624
647
|
bearer: true,
|
|
625
648
|
body: { name }
|
|
626
649
|
}),
|
|
627
|
-
/**
|
|
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
|
+
*/
|
|
628
665
|
switch: (organizationSlug) => this.request("/auth/switch-tenant", {
|
|
629
666
|
method: "POST",
|
|
630
667
|
bearer: true,
|
|
@@ -938,7 +975,21 @@ var AuthyonClient = class {
|
|
|
938
975
|
return this.http.parse(response);
|
|
939
976
|
}
|
|
940
977
|
// ── Auth flows ───────────────────────────────────────────────────────────
|
|
941
|
-
/**
|
|
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
|
+
*/
|
|
942
993
|
async register(params) {
|
|
943
994
|
return this.request("/auth/register", { method: "POST", body: params });
|
|
944
995
|
}
|
|
@@ -1295,12 +1346,29 @@ function createAuthyonAbility(source = {}, options = {}) {
|
|
|
1295
1346
|
return new AuthyonAbility(createAuthyonRules(source, options), options.detectSubjectType);
|
|
1296
1347
|
}
|
|
1297
1348
|
function hasPermission(source, requiredPermission) {
|
|
1298
|
-
const
|
|
1299
|
-
if (!
|
|
1300
|
-
return false;
|
|
1301
|
-
}
|
|
1349
|
+
const required = requiredPermission?.trim() ?? "";
|
|
1350
|
+
if (!isWellFormedPermission(required)) return false;
|
|
1302
1351
|
const permissionSource = isPermissionList(source) ? { permissions: source } : source;
|
|
1303
|
-
|
|
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);
|
|
1304
1372
|
}
|
|
1305
1373
|
function hasPermissionGroup(source, group) {
|
|
1306
1374
|
const hasAll = (group.allOf ?? []).every((permission) => hasPermission(source, permission));
|
|
@@ -1315,13 +1383,23 @@ function createAuthyonRules(source = {}, options = {}) {
|
|
|
1315
1383
|
...source.permissions ?? [],
|
|
1316
1384
|
...source.scope?.split(/\s+/).filter(Boolean) ?? []
|
|
1317
1385
|
]);
|
|
1318
|
-
const rules = [...permissions].map(
|
|
1386
|
+
const rules = [...permissions].map(grantToRule).filter(isAbilityRule);
|
|
1319
1387
|
for (const role of /* @__PURE__ */ new Set([...source.roles ?? [], ...options.roles ?? []])) {
|
|
1320
1388
|
rules.push(...(options.roleRules?.[role] ?? []).map(cloneRule));
|
|
1321
1389
|
}
|
|
1322
1390
|
rules.push(...(options.rules ?? []).map(cloneRule));
|
|
1323
1391
|
return rules;
|
|
1324
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
|
+
}
|
|
1325
1403
|
function permissionToRule(permission) {
|
|
1326
1404
|
const normalized = permission.trim();
|
|
1327
1405
|
if (!normalized) return null;
|
|
@@ -1418,6 +1496,32 @@ function isRecord(value) {
|
|
|
1418
1496
|
function isAbilityRule(value) {
|
|
1419
1497
|
return value !== null;
|
|
1420
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
|
+
};
|
|
1421
1525
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1422
1526
|
0 && (module.exports = {
|
|
1423
1527
|
AuthyonAbility,
|
|
@@ -1429,6 +1533,8 @@ function isAbilityRule(value) {
|
|
|
1429
1533
|
ErrorCodes,
|
|
1430
1534
|
FetchHttpAdapter,
|
|
1431
1535
|
LoggingHttpAdapter,
|
|
1536
|
+
SubtenantPermissions,
|
|
1537
|
+
TenantPermissions,
|
|
1432
1538
|
createAuthyonAbility,
|
|
1433
1539
|
createAuthyonRules,
|
|
1434
1540
|
createClient,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { T as TokenStorage, H as HttpAdapter, a as HttpLoggerOptions, A as AuthyonClient } from './ability-
|
|
2
|
-
export { b as AbilityConditions, c as AbilityEvent, d as AbilityListener, e as AbilityRule, f as AbilitySubject, g as AcceptOrganizationInviteInput, h as Activity, i as Approval, j as ApprovalAssurance, k as ApprovalPayload, l as ApprovalStatus, m as ApprovalWebAuthnOptions, n as AuthEvent, o as AuthState, p as AuthStateListener, q as AuthenticatorSetup, r as AuthyonAbility, s as AuthyonAbilityBuilder, t as AuthyonAbilityOptions, u as AuthyonClientOptions, v as AuthyonPermissionSource, w as AuthyonSessionController, C as ConfirmApprovalInput, x as CreateOrganizationInput, F as FetchHttpAdapter, y as HttpAdapterRequest, z as HttpLogEvent, B as HttpLogger, I as IntrospectResult, D as InviteMemberInput, L as LoggingHttpAdapter, E as LoginInput, G as LoginResult, O as Organization, J as OrganizationInvite, K as OrganizationInviteAccepted, M as OrganizationInviteInput, N as OrganizationInviteIssued, P as OrganizationInvitePreview, Q as OrganizationInviteStatus, R as OrganizationMember, S as Paged, U as PaginationOptions, V as PasswordResetCompleted, W as PasswordResetPreview, X as PasswordResetRequestOptions, Y as PermissionGroup, Z as RegisterInput, _ as Role, $ as SecurityMethods, a0 as Session, a1 as SessionControllerOptions, a2 as SessionInfo, a3 as SessionSnapshot, a4 as SessionSnapshotListener, a5 as SessionStatus, a6 as SsoProvider, a7 as SubjectApprovals, a8 as SubjectSecurity, a9 as TwoFactorChallenge, aa as TwoFactorMethod, ab as TwoFactorStatus, ac as User, ad as ValidateResult, ae as VerifyTwoFactorInput, af as WebAuthnAssertion, ag as WebAuthnCeremonyStart, ah as WebAuthnCredential, ai as createAuthyonAbility, aj as createAuthyonRules, ak as createClient, al as hasPermission, am as hasPermissionGroup } from './ability-
|
|
1
|
+
import { T as TokenStorage, H as HttpAdapter, a as HttpLoggerOptions, A as AuthyonClient } from './ability-DjEMCVjx.cjs';
|
|
2
|
+
export { b as AbilityConditions, c as AbilityEvent, d as AbilityListener, e as AbilityRule, f as AbilitySubject, g as AcceptOrganizationInviteInput, h as Activity, i as Approval, j as ApprovalAssurance, k as ApprovalPayload, l as ApprovalStatus, m as ApprovalWebAuthnOptions, n as AuthEvent, o as AuthState, p as AuthStateListener, q as AuthenticatorSetup, r as AuthyonAbility, s as AuthyonAbilityBuilder, t as AuthyonAbilityOptions, u as AuthyonClientOptions, v as AuthyonPermissionSource, w as AuthyonSessionController, C as ConfirmApprovalInput, x as CreateOrganizationInput, F as FetchHttpAdapter, y as HttpAdapterRequest, z as HttpLogEvent, B as HttpLogger, I as IntrospectResult, D as InviteMemberInput, L as LoggingHttpAdapter, E as LoginInput, G as LoginResult, O as Organization, J as OrganizationInvite, K as OrganizationInviteAccepted, M as OrganizationInviteInput, N as OrganizationInviteIssued, P as OrganizationInvitePreview, Q as OrganizationInviteStatus, R as OrganizationMember, S as Paged, U as PaginationOptions, V as PasswordResetCompleted, W as PasswordResetPreview, X as PasswordResetRequestOptions, Y as PermissionGroup, Z as RegisterInput, _ as Role, $ as SecurityMethods, a0 as Session, a1 as SessionControllerOptions, a2 as SessionInfo, a3 as SessionSnapshot, a4 as SessionSnapshotListener, a5 as SessionStatus, a6 as SsoProvider, a7 as SubjectApprovals, a8 as SubjectSecurity, a9 as TwoFactorChallenge, aa as TwoFactorMethod, ab as TwoFactorStatus, ac as User, ad as ValidateResult, ae as VerifyTwoFactorInput, af as WebAuthnAssertion, ag as WebAuthnCeremonyStart, ah as WebAuthnCredential, ai as createAuthyonAbility, aj as createAuthyonRules, ak as createClient, al as hasPermission, am as hasPermissionGroup } from './ability-DjEMCVjx.cjs';
|
|
3
3
|
|
|
4
4
|
declare const ErrorCodes: {
|
|
5
5
|
readonly Unknown: "unknown";
|
|
@@ -99,4 +99,54 @@ declare function createMemoryStorage(): TokenStorage;
|
|
|
99
99
|
declare function createLocalStorage(key?: string): TokenStorage;
|
|
100
100
|
declare function createDefaultStorage(): TokenStorage;
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
/**
|
|
103
|
+
* The permission values the tenant surface checks, as constants.
|
|
104
|
+
*
|
|
105
|
+
* They are strings on the wire and nothing forces you through here — the point
|
|
106
|
+
* is that a rename shows up as a type error instead of as a 403 nobody can
|
|
107
|
+
* explain. `tenants:settings:manage` is exactly that case: it used to be
|
|
108
|
+
* written `tenants:manage`, a two-segment value the API could not even store,
|
|
109
|
+
* so the endpoint requiring it refused every caller.
|
|
110
|
+
*/
|
|
111
|
+
declare const TenantPermissions: {
|
|
112
|
+
/** Rename the tenant and change its settings. */
|
|
113
|
+
readonly SettingsManage: "tenants:settings:manage";
|
|
114
|
+
/** Invite an address to the tenant. */
|
|
115
|
+
readonly MembersInvite: "tenants:members:invite";
|
|
116
|
+
/** Remove a member from the tenant. */
|
|
117
|
+
readonly MembersRemove: "tenants:members:remove";
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* What lets a member of one tenant reach the tenants beneath it.
|
|
121
|
+
*
|
|
122
|
+
* A separate domain, not a suffix on `tenants:…`, and the reason is worth
|
|
123
|
+
* knowing before you grant one: permissions match per segment, so naming these
|
|
124
|
+
* `tenants:tree:manage` would have handed every role already holding
|
|
125
|
+
* `tenants:*:*` power over the whole subtree the day the hierarchy shipped — a
|
|
126
|
+
* widening that appears in no diff. `subtenants:…` is only covered by `*:*:*`,
|
|
127
|
+
* which is explicit superuser.
|
|
128
|
+
*
|
|
129
|
+
* Reaching down is opt-in at every level: holding `tenants:settings:manage` on
|
|
130
|
+
* a parent grants nothing below it, and each of these is granted at the node
|
|
131
|
+
* the caller belongs to, never at the target.
|
|
132
|
+
*/
|
|
133
|
+
declare const SubtenantPermissions: {
|
|
134
|
+
/** Read any tenant below one you belong to. */
|
|
135
|
+
readonly TreeRead: "subtenants:tree:read";
|
|
136
|
+
/** Rename a tenant below yours, and add or remove its members. */
|
|
137
|
+
readonly TreeManage: "subtenants:tree:manage";
|
|
138
|
+
/** Create a tenant underneath one you belong to. */
|
|
139
|
+
readonly TreeCreate: "subtenants:tree:create";
|
|
140
|
+
/**
|
|
141
|
+
* Make a tenant below yours the ACTIVE tenant of the session — acting inside
|
|
142
|
+
* it rather than on it from outside. Deliberately not the same permission as
|
|
143
|
+
* {@link SubtenantPermissions.TreeManage}: administering a sub-tenant through
|
|
144
|
+
* the management endpoints and operating as that sub-tenant are different
|
|
145
|
+
* powers, and granting the first should not hand over the second.
|
|
146
|
+
*/
|
|
147
|
+
readonly TreeSwitch: "subtenants:tree:switch";
|
|
148
|
+
};
|
|
149
|
+
type TenantPermission = (typeof TenantPermissions)[keyof typeof TenantPermissions];
|
|
150
|
+
type SubtenantPermission = (typeof SubtenantPermissions)[keyof typeof SubtenantPermissions];
|
|
151
|
+
|
|
152
|
+
export { AuthyonClient, AuthyonClientBuilder, AuthyonError, type AuthyonErrorAction, type AuthyonErrorBody, type AuthyonErrorCategory, type AuthyonErrorInterpretation, ErrorCodes, HttpAdapter, HttpLoggerOptions, type KnownErrorCode, type SubtenantPermission, SubtenantPermissions, type TenantPermission, TenantPermissions, TokenStorage, createDefaultStorage, createLocalStorage, createMemoryStorage };
|