@meistrari/auth-core 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -21,7 +21,7 @@ function isTokenExpired(token, expiresInMs) {
21
21
  return nowInSeconds > expirationTime;
22
22
  }
23
23
 
24
- const version = "3.1.0";
24
+ const version = "3.3.0";
25
25
 
26
26
  function parsedEnumValues(schema) {
27
27
  return Object.fromEntries(
@@ -548,7 +548,7 @@ class AdminService {
548
548
  async requestApplicationRoute(path, request = {}) {
549
549
  const base = this.apiUrl.endsWith("/") ? this.apiUrl : `${this.apiUrl}/`;
550
550
  const hasBody = request.body !== void 0;
551
- const { data, error } = await this.client.$fetch(`${base}${path}`, {
551
+ const response = await this.client.$fetch(`${base}${path}`, {
552
552
  method: request.method ?? "GET",
553
553
  params: request.params,
554
554
  query: request.query,
@@ -561,10 +561,19 @@ class AdminService {
561
561
  jsonParser: (text) => JSON.parse(text),
562
562
  throw: false
563
563
  });
564
+ const { data, error } = response;
564
565
  if (error)
565
566
  throw new ApplicationRouteError(error);
566
567
  return data;
567
568
  }
569
+ /**
570
+ * Calls an admin endpoint and returns its JSON body. The client is configured
571
+ * to throw on error responses, but better-auth types `$fetch` as returning
572
+ * `{ data, error }`; this narrows the result to the body it actually resolves to.
573
+ */
574
+ async fetchAdminRoute(path, options) {
575
+ return await this.client.$fetch(path, options);
576
+ }
568
577
  /**
569
578
  * Lists applications, optionally filtered by organization.
570
579
  *
@@ -610,6 +619,35 @@ class AdminService {
610
619
  body: options
611
620
  });
612
621
  }
622
+ /**
623
+ * Disables an application. Sign-ins, token exchanges, refreshes and
624
+ * client-credentials exchanges through it fail with `application_disabled`,
625
+ * which carries the deprecation notice. Disabling an already disabled
626
+ * application replaces its notice.
627
+ *
628
+ * @param applicationId - ID of the application to disable
629
+ * @param options - Optional deprecation notice
630
+ * @returns The updated application
631
+ */
632
+ async disableApplication(applicationId, options = {}) {
633
+ return await this.requestApplicationRoute("applications/:applicationId/disable", {
634
+ params: { applicationId },
635
+ method: "POST",
636
+ body: options
637
+ });
638
+ }
639
+ /**
640
+ * Enables a disabled application and clears its deprecation notice.
641
+ *
642
+ * @param applicationId - ID of the application to enable
643
+ * @returns The updated application
644
+ */
645
+ async enableApplication(applicationId) {
646
+ return await this.requestApplicationRoute("applications/:applicationId/enable", {
647
+ params: { applicationId },
648
+ method: "POST"
649
+ });
650
+ }
613
651
  /** Lists the role catalog for an application. */
614
652
  async listApplicationRoles(applicationId) {
615
653
  return await this.requestApplicationRoute(
@@ -708,7 +746,7 @@ class AdminService {
708
746
  * @returns The paginated organization listing
709
747
  */
710
748
  async listOrganizations(options = {}) {
711
- return await this.client.$fetch("/admin/organizations", {
749
+ return await this.fetchAdminRoute("/admin/organizations", {
712
750
  method: "GET",
713
751
  query: {
714
752
  ...options.limit !== void 0 ? { limit: options.limit } : {},
@@ -731,7 +769,7 @@ class AdminService {
731
769
  * @returns The paginated user listing
732
770
  */
733
771
  async listUsers(options = {}) {
734
- return await this.client.$fetch("/admin/users", {
772
+ return await this.fetchAdminRoute("/admin/users", {
735
773
  method: "GET",
736
774
  query: {
737
775
  ...options.limit !== void 0 ? { limit: options.limit } : {},
@@ -773,7 +811,7 @@ class AdminService {
773
811
  * @returns The organization
774
812
  */
775
813
  async getOrganization(options) {
776
- const { data } = await this.client.$fetch(`/admin/organizations/${encodeURIComponent(options.organizationId)}`, {
814
+ const { data } = await this.fetchAdminRoute(`/admin/organizations/${encodeURIComponent(options.organizationId)}`, {
777
815
  method: "GET",
778
816
  query: {
779
817
  ...options.sortBy ? { sortBy: options.sortBy } : {},
@@ -795,7 +833,7 @@ class AdminService {
795
833
  * @returns The organization summaries for the IDs that exist
796
834
  */
797
835
  async getOrganizationsSummary(options) {
798
- const { organizations } = await this.client.$fetch("/admin/organizations/summary", {
836
+ const { organizations } = await this.fetchAdminRoute("/admin/organizations/summary", {
799
837
  method: "GET",
800
838
  query: {
801
839
  ids: options.ids
@@ -812,7 +850,7 @@ class AdminService {
812
850
  * @returns The user access report
813
851
  */
814
852
  async getUserAccess(userId) {
815
- const { data } = await this.client.$fetch(`/admin/users/${encodeURIComponent(userId)}/access`, {
853
+ const { data } = await this.fetchAdminRoute(`/admin/users/${encodeURIComponent(userId)}/access`, {
816
854
  method: "GET"
817
855
  });
818
856
  return data;
@@ -826,7 +864,7 @@ class AdminService {
826
864
  * @returns The application listing with entitlement details
827
865
  */
828
866
  async listAdminApplications(options = {}) {
829
- return await this.client.$fetch("/admin/applications", {
867
+ return await this.fetchAdminRoute("/admin/applications", {
830
868
  method: "GET",
831
869
  query: {
832
870
  ...options.limit !== void 0 ? { limit: options.limit } : {},
@@ -848,7 +886,7 @@ class AdminService {
848
886
  * @returns The paginated application member listing
849
887
  */
850
888
  async listApplicationMembers(options) {
851
- return await this.client.$fetch(`/admin/applications/${encodeURIComponent(options.applicationId)}/members`, {
889
+ return await this.fetchAdminRoute(`/admin/applications/${encodeURIComponent(options.applicationId)}/members`, {
852
890
  method: "GET",
853
891
  query: {
854
892
  ...options.limit !== void 0 ? { limit: options.limit } : {},
@@ -865,7 +903,7 @@ class AdminService {
865
903
  * @returns The paginated organization member listing
866
904
  */
867
905
  async listOrganizationMembers(options) {
868
- return await this.client.$fetch(`/admin/organizations/${encodeURIComponent(options.organizationId)}/members`, {
906
+ return await this.fetchAdminRoute(`/admin/organizations/${encodeURIComponent(options.organizationId)}/members`, {
869
907
  method: "GET",
870
908
  query: {
871
909
  ...options.limit !== void 0 ? { limit: options.limit } : {},
@@ -881,6 +919,67 @@ class AdminService {
881
919
  }
882
920
  });
883
921
  }
922
+ /**
923
+ * Lists the applications available to one organization with the entitlement
924
+ * that decides their access and its rules. Global admins may list any
925
+ * organization; an organization admin only their own.
926
+ *
927
+ * @param options - Organization ID and search options
928
+ * @returns The organization's applications
929
+ */
930
+ async listOrganizationApplications(options) {
931
+ const { data } = await this.fetchAdminRoute(`/admin/organizations/${encodeURIComponent(options.organizationId)}/applications`, {
932
+ method: "GET",
933
+ query: {
934
+ ...options.search ? { search: options.search } : {}
935
+ }
936
+ });
937
+ return data;
938
+ }
939
+ /**
940
+ * Fetches one application of an organization with its rules and the roles
941
+ * those rules can grant.
942
+ *
943
+ * @param options - Organization and application IDs
944
+ * @returns The application access
945
+ */
946
+ async getOrganizationApplication(options) {
947
+ const { data } = await this.fetchAdminRoute(`/admin/organizations/${encodeURIComponent(options.organizationId)}/applications/${encodeURIComponent(options.applicationId)}`, {
948
+ method: "GET"
949
+ });
950
+ return data;
951
+ }
952
+ /**
953
+ * Replaces the rules of an organization's own entitlement to an application,
954
+ * in the order given, as a global admin. Global access is edited on the application.
955
+ *
956
+ * @param options - Organization and application IDs, and the rules in evaluation order
957
+ * @returns The updated application access
958
+ */
959
+ async setOrganizationApplicationRules(options) {
960
+ const { data } = await this.fetchAdminRoute(`/admin/organizations/${encodeURIComponent(options.organizationId)}/applications/${encodeURIComponent(options.applicationId)}/rules`, {
961
+ method: "POST",
962
+ body: { rules: options.rules }
963
+ });
964
+ return data;
965
+ }
966
+ /**
967
+ * Lists every active member of an organization with the rule that decides
968
+ * their access to an application and the role it grants.
969
+ *
970
+ * @param options - Organization and application IDs, pagination, and search options
971
+ * @returns The paginated member listing
972
+ */
973
+ async listOrganizationApplicationMembers(options) {
974
+ return await this.fetchAdminRoute(`/admin/organizations/${encodeURIComponent(options.organizationId)}/applications/${encodeURIComponent(options.applicationId)}/members`, {
975
+ method: "GET",
976
+ query: {
977
+ ...options.limit !== void 0 ? { limit: options.limit } : {},
978
+ ...options.offset !== void 0 ? { offset: options.offset } : {},
979
+ ...options.query ? { query: options.query } : {}
980
+ }
981
+ });
982
+ }
884
983
  /**
885
984
  * Lists teams for one organization. Global admins may list any organization;
886
985
  * an organization admin only their own.
@@ -889,7 +988,7 @@ class AdminService {
889
988
  * @returns The paginated organization team listing
890
989
  */
891
990
  async listOrganizationTeams(options) {
892
- return await this.client.$fetch(`/admin/organizations/${encodeURIComponent(options.organizationId)}/teams`, {
991
+ return await this.fetchAdminRoute(`/admin/organizations/${encodeURIComponent(options.organizationId)}/teams`, {
893
992
  method: "GET",
894
993
  query: {
895
994
  ...options.limit !== void 0 ? { limit: options.limit } : {},
@@ -907,7 +1006,7 @@ class AdminService {
907
1006
  * @returns The created team
908
1007
  */
909
1008
  async createOrganizationTeam(options) {
910
- const { data } = await this.client.$fetch(`/admin/organizations/${encodeURIComponent(options.organizationId)}/teams`, {
1009
+ const { data } = await this.fetchAdminRoute(`/admin/organizations/${encodeURIComponent(options.organizationId)}/teams`, {
911
1010
  method: "POST",
912
1011
  body: {
913
1012
  name: options.name,
@@ -924,7 +1023,7 @@ class AdminService {
924
1023
  * @returns The updated team
925
1024
  */
926
1025
  async updateOrganizationTeam(options) {
927
- const { data } = await this.client.$fetch(`/admin/organizations/${encodeURIComponent(options.organizationId)}/teams/update`, {
1026
+ const { data } = await this.fetchAdminRoute(`/admin/organizations/${encodeURIComponent(options.organizationId)}/teams/update`, {
928
1027
  method: "POST",
929
1028
  body: {
930
1029
  teamId: options.teamId,
@@ -1003,7 +1102,7 @@ class AdminService {
1003
1102
  * @returns The created organization
1004
1103
  */
1005
1104
  async createOrganization(options) {
1006
- const { data } = await this.client.$fetch("/admin/organizations", {
1105
+ const { data } = await this.fetchAdminRoute("/admin/organizations", {
1007
1106
  method: "POST",
1008
1107
  body: options
1009
1108
  });
@@ -1019,7 +1118,7 @@ class AdminService {
1019
1118
  * @returns The updated organization
1020
1119
  */
1021
1120
  async updateOrganization(options) {
1022
- const { data } = await this.client.$fetch("/admin/organizations/update", {
1121
+ const { data } = await this.fetchAdminRoute("/admin/organizations/update", {
1023
1122
  method: "POST",
1024
1123
  body: options
1025
1124
  });
@@ -1032,7 +1131,7 @@ class AdminService {
1032
1131
  * @returns The updated membership
1033
1132
  */
1034
1133
  async updateMemberRole(options) {
1035
- const { data } = await this.client.$fetch("/admin/organizations/members/update-role", {
1134
+ const { data } = await this.fetchAdminRoute("/admin/organizations/members/update-role", {
1036
1135
  method: "POST",
1037
1136
  body: options
1038
1137
  });
@@ -1045,7 +1144,7 @@ class AdminService {
1045
1144
  * @returns The removed membership
1046
1145
  */
1047
1146
  async removeMember(options) {
1048
- const { data } = await this.client.$fetch("/admin/organizations/members/remove", {
1147
+ const { data } = await this.fetchAdminRoute("/admin/organizations/members/remove", {
1049
1148
  method: "POST",
1050
1149
  body: options
1051
1150
  });
@@ -1144,7 +1243,7 @@ class AdminService {
1144
1243
  * @returns Per-email invite outcomes
1145
1244
  */
1146
1245
  async inviteOrganizationUsers(options) {
1147
- const { data } = await this.client.$fetch("/admin/organizations/users", {
1246
+ const { data } = await this.fetchAdminRoute("/admin/organizations/users", {
1148
1247
  method: "POST",
1149
1248
  body: { ...options, sendInvite: options.sendInvite ?? true }
1150
1249
  });
@@ -1202,6 +1301,41 @@ class ApplicationSessionAssuranceError extends ApplicationError {
1202
1301
  this.validStepUps = payload?.validStepUps ?? [];
1203
1302
  }
1204
1303
  }
1304
+ function parseApplicationDisabledPayload(error) {
1305
+ const seen = /* @__PURE__ */ new Set();
1306
+ let current = error;
1307
+ while (current && typeof current === "object" && !seen.has(current)) {
1308
+ seen.add(current);
1309
+ const candidate = current;
1310
+ for (const details of [candidate.details, candidate.error?.details]) {
1311
+ if (!details || typeof details !== "object") {
1312
+ continue;
1313
+ }
1314
+ const payload = details;
1315
+ if (payload.code === "application_disabled" && typeof payload.applicationId === "string") {
1316
+ return {
1317
+ applicationId: payload.applicationId,
1318
+ applicationName: typeof payload.applicationName === "string" ? payload.applicationName : "",
1319
+ deprecationNotice: typeof payload.deprecationNotice === "string" ? payload.deprecationNotice : null
1320
+ };
1321
+ }
1322
+ }
1323
+ current = candidate.cause;
1324
+ }
1325
+ return null;
1326
+ }
1327
+ class ApplicationDisabledError extends ApplicationError {
1328
+ applicationId;
1329
+ applicationName;
1330
+ deprecationNotice;
1331
+ constructor(payload = null, options) {
1332
+ super(payload?.deprecationNotice || `${payload?.applicationName || "The application"} has been disabled`, options);
1333
+ this.code = "APPLICATION_DISABLED";
1334
+ this.applicationId = payload?.applicationId ?? null;
1335
+ this.applicationName = payload?.applicationName || null;
1336
+ this.deprecationNotice = payload?.deprecationNotice ?? null;
1337
+ }
1338
+ }
1205
1339
  class UserNotLoggedInError extends ApplicationError {
1206
1340
  constructor(message, options) {
1207
1341
  super(message, options);
@@ -1433,8 +1567,17 @@ function parseErrorMessage(error) {
1433
1567
  const nested = typeof candidateError.error === "string" ? candidateError.error : candidateError.error?.message;
1434
1568
  return candidateError.message ?? nested ?? "Failed to exchange device code for tokens";
1435
1569
  }
1570
+ function throwIfApplicationDisabled(error) {
1571
+ const payload = parseApplicationDisabledPayload(error);
1572
+ if (payload) {
1573
+ throw new ApplicationDisabledError(payload, { cause: error });
1574
+ }
1575
+ }
1436
1576
  function throwDeviceGrantError(error) {
1437
1577
  const code = parseErrorCode(error);
1578
+ if (code === "application_disabled") {
1579
+ throw new ApplicationDisabledError(parseApplicationDisabledPayload(error), { cause: error });
1580
+ }
1438
1581
  if (code === "authorization_pending") {
1439
1582
  throw new DeviceAuthorizationPendingError({ cause: error });
1440
1583
  }
@@ -1512,6 +1655,7 @@ class ApplicationService {
1512
1655
  try {
1513
1656
  const response = await this.client.applications.startAuthorizationFlow(applicationId, redirectUri, codeChallenge, organizationId);
1514
1657
  if (!response.data) {
1658
+ throwIfApplicationDisabled(response.error);
1515
1659
  const payload = parseSessionAssuranceRequiredPayload(response.error);
1516
1660
  if (payload) {
1517
1661
  throw new SessionAssuranceRequiredError(payload, { cause: response.error });
@@ -1520,9 +1664,10 @@ class ApplicationService {
1520
1664
  }
1521
1665
  return response.data;
1522
1666
  } catch (error) {
1523
- if (error instanceof SessionAssuranceRequiredError) {
1667
+ if (error instanceof SessionAssuranceRequiredError || error instanceof ApplicationDisabledError) {
1524
1668
  throw error;
1525
1669
  }
1670
+ throwIfApplicationDisabled(error);
1526
1671
  const payload = parseSessionAssuranceRequiredPayload(error);
1527
1672
  if (payload) {
1528
1673
  throw new SessionAssuranceRequiredError(payload, { cause: error });
@@ -1605,6 +1750,7 @@ class ApplicationService {
1605
1750
  * @throws {DeviceCodeExpiredError} The device code has expired
1606
1751
  * @throws {DeviceTransientServerError} Transient server error — safe to retry
1607
1752
  * @throws {ApplicationSessionAssuranceError} Assurance policy changed or is no longer met — start a new flow
1753
+ * @throws {ApplicationDisabledError} The requester or target application is disabled — stop polling
1608
1754
  */
1609
1755
  async exchangeDeviceCodeForTokens(deviceCode) {
1610
1756
  try {
@@ -1629,8 +1775,12 @@ class ApplicationService {
1629
1775
  * @returns A short-lived access token with service-account and organization details
1630
1776
  */
1631
1777
  async exchangeClientCredentialsForToken(credentials) {
1632
- const response = await this.client.applications.exchangeClientCredentialsForToken(credentials);
1778
+ const response = await this.client.applications.exchangeClientCredentialsForToken(credentials).catch((error) => {
1779
+ throwIfApplicationDisabled(error);
1780
+ throw error;
1781
+ });
1633
1782
  if (!response.data) {
1783
+ throwIfApplicationDisabled(response.error);
1634
1784
  throw new Error("No data returned from the API", { cause: response.error });
1635
1785
  }
1636
1786
  return response.data;
@@ -1642,8 +1792,12 @@ class ApplicationService {
1642
1792
  * @param codeVerifier - The code verifier to use for the authorization flow
1643
1793
  */
1644
1794
  async completeAuthorizationFlow(code, codeVerifier) {
1645
- const response = await this.client.applications.completeAuthorizationFlow(code, codeVerifier);
1795
+ const response = await this.client.applications.completeAuthorizationFlow(code, codeVerifier).catch((error) => {
1796
+ throwIfApplicationDisabled(error);
1797
+ throw error;
1798
+ });
1646
1799
  if (!response.data) {
1800
+ throwIfApplicationDisabled(response.error);
1647
1801
  throw new Error("No data returned from the API", { cause: response.error });
1648
1802
  }
1649
1803
  return response.data;
@@ -1653,11 +1807,15 @@ class ApplicationService {
1653
1807
  *
1654
1808
  * @param refreshToken - The refresh token to use for the authentication token refresh
1655
1809
  * @throws {RefreshTokenExpiredError} When the refresh token has expired or is invalid
1810
+ * @throws {ApplicationDisabledError} When the application has been disabled
1656
1811
  * @throws {ApplicationError} For other API errors
1657
1812
  */
1658
1813
  async refreshAccessToken(refreshToken) {
1659
1814
  const handleRefreshError = (error) => {
1660
1815
  const status = error?.status;
1816
+ if (parseErrorCode(error) === "application_disabled") {
1817
+ throw new ApplicationDisabledError(parseApplicationDisabledPayload(error), { cause: error, status });
1818
+ }
1661
1819
  if (status === 401 || status === 404) {
1662
1820
  throw new RefreshTokenExpiredError({ cause: error, status });
1663
1821
  }
@@ -1866,6 +2024,7 @@ class OrganizationService {
1866
2024
  applicationName: responseWithContext.applicationName ?? null,
1867
2025
  homeUrl: responseWithContext.homeUrl ?? null,
1868
2026
  inviteAcceptedUrl: responseWithContext.inviteAcceptedUrl ?? null,
2027
+ // better-auth types the role as a plain string; the organization plugin only stores configured roles.
1869
2028
  member: response.member
1870
2029
  };
1871
2030
  }
@@ -2643,4 +2802,4 @@ function extractTokenPayload(token) {
2643
2802
  return payload;
2644
2803
  }
2645
2804
 
2646
- export { AAL, AALBrand, AALs, ACR, ACRBrand, ACRs, AMR, AMRBrand, AMRs, ApiKeyMetadata, ApplicationConfig, ApplicationError, ApplicationSessionAssuranceConfig, ApplicationSessionAssuranceError, ApplicationTokenStorage, AuthClient, AuthMethod, AuthMethodBrand, AuthMethods, AuthorizationFlowError, DEFAULT_APPLICATION_CONFIG, DEFAULT_JWT_ASSURANCE, DEFAULT_SESSION_ASSURANCE_CONFIG, DeviceAccessDeniedError, DeviceAuthorizationPendingError, DeviceAuthorizationSlowDownError, DeviceCodeExpiredError, DeviceTransientServerError, EmailOTPService, EmailRequired, InvalidCallbackURL, InvalidSocialProvider, JWTPayload, JWTPayloadAssurance, JWTPayloadUser, JWTPayloadWorkspace, LOGIN_METHODS, OAuthStepUpProvider, RefreshTokenExpiredError, Roles, SENSITIVE_ACTION_ASSURANCE_POLICY, STEP_UP_MAX_AGE_SECONDS, SessionAssurancePolicy, SessionAssuranceRequiredError, SessionAssuranceRequiredPayload, SessionAssuranceService, StepUpOption, UserNotLoggedInError, ac, assuranceFields, canStepUpWithAmr, extractTokenPayload, getAalRank, invitationAdditionalFields, isAssuranceSufficientForPolicy, isSessionAssuranceSufficient, isTokenExpired, memberAdditionalFields, mergeStepUpAssurance, normalizeAmr, organizationAdditionalFields, parseSessionAssuranceRequiredPayload, rolesAccessControl, sessionAssurancePluginClient, userAdditionalFields, validateToken };
2805
+ export { AAL, AALBrand, AALs, ACR, ACRBrand, ACRs, AMR, AMRBrand, AMRs, ApiKeyMetadata, ApplicationConfig, ApplicationDisabledError, ApplicationError, ApplicationSessionAssuranceConfig, ApplicationSessionAssuranceError, ApplicationTokenStorage, AuthClient, AuthMethod, AuthMethodBrand, AuthMethods, AuthorizationFlowError, DEFAULT_APPLICATION_CONFIG, DEFAULT_JWT_ASSURANCE, DEFAULT_SESSION_ASSURANCE_CONFIG, DeviceAccessDeniedError, DeviceAuthorizationPendingError, DeviceAuthorizationSlowDownError, DeviceCodeExpiredError, DeviceTransientServerError, EmailOTPService, EmailRequired, InvalidCallbackURL, InvalidSocialProvider, JWTPayload, JWTPayloadAssurance, JWTPayloadUser, JWTPayloadWorkspace, LOGIN_METHODS, OAuthStepUpProvider, RefreshTokenExpiredError, Roles, SENSITIVE_ACTION_ASSURANCE_POLICY, STEP_UP_MAX_AGE_SECONDS, SessionAssurancePolicy, SessionAssuranceRequiredError, SessionAssuranceRequiredPayload, SessionAssuranceService, StepUpOption, UserNotLoggedInError, ac, assuranceFields, canStepUpWithAmr, extractTokenPayload, getAalRank, invitationAdditionalFields, isAssuranceSufficientForPolicy, isSessionAssuranceSufficient, isTokenExpired, memberAdditionalFields, mergeStepUpAssurance, normalizeAmr, organizationAdditionalFields, parseApplicationDisabledPayload, parseSessionAssuranceRequiredPayload, rolesAccessControl, sessionAssurancePluginClient, userAdditionalFields, validateToken };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/auth-core",
3
- "version": "3.1.0",
3
+ "version": "3.3.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {