К основному содержанию
скилл CX Codex

extension-authorization — скилл для Claude Code: доступы и установка

Скилл добавляет в Motoko-приложение авторизацию с ролями и проверками доступа. Пригодится, если пользователям нужны разные права на чтение и изменение данных.

Установить в Claude Code · Через Skills CLI (Vercel) Все способы и платформы ↓
npx skills add caffeinelabs/skills --skill extension-authorization -a claude-code
Открыть на GitHub

Что делает extension-authorization

Скилл описывает систему аутентификации и ролевого доступа для Caffeine AI. В основе — библиотека mo:caffeineai-authorization/access-control и миксин MixinAuthorization, который подключают в main.mo. Есть роли администратора, пользователя и гостя, а также проверки прав и примеры защитных условий для публичных функций.

Инструкции показывают, как добавить состояние доступа через цепочку миграций и передать его пользовательскому миксину. Первый вошедший аутентифицированный пользователь автоматически получает роль администратора; токен или секрет для этого не нужен. Это важное условие: первый вход определяет, кто сможет назначать роли. Скилл также описывает обработку подтверждённых атрибутов Internet Identity и порядок удаления пакета при переходе на собственную авторизацию.

Какие задачи решает

  • разграничить доступ администратора, пользователя и гостя
  • закрыть публичные функции проверкой роли
  • передать состояние доступа в пользовательский миксин
  • инициализировать состояние авторизации через миграцию

Примеры запросов агенту

  • Добавь в main.mo авторизацию с ролями через MixinAuthorization.
  • Защити сохранение профиля так, чтобы его менял только вошедший пользователь.
  • Покажи, как проверять права администратора перед назначением роли.

Как установить extension-authorization

1Через Skills CLI (Vercel)

npx skills add caffeinelabs/skills --skill extension-authorization -a claude-code

2Вручную: положить файлы

~/.claude/skills/extension-authorization/SKILL.md

Папка скилла целиком; для одного проекта — .claude/skills/extension-authorization/

По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов

Вопросы о extension-authorization

Как установить extension-authorization в Claude Code?

Выполни npx skills add caffeinelabs/skills --skill extension-authorization -a claude-code. Файл скилла находится по пути ~/.claude/skills/extension-authorization/SKILL.md.

Что делает extension-authorization?

Скилл помогает добавить в Motoko-приложение аутентификацию и ролевые проверки. Он описывает миксин, состояние доступа и защиту функций.

Кто становится администратором первым?

Первый аутентифицированный пользователь, который входит в систему. Для этого не требуется токен или секрет.

Исходный текст

SKILL.md · Apache-2.0
---
name: extension-authorization
description: Authorization system with role-based access control. Must-have for all apps that manage personal or access-restricted data.
version: 1.3.3
compatibility:
  mops:
    caffeineai-authorization: "~1.0.0"
  npm:
    "@caffeineai/core-infrastructure": "^2.0.0"
caffeineai-subscription: [none]
---

# Authorization
Authorization extendsion for [Caffeine AI](https://caffeine.ai?utm_source=caffeine-skill&utm_medium=referral).

## Overview

This skill adds an authentication and authorization system with role-based access control using the mixin pattern. The `MixinAuthorization` mixin provides standard authorization endpoints automatically.

# Backend

Authentication system with role-based access control.

There is a prefabricated library `mo:caffeineai-authorization/access-control.mo`. It provides core authentication with role-based access control.

## Module API

```mo:caffeineai-authorization/access-control.mo
module {
  public type UserRole = {
    #admin;
    #user;
    #guest;
  };

  public type AccessControlState = { /* internal state */ };

  public func initState() : AccessControlState;
  public func getUserRole(state : AccessControlState, caller : Principal) : UserRole;
  public func assignRole(state : AccessControlState, caller : Principal, user : Principal, role : UserRole);
  public func isAdmin(state : AccessControlState, caller : Principal) : Bool;
  public func hasPermission(state : AccessControlState, caller : Principal, requiredRole : UserRole) : Bool;
};
```

Initialization is handled internally by `MixinAuthorization` -- do not call `initialize` directly. The first authenticated user to log in automatically becomes admin; no token or secret is required.

IMPORTANT: The `include MixinAuthorization(accessControlState)` line MUST be placed in `main.mo`, not in a custom mixin file.

## Opting out / custom authentication

If the user asks to replace this authorization system with custom authentication (for example username/password auth, no Internet Identity, or no `MixinAuthorization`), remove the `caffeineai-authorization` mops package from the project. This is the only supported way to remove the `include-authorization` lint requirement, because that lint rule is shipped by the package. Do not add suppression comments or leave the package installed while omitting `include MixinAuthorization(accessControlState)`.

When removing the package, also remove all `mo:caffeineai-authorization/*` imports, the `accessControlState` initialization, `include MixinAuthorization(accessControlState)`, and any `AccessControl` guard calls that belonged to this component. Replace them with the custom authentication and authorization checks requested by the user.

## Setup in main.mo

```motoko filepath=src/backend/main.mo
import Map "mo:core/Map";
import Principal "mo:core/Principal";
import AccessControl "mo:caffeineai-authorization/access-control";
import MixinAuthorization "mo:caffeineai-authorization/MixinAuthorization";
import Types "types";
import ProfileMixin "mixins/Profile";

actor {
  let accessControlState : AccessControl.AccessControlState;
  include MixinAuthorization(accessControlState, null);

  let userProfiles : Map.Map<Principal, Types.UserProfile>;

  include ProfileMixin(accessControlState, userProfiles);
};
```

The migration chain head:

```motoko filepath=src/backend/migrations/00000000_000000.mo
import Map "mo:core/Map";
import AccessControl "mo:caffeineai-authorization/access-control";

module {
  type UserProfile = {
    name : Text;
  };

  type NewActor = {
    accessControlState : AccessControl.AccessControlState;
    userProfiles : Map.Map<Principal, UserProfile>;
  };

  public func migration(_old : {}) : NewActor {
    {
      accessControlState = AccessControl.initState();
      userProfiles = Map.empty<Principal, UserProfile>();
    };
  };
};
```

## Type Definitions in types.mo

```motoko filepath=src/backend/types.mo
module {
  public type UserProfile = {
    name : Text;
  };
};
```

## Custom Mixin Example (mixins/Profile.mo)

The frontend requires `getCallerUserProfile`, `saveCallerUserProfile`, and `getUserProfile`. Pass `accessControlState` to your mixin so it can check permissions.

```motoko filepath=src/backend/mixins/Profile.mo
import Map "mo:core/Map";
import Principal "mo:core/Principal";
import Runtime "mo:core/Runtime";
import AccessControl "mo:caffeineai-authorization/access-control";
import Types "../types";

mixin (
  accessControlState : AccessControl.AccessControlState,
  userProfiles : Map.Map<Principal, Types.UserProfile>,
) {
  public query ({ caller }) func getCallerUserProfile() : async ?Types.UserProfile {
    if (not AccessControl.hasPermission(accessControlState, caller, #user)) {
      Runtime.trap("Unauthorized");
    };
    userProfiles.get(caller);
  };

  public shared ({ caller }) func saveCallerUserProfile(profile : Types.UserProfile) : async () {
    if (not AccessControl.hasPermission(accessControlState, caller, #user)) {
      Runtime.trap("Unauthorized");
    };
    userProfiles.add(caller, profile);
  };

  public query ({ caller }) func getUserProfile(user : Principal) : async ?Types.UserProfile {
    if (caller != user and not AccessControl.isAdmin(accessControlState, caller)) {
      Runtime.trap("Unauthorized: Can only view your own profile");
    };
    userProfiles.get(user);
  };
};
```

## Guard Patterns

Apply the appropriate guard to every public function:

```
// Admin-only:
if (not AccessControl.hasPermission(accessControlState, caller, #admin)) {
  Runtime.trap("Unauthorized: Only admins can perform this action");
};

// Users only:
if (not AccessControl.hasPermission(accessControlState, caller, #user)) {
  Runtime.trap("Unauthorized: Only users can perform this action");
};

// Any user including guests: No check needed
```

## Design Guidelines

- Anonymous principals are treated as guests.
- `assignRole` includes an admin-only guard internally.
- Use `shared({ caller })` for authenticated endpoints that modify data.
- Use `query({ caller })` for authenticated endpoints that fetch data.
- Handle ownership verification where needed.
- Use `Runtime.trap` for authorization failures.

## Email Attributes

`MixinAuthorization` can capture the user's verified Internet Identity attributes (name and email) at sign-in. Pass a callback as the second argument instead of `null`; it runs once per sign-in, after the attribute bundle has been verified.

Do NOT use the `mo:identity-attributes` mixin directly -- always go through `MixinAuthorization`. The callback receives the caller principal and the verified attributes:

```
{
  name : ?Text;   // verified display name, when present
  email : ?Text;  // always the verified address -- sourced from II's `verified_email`, never the unverified `email` key
  sso : ?Text;    // SSO domain when the identity came from SSO, otherwise null
}
```

The field is named `email`, but it only ever holds II's `verified_email` value -- the unverified `email` key is never read. Use `attrs.email` in the callback (there is no `attrs.verified_email` field).

Which attributes arrive depends on the sign-in variant the frontend used (see the `extension-authorization`-compatible `login()` options in the `extension-core-infrastructure` skill):

- `login()` (plain Internet Identity): requests `verified_email` and shows an Internet Identity consent step first. `email` is present only when the user's identity is linked to a Google or Microsoft account; passkey-only and Apple identities never have one. `sso` is null.
- `login({ provider: 'google' })`: verified `name` and `email` from the user's Google account; `sso` is null.
- `login({ provider: 'microsoft' })`: verified `name` and `email` from the user's Microsoft account; `sso` is null.
- `login({ ssoDomain: 'acme.com' })`: verified `name` and `email` from the company's identity provider; `sso` holds the domain (e.g. `"acme.com"`). Use `attrs.sso` to gate features to company members or auto-assign roles by domain.

Treat `email` as optional for every variant: show or use it when it is present, and keep the feature working for a user without one. Never add an email-verification step for an address that arrived here.

Store them in your own state and expose a getter to read them back:

<!-- motoko-check:skip -->
```motoko
import Map "mo:core/Map";
import Principal "mo:core/Principal";
import AccessControl "mo:caffeineai-authorization/access-control";
import MixinAuthorization "mo:caffeineai-authorization/MixinAuthorization";

actor {
  let accessControlState : AccessControl.AccessControlState;

  let emails : Map.Map<Principal, Text>;

  include MixinAuthorization(
    accessControlState,
    ?(func(caller : Principal, attrs : { name : ?Text; email : ?Text; sso : ?Text }) {
      switch (attrs.email) {
        case (?email) { emails.add(caller, email) };
        case null {};
      };
    }),
  );

  public query ({ caller }) func getCallerEmail() : async ?Text {
    emails.get(caller);
  };
};
```

The `trusted_attribute_signers` and `frontend_origins` canister environment variables required for attribute verification are configured automatically by the Caffeine platform -- you do not set them.

### Fetching the Email on the Frontend

After sign-in, query the getter like any other authenticated actor method:

```typescript
const { data: callerEmail } = useQuery<string | null>({
  queryKey: ['callerEmail'],
  queryFn: () => actor.getCallerEmail(),
  enabled: !!actor && isAuthenticated,
});
```

# Frontend

Authentication system with role-based access control.

## Sign-In Options

Sign-in goes through the `useInternetIdentity` hook from `@caffeineai/core-infrastructure` (see the `extension-core-infrastructure` skill for the full API and UI pattern). Default to plain `login()` unless Google, Microsoft, or company/workspace SSO sign-in was requested.

Rules:

- When Google, Microsoft, or SSO sign-in **is** requested, the sign-in screen must show the requested direct option (Google button, Microsoft button, and/or SSO domain input) **and** keep a plain `login()` "Sign in with Internet Identity" button as a fallback — users without a Google or Microsoft account or a registered company domain must still be able to sign in.
- Never remove the Internet Identity option from an existing app that already has users. The principal is derived from the user's Internet Identity: a user whose II identity is Google- or Microsoft-backed gets the **same principal** through either button, but a user who signed up with a passkey-based II identity gets a **different principal** if they authenticate with a Google or Microsoft account that is not linked to that identity — their data would appear missing. Keeping both buttons lets every existing user sign in the way they always have.

## User Profile Setup

When using Internet Identity, the user gets a principal id only after login. Anonymous principals are treated as guests. The principal id is not human-readable -- ask the user for their name the first time they log in with a new principal.

Backend API for profiles:
- `getCallerUserProfile(): Promise<UserProfile | null>` -- returns `null` if no profile exists
- `saveCallerUserProfile(profile: UserProfile): Promise<void>` -- saves name and profile data
- `getUserProfile(user: Principal): Promise<UserProfile | null>` -- fetch another user's profile

Rules:
- On login, if the user already has a profile, do not ask for the name again
- Display the user's profile name instead of the principal id
- Make sure the user must be logged in before seeing any application data
- When logging out, clear all cached application data including the cached user profile

### Preventing Profile Setup Modal Flash

```typescript
export function useGetCallerUserProfile() {
  const { actor, isFetching: actorFetching } = useActor();

  const query = useQuery<UserProfile | null>({
    queryKey: ['currentUserProfile'],
    queryFn: async () => {
      if (!actor) throw new Error('Actor not available');
      return actor.getCallerUserProfile();
    },
    enabled: !!actor && !actorFetching,
    retry: false,
  });

  return {
    ...query,
    isLoading: actorFetching || query.isLoading,
    isFetched: !!actor && query.isFetched,
  };
}
```

Then in your component:
```typescript
const showProfileSetup = isAuthenticated && !profileLoading && isFetched && userProfile === null;
```

## Auth State Lifecycle

The `useInternetIdentity` hook exposes two kinds of state — use the right one:

| Scenario | `loginStatus` | `isAuthenticated` |
|---|---|---|
| Page load, no stored session | `"idle"` | `false` |
| Page load, restoring stored session | `"initializing"` | `false` → `true` |
| Stored session restored after reload | `"idle"` | `true` |
| Interactive login in progress (popup open) | `"logging-in"` | `false` |
| Interactive login just completed | `"success"` | `true` |
| Login popup failed / cancelled | `"loginError"` | `false` |
| Session expired (idle / TTL / revoke) | `"expired"` | `false` |
| Sign-out in another tab | `"idle"` | `false` |

**IMPORTANT:** `isLoginSuccess` (`loginStatus === "success"`) is only `true` after an interactive login via the popup. It is **NOT** `true` when a stored identity is restored on page reload. Never use `isLoginSuccess` to gate authenticated vs. unauthenticated UI — always use `isAuthenticated`. A session that expired (`loginStatus === "expired"`) is not a deliberate logout.

Key states for the login button:
- `isInitializing` — `AuthClient` is loading from IndexedDB; disable the button to prevent clicks before the client is ready.
- `isLoggingIn` — the II popup is open; disable the button to prevent duplicate popups.

## Login Component

```typescript
import { useInternetIdentity } from '@caffeineai/core-infrastructure';
import { useQueryClient } from '@tanstack/react-query';

export default function LoginButton() {
  const { login, clear, isAuthenticated, isInitializing, isLoggingIn } = useInternetIdentity();
  const queryClient = useQueryClient();

  const handleAuth = () => {
    if (isAuthenticated) {
      clear();
      queryClient.clear();
    } else {
      login();
    }
  };

  return (
    <button
      onClick={handleAuth}
      disabled={isInitializing || isLoggingIn}
      className={`px-6 py-2 rounded-full transition-colors font-medium ${
        isAuthenticated
          ? 'bg-gray-200 hover:bg-gray-300 text-gray-800'
          : 'bg-blue-600 hover:bg-blue-700 text-white'
      } disabled:opacity-50`}
    >
      {isInitializing ? 'Loading...' : isAuthenticated ? 'Logout' : 'Login'}
    </button>
  );
}
```

The `login()` and `clear()` functions are fire-and-forget (they don't return promises that track the full flow). The hook's `isLoggingIn` / `isInitializing` states track the async lifecycle — do **not** wrap them in local `useState` / `isPending` logic.

When the app calls for Google or Microsoft sign-in or company/workspace SSO, use the same hook with options: `login({ provider: 'google' })`, `login({ provider: 'microsoft' })`, or `login({ ssoDomain: 'acme.com' })`. All variants produce the same identity and session behavior; see the `extension-core-infrastructure` skill for the full sign-in UI pattern.

Gate authenticated UI on `isAuthenticated` (covers both fresh login and restored sessions on page reload):
```typescript
{isAuthenticated ? (
  <AuthenticatedApp />
) : (
  <LoginScreen />
)}
```

## Comparing Current User with Data Author

```typescript
import { useInternetIdentity } from '@caffeineai/core-infrastructure';
import type { Principal } from '@icp-sdk/core/principal';

const { identity } = useInternetIdentity();

const isAuthor = (authorPrincipal: Principal): boolean => {
  if (!identity) return false;
  return authorPrincipal.toString() === identity.getPrincipal().toString();
};
```

## Access Control UI

For admin-only or personal applications, show an AccessDeniedScreen component when unauthorized users try to access the application.

## Error Handling

Handle authorization errors from backend `Debug.trap` calls gracefully in the UI with appropriate error messages shown to the user.

Note: The initialization of the first admin is done automatically in `@caffeineai/core-infrastructure`. The first authenticated user to log in becomes admin; no token or secret is needed.

© caffeinelabs · Apache-2.0 · caffeinelabs/skills

Похожие скиллы

Все скиллы по теме «Бэкенд и API» →
mcp-builder официальный anthropics Скилл проводит через создание MCP-сервера на TypeScript или Python: от изучения протокола до тестов и оценки качества. 130 949установок claude-api официальный anthropics Справочник по Anthropic SDK и API Claude для генерации кода без устаревших параметров и выдуманных методов. 74 198установок lark-im larksuite Скилл подключает Lark IM к агенту: он отправляет сообщения, ищет историю, управляет чатами и файлами. Подойдёт для задач вокруг командных коммуникаций. CX Codex +1 472 900установок lark-event larksuite Слушает события Lark/Feishu и отдаёт их потоком NDJSON. Подходит для ботов, обработчиков сообщений и долгих подписчиков. CX Codex +1 466 800установок caveman-discover JuliusBrussee Находит точки вызова LLM в репозитории и размечает их воркфлоу для Caveman Cloud, чтобы расходы на токены не сливались в одну общую кучу. CX Codex +1 104 400установок migration JuliusBrussee Скилл задаёт правила безопасных миграций схем, данных и API с обязательным откатом. Пригодится при изменениях, где нельзя терять данные или ломать совместимость. CX Codex +1 103 900установок

Из того же набора30

extension-email-calendar-events Скилл добавляет в приложение календарные события и рассылку iCalendar-приглашений по почте. Пригодится, если нужны встречи с гостями без ручной сборки ICS. CX Codex +1 28 400установок extension-email-marketing Добавляет в приложение на Caffeine AI маркетинговые письма по темам: подписчики, токены отписки и страница /unsubscribe на своём фронтенде. CX Codex +1 28 100установок extension-camera Добавляет в агент правила работы с веб-камерой через React-хук useCamera. Пригодится для съёмки фото и видеопревью в браузере. CX Codex +1 28 100установок extension-qr-code Добавляет в React-приложение сканирование QR-кодов через камеру устройства. Пригодится, когда нужно считать код прямо в браузере. CX Codex +1 28 200установок extension-stripe Добавляет в проект на Caffeine AI оплату картами через Stripe: создание checkout-сессии и проверку её статуса. Пригодится, когда нужен приём платежей внутри канистры. CX Codex +1 28 200установок extension-posting-to-x Скилл для сборок Caffeine, которые публикуют твиты в X. Задаёт единственный поддерживаемый путь через пакет x-client и OAuth 2.0 PKCE. CX Codex +1 28 100установок extension-openai Задаёт правила интеграции OpenAI в канистры Motoko через пакет openai-client. Защищает API-ключ от утечки при реплицированных вызовах. CX Codex +1 28 100установок extension-email-raw Скилл добавляет в проект отправку одного письма сразу нескольким получателям через to, cc и bcc. Пригодится, когда нужно разослать уведомление участникам встречи. CX Codex +1 28 100установок

Спонсоры проекта

Компании, которые помогают держать аналитику открытой