Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/common/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## @esri/hub-common [20.11.1](https://github.com/Esri/hub.js/compare/@esri/[email protected]...@esri/[email protected]) (2025-09-26)


### Bug Fixes

* only set the layout to a template if _layoutSetup is passd ([#2021](https://github.com/Esri/hub.js/issues/2021)) ([e02888f](https://github.com/Esri/hub.js/commit/e02888fc240366a22c4a0c328607c731344fb4e0))

# @esri/hub-common [20.11.0](https://github.com/Esri/hub.js/compare/@esri/[email protected]...@esri/[email protected]) (2025-09-25)


Expand Down
2 changes: 1 addition & 1 deletion packages/common/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@esri/hub-common",
"version": "20.11.0",
"version": "20.11.1",
"description": "Common TypeScript types and utility functions for @esri/hub.js.",
"main": "dist/node/index.js",
"module": "dist/esm/index.js",
Expand Down
19 changes: 19 additions & 0 deletions packages/common/src/access/compareAccess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { AccessLevel } from "../core";

/**
* Simple access comparison.
* If access1 is more permissive than access2, returns "private" (downgraded); otherwise returns access1 unchanged.
* Order: private < shared < org < public
* Currently used to ensure that a Hub Assistant's access level is not more permissive than the site entity.
* @param access1 Candidate access level to validate.
* @param access2 Reference access level to compare against.
* @returns The resulting (possibly downgraded) access level.
*/
export function compareAccess(
access1: AccessLevel,
access2: AccessLevel
): AccessLevel {
const order: AccessLevel[] = ["private", "shared", "org", "public"];
const rank = (lvl: AccessLevel): number => order.indexOf(lvl);
return rank(access1) > rank(access2) ? "private" : access1;
}
1 change: 1 addition & 0 deletions packages/common/src/access/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from "./can-edit-item";
export * from "./can-edit-site-content";
export * from "./can-edit-site";
export * from "./has-base-priv";
export * from "./compareAccess";
4 changes: 3 additions & 1 deletion packages/common/src/assistants/IHubAssistant.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { AccessLevel } from "../core";

/**
* Interface representing a Hub Assistant.
* This interface defines the structure of an assistant in the hub, including its properties and workflows.
Expand All @@ -11,7 +13,7 @@ export interface IHubAssistant {
* Access level for the assistant.
* This can be public, org, or private.
*/
access?: string;
access?: AccessLevel;
/**
* Assistant access groups when not public.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ export const HubItemEntitySchema: IAsyncConfigurationSchema = {
personality: { type: "string" },
description: { type: "string" },
location: { type: "string" },
examplePrompts: { type: "array", items: { type: "string" } },
examplePrompts: {
type: "array",
items: { type: "string" },
},
workflows: {
type: "array",
items: {
Expand Down
9 changes: 5 additions & 4 deletions packages/common/src/sites/_internal/SiteUiSchemaAssistant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ export const buildUiSchema = async (
itemType: "assistant",
orgName: context.portal.name,
accessOptions: {
canSetAccessToPublic: true,
canSetAccessToOrg: true,
canSetAccessToPrivate: true,
canSetAccessToPublic: options.access === "public",
canSetAccessToOrg:
options.access === "org" || options.access === "public",
canSetAccessToPrivate: true, // always allow private access
},
},
},
Expand Down Expand Up @@ -173,7 +174,7 @@ export const buildUiSchema = async (
},
elements: [
{
label: "Assistant Personality",
label: `{{${i18nScope}.assistant.fields.personality.label:translate}}`,
scope: "/properties/assistant/properties/personality",
type: "Control",
options: {
Expand Down
8 changes: 8 additions & 0 deletions packages/common/src/sites/_internal/computeProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { IHubSite } from "../../core/types/IHubSite";
import { computeItemProps } from "../../core/_internal/computeItemProps";
import { computeLinks } from "./computeLinks";
import { getCatalogFromSiteModel } from "../get-catalog-from-site-model";
import { getProp } from "../../objects";
import { compareAccess } from "../../access";

/**
* Given a model and a site, set various computed properties that can't be directly mapped
Expand Down Expand Up @@ -53,6 +55,12 @@ export function computeProps(

// Determine if the site is still using the legacy v1 catalog
site.isCatalogV1Enabled = !!model.data.catalog;

// Update the hub assistant's access level based on the site's access level if needed
// Cannot have a sites access level be private while the hub assistant level is org or public
if (getProp(site, "assistant.access")) {
site.assistant.access = compareAccess(site.assistant.access, site.access);
}

// cast b/c this takes a partial but returns a full site
return site as IHubSite;
Expand Down
46 changes: 46 additions & 0 deletions packages/common/test/access/compareAccess.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { compareAccess } from "../../src/access/compareAccess";
import { AccessLevel } from "../../src/core";

describe("compareAccess", function () {
it("returns access1 when both levels are equal", function () {
const levels: AccessLevel[] = ["private", "shared", "org", "public"];
levels.forEach((level) => {
expect(compareAccess(level, level)).toBe(level);
});
});

it("returns access1 when access1 is less or equally permissive than access2", function () {
const cases: Array<[AccessLevel, AccessLevel]> = [
["private", "shared"],
["private", "org"],
["private", "public"],
["shared", "org"],
["shared", "public"],
["org", "public"],
];
cases.forEach(([a, b]) => {
expect(compareAccess(a, b)).toBe(a);
});
});

it("returns 'private' when access1 is more permissive than access2", function () {
const downgradeCases: Array<[AccessLevel, AccessLevel]> = [
["shared", "private"],
["org", "private"],
["org", "shared"],
["public", "private"],
["public", "shared"],
["public", "org"],
];
downgradeCases.forEach(([a, b]) => {
expect(compareAccess(a, b)).toBe("private");
});
});

it("does not downgrade when access1 is already 'private'", function () {
const targets: AccessLevel[] = ["private", "shared", "org", "public"];
targets.forEach((target) => {
expect(compareAccess("private", target)).toBe("private");
});
});
});
1 change: 1 addition & 0 deletions packages/common/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,7 @@ describe("index", () => {
"fetchCategoriesUiSchemaElement",
"btoa",
"atob",
"compareAccess",
];

const NODE_EXPORTS = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { MOCK_CONTEXT } from "../../mocks/mock-auth";

describe("buildUiSchema: site assistant", () => {
it("returns the full site assistant uiSchema", async () => {
const uiSchema = await buildUiSchema("some.scope", {} as any, MOCK_CONTEXT);
const uiSchema = await buildUiSchema(
"some.scope",
{ access: "public" } as any,
MOCK_CONTEXT
);
expect(uiSchema).toEqual({
type: "Layout",
elements: [
Expand Down Expand Up @@ -160,7 +164,7 @@ describe("buildUiSchema: site assistant", () => {
},
elements: [
{
label: "Assistant Personality",
label: `{{some.scope.assistant.fields.personality.label:translate}}`,
scope: "/properties/assistant/properties/personality",
type: "Control",
options: {
Expand Down
42 changes: 42 additions & 0 deletions packages/common/test/sites/_internal/computeProps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,46 @@ describe("sites: computeProps:", () => {
const chk = computeProps(model, init, requestOptions);
expect(chk.isCatalogV1Enabled).toBe(false);
});
it("downgrades assistant access when more permissive than site", () => {
const model: IModel = {
item: {
id: "abc",
type: "Hub Site Application",
access: "private",
created: Date.now(),
modified: Date.now(),
} as any,
data: {},
} as unknown as IModel;

const init: Partial<IHubSite> = {
id: "abc",
access: "org",
assistant: { access: "public" } as any,
};

const chk = computeProps(model, init, requestOptions);
expect(chk.assistant.access).toBe("private");
});
it("does not downgrade assistant access when it is less permissive than site", () => {
const model: IModel = {
item: {
id: "xyz",
type: "Hub Site Application",
access: "public",
created: Date.now(),
modified: Date.now(),
} as any,
data: {},
} as unknown as IModel;

const init: Partial<IHubSite> = {
id: "xyz",
access: "public",
assistant: { access: "org" } as any,
};

const chk = computeProps(model, init, requestOptions);
expect(chk.assistant.access).toBe("org");
});
});