diff --git a/README.md b/README.md
index 69fd7a4..082abd5 100644
--- a/README.md
+++ b/README.md
@@ -382,11 +382,11 @@ mutation {
mutation {
createTenant(input: {
name: "factory-a",
- route: "factory-a"
+ alias: "factory-a"
}) {
id
name
- route
+ alias
status
}
}
@@ -395,12 +395,14 @@ mutation {
createEntity(input: {
profileId: "client-profile-id",
name: "meter-001",
+ alias: "meter-001",
attributes: {
serial_no: "WM-001"
}
}) {
id
kind
+ alias
profileId
profileVersionId
attributes
@@ -411,6 +413,7 @@ mutation {
createResource(input: {
kind: "channel",
name: "telemetry",
+ alias: "telemetry",
attributes: {
topic: "telemetry"
}
@@ -418,6 +421,7 @@ mutation {
id
kind
name
+ alias
attributes
}
}
@@ -792,7 +796,7 @@ active | inactive | frozen | deleted
| ---------------- | -------------------- |
| domain `id` | `tenants.id` |
| domain `name` | `tenants.name` |
-| `route` | `tenants.route` |
+| domain `route` | `tenants.alias` |
| `metadata` | `tenants.attributes` |
| `tags` | `tenants.tags` |
| `enabled` | `status = active` |
diff --git a/apidocs/grpc-reference.md b/apidocs/grpc-reference.md
index 42c049d..78b6b88 100644
--- a/apidocs/grpc-reference.md
+++ b/apidocs/grpc-reference.md
@@ -9,11 +9,14 @@
- [CheckRequest](#atom-v1-CheckRequest)
- [CheckRequest.ContextEntry](#atom-v1-CheckRequest-ContextEntry)
- [CheckResponse](#atom-v1-CheckResponse)
+ - [ResolveAliasRequest](#atom-v1-ResolveAliasRequest)
+ - [ResolveAliasResponse](#atom-v1-ResolveAliasResponse)
- [ResolveCertificateRequest](#atom-v1-ResolveCertificateRequest)
- [ResolveCertificateResponse](#atom-v1-ResolveCertificateResponse)
- [RevokeEntityCertificatesRequest](#atom-v1-RevokeEntityCertificatesRequest)
- [RevokeEntityCertificatesResponse](#atom-v1-RevokeEntityCertificatesResponse)
+ - [AliasService](#atom-v1-AliasService)
- [AuthService](#atom-v1-AuthService)
- [AuthzService](#atom-v1-AuthzService)
- [CertificateService](#atom-v1-CertificateService)
@@ -113,6 +116,41 @@
+
+
+### ResolveAliasRequest
+
+
+
+| Field | Type | Label | Description |
+| ----- | ---- | ----- | ----------- |
+| tenant_id | [string](#string) | | Tenant selector — exactly one of tenant_id, tenant_alias, or global must be set. tenant_alias is the case-folded tenant slug. |
+| tenant_alias | [string](#string) | | |
+| object_kind | [string](#string) | | Which table the object alias addresses: "entity" (clients/devices) or "resource" (channels). Other values are rejected. Generic on purpose — no domain/channel vocabulary. |
+| object_alias | [string](#string) | | The object's alias slug, unique within the tenant. |
+| global | [bool](#bool) | | Resolve an entity or resource whose tenant_id is NULL. |
+
+
+
+
+
+
+
+
+### ResolveAliasResponse
+
+
+
+| Field | Type | Label | Description |
+| ----- | ---- | ----- | ----------- |
+| tenant_id | [string](#string) | | empty string for global objects |
+| object_id | [string](#string) | | |
+
+
+
+
+
+
### ResolveCertificateRequest
@@ -184,6 +222,20 @@
+
+
+### AliasService
+AliasService resolves human-friendly alias slugs to canonical UUIDs.
+Atom owns the alias registry and its uniqueness; callers (e.g. a message
+broker) resolve an alias once, cache the UUID, then authorize by UUID via
+AuthzService.Check. Resolution is capability-neutral — it reveals only the
+UUIDs; the Check call is the authorization gate.
+
+| Method Name | Request Type | Response Type | Description |
+| ----------- | ------------ | ------------- | ------------|
+| ResolveAlias | [ResolveAliasRequest](#atom-v1-ResolveAliasRequest) | [ResolveAliasResponse](#atom-v1-ResolveAliasResponse) | |
+
+
### AuthService
diff --git a/apidocs/grpc.md b/apidocs/grpc.md
index d61bf49..a5c7600 100644
--- a/apidocs/grpc.md
+++ b/apidocs/grpc.md
@@ -1,6 +1,6 @@
# gRPC API Reference
-Atom exposes three gRPC services on port **8081** by default, configurable with `GRPC_ADDR`.
+Atom exposes four gRPC services on port **8081** by default, configurable with `GRPC_ADDR`.
The proto source lives at [`proto/atom/v1/atom.proto`](../proto/atom/v1/atom.proto). The generated proto reference lives at [`apidocs/grpc-reference.md`](./grpc-reference.md) and should be regenerated only when the proto changes.
@@ -18,7 +18,7 @@ Runtime services should call Atom over the service network. The default containe
### Authentication Metadata
-`AuthzService.Check`, `CertificateService.ResolveCertificate`, and `CertificateService.RevokeEntityCertificates` require gRPC metadata:
+`AuthzService.Check`, `AliasService.ResolveAlias`, `CertificateService.ResolveCertificate`, and `CertificateService.RevokeEntityCertificates` require gRPC metadata:
```text
authorization: Bearer
@@ -149,6 +149,58 @@ grpcurl -plaintext \
---
+### `atom.v1.AliasService`
+
+Alias resolution converts human-friendly tenant/entity/resource handles into canonical UUIDs. Resolution does not grant access; callers must authorize the returned object UUID separately with `AuthzService.Check`.
+
+#### `ResolveAlias`
+
+```text
+rpc ResolveAlias(ResolveAliasRequest) returns (ResolveAliasResponse)
+```
+
+Requires `authorization: Bearer ` metadata.
+
+Exactly one tenant selector is required:
+
+- `tenant_id` for a tenant UUID;
+- `tenant_alias` for a case-insensitive tenant alias;
+- `global = true` for an entity or resource whose `tenant_id` is null.
+
+`object_kind` must be exactly `entity` or `resource` (case-insensitive). Other values return `INVALID_ARGUMENT`.
+
+**Request: `ResolveAliasRequest`**
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `tenant_id` | `string` UUID | conditional | Tenant UUID selector. |
+| `tenant_alias` | `string` | conditional | Tenant alias selector. |
+| `global` | `bool` | conditional | Select the global null-tenant namespace. |
+| `object_kind` | `string` | yes | `entity` or `resource`. |
+| `object_alias` | `string` | yes | Object alias within the selected namespace. |
+
+**Response: `ResolveAliasResponse`**
+
+| Field | Type | Description |
+|---|---|---|
+| `tenant_id` | `string` UUID | Resolved tenant; empty for global objects. |
+| `object_id` | `string` UUID | Resolved entity or resource UUID. |
+
+**Example**
+
+```bash
+grpcurl -plaintext \
+ -H 'authorization: Bearer '"$ATOM_TOKEN" \
+ -d '{
+ "tenant_alias": "factory-a",
+ "object_kind": "resource",
+ "object_alias": "telemetry"
+ }' \
+ atom:8081 atom.v1.AliasService/ResolveAlias
+```
+
+---
+
### `atom.v1.CertificateService`
Certificate runtime lookup and entity-wide certificate revocation for services that terminate mTLS outside Atom.
diff --git a/apidocs/openapi.yaml b/apidocs/openapi.yaml
index 5814183..bbe13c7 100644
--- a/apidocs/openapi.yaml
+++ b/apidocs/openapi.yaml
@@ -205,6 +205,9 @@ components:
$ref: '#/components/schemas/EntityKind'
name:
type: string
+ alias:
+ type: string
+ nullable: true
tenant_id:
type: string
format: uuid
@@ -252,6 +255,8 @@ components:
name:
type: string
example: sensor-01
+ alias:
+ type: string
tenant_id:
type: string
format: uuid
@@ -263,6 +268,9 @@ components:
properties:
name:
type: string
+ alias:
+ type: string
+ nullable: true
status:
$ref: '#/components/schemas/EntityStatus'
attributes:
@@ -597,6 +605,9 @@ components:
name:
type: string
nullable: true
+ alias:
+ type: string
+ nullable: true
tenant_id:
type: string
format: uuid
@@ -625,6 +636,8 @@ components:
example: channel
name:
type: string
+ alias:
+ type: string
tenant_id:
type: string
format: uuid
@@ -639,6 +652,9 @@ components:
properties:
name:
type: string
+ alias:
+ type: string
+ nullable: true
attributes:
type: object
@@ -899,7 +915,7 @@ components:
format: uuid
name:
type: string
- route:
+ alias:
type: string
nullable: true
status:
@@ -933,8 +949,9 @@ components:
name:
type: string
example: factory-1
- route:
+ alias:
type: string
+ nullable: true
tags:
type: array
items:
@@ -947,8 +964,9 @@ components:
properties:
name:
type: string
- route:
+ alias:
type: string
+ nullable: true
tags:
type: array
items:
@@ -2384,7 +2402,7 @@ paths:
in: query
schema:
type: string
- - name: route
+ - name: alias
in: query
schema:
type: string
@@ -2425,7 +2443,7 @@ paths:
$ref: '#/components/schemas/CreateTenant'
example:
name: factory-1
- route: factory-1
+ alias: factory-1
tags: [pilot]
attributes:
magistrala:
diff --git a/app/components/app-shell/tenant-switcher.tsx b/app/components/app-shell/tenant-switcher.tsx
index 9f7eb70..18b4b09 100644
--- a/app/components/app-shell/tenant-switcher.tsx
+++ b/app/components/app-shell/tenant-switcher.tsx
@@ -32,13 +32,13 @@ const GLOBAL_OPTION: TenantSelection = { id: GLOBAL_TENANT, name: "Global" };
const TENANTS_QUERY = `
query TenantSwitcher {
tenants(limit: 100, offset: 0) {
- items { id name route }
+ items { id name alias }
}
}
`;
type TenantsData = {
- tenants: { items: { id: string; name: string; route: string | null }[] };
+ tenants: { items: { id: string; name: string; alias: string | null }[] };
};
export function TenantSwitcher() {
diff --git a/app/components/crud/table/initial-values.test.ts b/app/components/crud/table/initial-values.test.ts
new file mode 100644
index 0000000..e4a4e46
--- /dev/null
+++ b/app/components/crud/table/initial-values.test.ts
@@ -0,0 +1,34 @@
+import { describe, expect, it } from "vitest";
+import {
+ entityFormInitialValues,
+ resourceFormInitialValues,
+} from "@/components/crud/table/initial-values";
+
+describe("alias form initial values", () => {
+ it("loads entity aliases for editing", () => {
+ const values = entityFormInitialValues({
+ id: "entity-1",
+ name: "Sensor",
+ alias: "sensor-01",
+ kind: "device",
+ });
+
+ expect(values.alias).toBe("sensor-01");
+ });
+
+ it("loads resource aliases for editing", () => {
+ const values = resourceFormInitialValues({
+ id: "resource-1",
+ kind: "resource:channel",
+ name: "Telemetry",
+ alias: "telemetry",
+ });
+
+ expect(values.alias).toBe("telemetry");
+ });
+
+ it("uses an empty alias when the row has none", () => {
+ expect(entityFormInitialValues({ id: "entity-1" }).alias).toBe("");
+ expect(resourceFormInitialValues({ id: "resource-1" }).alias).toBe("");
+ });
+});
diff --git a/app/components/crud/table/initial-values.ts b/app/components/crud/table/initial-values.ts
index 3a7618c..390f2dd 100644
--- a/app/components/crud/table/initial-values.ts
+++ b/app/components/crud/table/initial-values.ts
@@ -43,6 +43,7 @@ export function resourceFormInitialValues(row: Row): ResourceFormInitialValues {
id: String(row.id),
kind: typeof row.kind === "string" ? row.kind : "",
name: typeof row.name === "string" ? row.name : "",
+ alias: typeof row.alias === "string" ? row.alias : "",
tenantId: typeof row.tenantId === "string" ? row.tenantId : "",
ownerId: typeof row.ownerId === "string" ? row.ownerId : "",
attributes:
@@ -87,6 +88,7 @@ export function entityFormInitialValues(row: Row): EntityFormInitialValues {
return {
id: String(row.id),
name: typeof row.name === "string" ? row.name : "",
+ alias: typeof row.alias === "string" ? row.alias : "",
kind: (ENTITY_KINDS as readonly string[]).includes(rawKind)
? (rawKind as EntityFormInitialValues["kind"])
: "human",
@@ -105,7 +107,7 @@ export function tenantFormInitialValues(row: Row): TenantFormInitialValues {
return {
id: String(row.id),
name: typeof row.name === "string" ? row.name : "",
- route: typeof row.route === "string" ? row.route : "",
+ alias: typeof row.alias === "string" ? row.alias : "",
tags: Array.isArray(row.tags) ? row.tags.map((tag) => String(tag)) : [],
attributes:
row.attributes && typeof row.attributes === "object"
diff --git a/app/components/endpoints/api-endpoints-table.tsx b/app/components/endpoints/api-endpoints-table.tsx
index c5bb647..97ab7af 100644
--- a/app/components/endpoints/api-endpoints-table.tsx
+++ b/app/components/endpoints/api-endpoints-table.tsx
@@ -176,14 +176,14 @@ const ENDPOINT_PRESETS: EndpointPreset[] = [
values: {
key: "create_tenant",
name: "Create Tenant",
- description: "Creates a new tenant with a name and optional route.",
+ description: "Creates a new tenant with a name and optional alias.",
method: "POST",
path: "/api/custom/tenants",
operationKind: "mutation",
- graphql: `mutation CreateTenant($input: CreateTenantInput!) {\n createTenant(input: $input) {\n id\n name\n route\n status\n createdAt\n }\n}`,
+ graphql: `mutation CreateTenant($input: CreateTenantInput!) {\n createTenant(input: $input) {\n id\n name\n alias\n status\n createdAt\n }\n}`,
authMode: "caller_context",
variablesMapping: JSON.stringify(
- { "input.name": "$body.name", "input.route": "$body.route" },
+ { "input.name": "$body.name", "input.alias": "$body.alias" },
null,
2,
),
@@ -193,7 +193,7 @@ const ENDPOINT_PRESETS: EndpointPreset[] = [
required: ["name"],
properties: {
name: { type: "string" },
- route: { type: "string" },
+ alias: { type: "string" },
},
},
null,
@@ -209,17 +209,17 @@ const ENDPOINT_PRESETS: EndpointPreset[] = [
values: {
key: "update_tenant",
name: "Update Tenant",
- description: "Updates a tenant's name or route.",
+ description: "Updates a tenant's name or alias.",
method: "PATCH",
path: "/api/custom/tenants/update",
operationKind: "mutation",
- graphql: `mutation UpdateTenant($id: ID!, $input: UpdateTenantInput!) {\n updateTenant(id: $id, input: $input) {\n id\n name\n route\n status\n updatedAt\n }\n}`,
+ graphql: `mutation UpdateTenant($id: ID!, $input: UpdateTenantInput!) {\n updateTenant(id: $id, input: $input) {\n id\n name\n alias\n status\n updatedAt\n }\n}`,
authMode: "caller_context",
variablesMapping: JSON.stringify(
{
id: "$query.id",
"input.name": "$body.name",
- "input.route": "$body.route",
+ "input.alias": "$body.alias",
},
null,
2,
@@ -229,7 +229,7 @@ const ENDPOINT_PRESETS: EndpointPreset[] = [
type: "object",
properties: {
name: { type: "string" },
- route: { type: "string" },
+ alias: { type: "string" },
},
},
null,
@@ -245,15 +245,17 @@ const ENDPOINT_PRESETS: EndpointPreset[] = [
values: {
key: "create_entity",
name: "Create Entity",
- description: "Creates an entity with a name, kind, and optional tenant.",
+ description:
+ "Creates an entity with a name, kind, optional alias, and optional tenant.",
method: "POST",
path: "/api/custom/entities",
operationKind: "mutation",
- graphql: `mutation CreateEntity($input: CreateEntityInput!) {\n createEntity(input: $input) {\n id\n name\n kind\n status\n createdAt\n }\n}`,
+ graphql: `mutation CreateEntity($input: CreateEntityInput!) {\n createEntity(input: $input) {\n id\n name\n alias\n kind\n status\n createdAt\n }\n}`,
authMode: "caller_context",
variablesMapping: JSON.stringify(
{
"input.name": "$body.name",
+ "input.alias": "$body.alias",
"input.kind": "$body.kind",
"input.tenantId": "$body.tenantId",
},
@@ -266,6 +268,7 @@ const ENDPOINT_PRESETS: EndpointPreset[] = [
required: ["name"],
properties: {
name: { type: "string" },
+ alias: { type: "string" },
kind: { type: "string" },
tenantId: { type: "string" },
},
@@ -283,16 +286,17 @@ const ENDPOINT_PRESETS: EndpointPreset[] = [
values: {
key: "update_entity",
name: "Update Entity",
- description: "Updates an entity's name or attributes.",
+ description: "Updates an entity's name, alias, or attributes.",
method: "PATCH",
path: "/api/custom/entities/update",
operationKind: "mutation",
- graphql: `mutation UpdateEntity($id: ID!, $input: UpdateEntityInput!) {\n updateEntity(id: $id, input: $input) {\n id\n name\n kind\n status\n updatedAt\n }\n}`,
+ graphql: `mutation UpdateEntity($id: ID!, $input: UpdateEntityInput!) {\n updateEntity(id: $id, input: $input) {\n id\n name\n alias\n kind\n status\n updatedAt\n }\n}`,
authMode: "caller_context",
variablesMapping: JSON.stringify(
{
id: "$query.id",
"input.name": "$body.name",
+ "input.alias": "$body.alias",
"input.attributes": "$body.attributes",
},
null,
@@ -303,6 +307,7 @@ const ENDPOINT_PRESETS: EndpointPreset[] = [
type: "object",
properties: {
name: { type: "string" },
+ alias: { type: ["string", "null"] },
attributes: { type: "object" },
},
},
@@ -319,14 +324,20 @@ const ENDPOINT_PRESETS: EndpointPreset[] = [
values: {
key: "create_resource",
name: "Create Resource",
- description: "Creates a resource that can be referenced in policies.",
+ description:
+ "Creates a resource with an optional alias that can be referenced in policies.",
method: "POST",
path: "/api/custom/resources",
operationKind: "mutation",
- graphql: `mutation CreateResource($input: CreateResourceInput!) {\n createResource(input: $input) {\n id\n name\n kind\n status\n createdAt\n }\n}`,
+ graphql: `mutation CreateResource($input: CreateResourceInput!) {\n createResource(input: $input) {\n id\n name\n alias\n kind\n createdAt\n }\n}`,
authMode: "caller_context",
variablesMapping: JSON.stringify(
- { "input.name": "$body.name", "input.kind": "$body.kind" },
+ {
+ "input.name": "$body.name",
+ "input.alias": "$body.alias",
+ "input.kind": "$body.kind",
+ "input.tenantId": "$body.tenantId",
+ },
null,
2,
),
@@ -336,7 +347,9 @@ const ENDPOINT_PRESETS: EndpointPreset[] = [
required: ["name"],
properties: {
name: { type: "string" },
+ alias: { type: "string" },
kind: { type: "string" },
+ tenantId: { type: "string" },
},
},
null,
@@ -352,21 +365,30 @@ const ENDPOINT_PRESETS: EndpointPreset[] = [
values: {
key: "update_resource",
name: "Update Resource",
- description: "Updates a resource's name or attributes.",
+ description: "Updates a resource's name, alias, or attributes.",
method: "PATCH",
path: "/api/custom/resources/update",
operationKind: "mutation",
- graphql: `mutation UpdateResource($id: ID!, $input: UpdateResourceInput!) {\n updateResource(id: $id, input: $input) {\n id\n name\n kind\n status\n updatedAt\n }\n}`,
+ graphql: `mutation UpdateResource($id: ID!, $input: UpdateResourceInput!) {\n updateResource(id: $id, input: $input) {\n id\n name\n alias\n kind\n updatedAt\n }\n}`,
authMode: "caller_context",
variablesMapping: JSON.stringify(
- { id: "$query.id", "input.name": "$body.name" },
+ {
+ id: "$query.id",
+ "input.name": "$body.name",
+ "input.alias": "$body.alias",
+ "input.attributes": "$body.attributes",
+ },
null,
2,
),
requestSchema: JSON.stringify(
{
type: "object",
- properties: { name: { type: "string" } },
+ properties: {
+ name: { type: "string" },
+ alias: { type: ["string", "null"] },
+ attributes: { type: "object" },
+ },
},
null,
2,
diff --git a/app/components/entities/entity-create-form.tsx b/app/components/entities/entity-create-form.tsx
index d5ac029..ddc8767 100644
--- a/app/components/entities/entity-create-form.tsx
+++ b/app/components/entities/entity-create-form.tsx
@@ -75,6 +75,7 @@ const CREATE_ENTITY_MUTATION = `
profileId
profileVersionId
name
+ alias
tenantId
status
createdAt
@@ -91,6 +92,7 @@ const UPDATE_ENTITY_MUTATION = `
profileId
profileVersionId
name
+ alias
tenantId
status
updatedAt
@@ -133,6 +135,7 @@ type ProfileVersionsData = {
const entityFormSchema = z.object({
name: z.string().trim().min(1, "Name is required."),
+ alias: z.string().trim(),
kind: z.enum(ENTITY_KINDS),
tenantId: z.string().trim(),
profileId: z.string().trim(),
@@ -202,6 +205,7 @@ type EntityFormValues = z.infer;
export type EntityFormInitialValues = {
id: string;
name: string;
+ alias: string;
kind: (typeof ENTITY_KINDS)[number];
tenantId: string;
profileId: string;
@@ -211,6 +215,7 @@ export type EntityFormInitialValues = {
const defaultValues: EntityFormValues = {
name: "",
+ alias: "",
kind: "human",
tenantId: "",
profileId: "",
@@ -243,6 +248,7 @@ export function EntityCreateForm({
defaultValues: entity
? {
name: entity.name,
+ alias: entity.alias,
kind: entity.kind,
tenantId: entity.tenantId,
profileId: entity.profileId,
@@ -340,14 +346,17 @@ export function EntityCreateForm({
query: UPDATE_ENTITY_MUTATION,
variables: {
id: entity.id,
- input: removeEmptyValues({
- name: values.name,
- kind: values.kind,
- tenantId: values.tenantId,
- profileId: values.profileId,
- profileVersionId: values.profileVersionId,
- attributes,
- }),
+ input: {
+ ...removeEmptyValues({
+ name: values.name,
+ kind: values.kind,
+ tenantId: values.tenantId,
+ profileId: values.profileId,
+ profileVersionId: values.profileVersionId,
+ attributes,
+ }),
+ alias: values.alias || null,
+ },
},
});
} else {
@@ -356,6 +365,7 @@ export function EntityCreateForm({
variables: {
input: removeEmptyValues({
name: values.name,
+ alias: values.alias,
kind: values.kind,
tenantId: values.tenantId,
profileId: values.profileId,
@@ -382,6 +392,7 @@ export function EntityCreateForm({