Skip to content
Back to skills

Auth

ASecurity

Only some people may use the app (invited e-mails, a company domain), some of them are admins, or records must belong to the signed-in person. Sign-in is a 6-digit code e-mailed by drobek, or a sign-in provider the server runs (company SSO with OpenID Connect: `skill_info('oidc')`); no passwords. Never build your own login; Firebase Auth, Auth0, Clerk, NextAuth cannot run here.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 29, 2026
ai-agentsgoreactnodegitapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 20 files and shows the line behind each finding

Scanned October 7, 2026

npx -y skills add freema/drobek --skill auth --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Auth?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Auth
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/freema-auth/badge)](https://www.skillsdirectory.com/skills/freema-auth)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
# auth — sign-in for the people who use the app

## 1. When to use

Only some people may use the app (invited e-mails, a company domain), some
of them are admins, or records must belong to the signed-in person. Sign-in
is a 6-digit code e-mailed by drobek, or a sign-in provider the server runs
(company SSO with OpenID Connect: `skill_info('oidc')`); no passwords. Never
build your own login; Firebase Auth, Auth0, Clerk, NextAuth cannot run here.

## 2. Minimal working code

```json
{ "app_id": "…", "module": "auth", "config": {
  "allow": { "emails": ["ana@example.com"], "domains": ["example.com"] },
  "adminEmails": ["boss@example.com"] } }
```

```tsx
// src/main.tsx
import { createRoot } from 'react-dom/client';
import { drobek } from 'drobek';
import { LoginGate } from 'drobek/auth';
import './styles.css';

function Board({ email, role }: { email: string; role: 'user' | 'admin' }) {
  return (
    <main>
      <h1>Team board</h1>
      <p id="who">Signed in as {email} ({role})</p>
      {role === 'admin' && <p>Admin tools go here.</p>}
      <button onClick={() => drobek.auth.logout()}>Sign out</button>
    </main>
  );
}

createRoot(document.getElementById('root')!).render(
  <LoginGate title="Team board">{(user) => <Board email={user.email} role={user.role} />}</LoginGate>
);
```

- `<LoginGate>` shows the e-mail → code form (labels "Email", "Code") and a
  "Continue with <label>" button per enabled provider to signed-out
  visitors, its children to signed-in users. `requireAdmin` lets only
  admins through.
- `drobek/auth` is compiled into the app with the app's React: `drobek.json`
  must map `react` and `react/jsx-runtime` (the react-ts template does).
- The editors of the app's workspace (you, the user you build for) can
  always sign in with their own address, as `admin`: the preview works
  before any config.

Without React:

```js
import { drobek } from 'drobek';

const form = document.querySelector('form');
if (form) form.addEventListener('submit', async (e) => {
  e.preventDefault();
  const email = String(new FormData(form).get('email'));
  const code = String(new FormData(form).get('code') ?? '');
  if (!code) await drobek.auth.sendCode(email);
  else console.log('signed in', await drobek.auth.verify(email, code));
});
console.log('current user', await drobek.auth.me());
```

## 3. API and types

```ts api
// drobek.auth
export interface User { id: string; email: string; role: 'user' | 'admin' }
export interface Api {
  me(): Promise<User | null>; // null = signed out; also extends the 30-day session
  sendCode(email: string): Promise<{ sent: true; email: string; expires_in: number }>;
  verify(email: string, code: string): Promise<User>; // sets the session cookie
  logout(): Promise<void>;
  onChange(listener: (user: User | null) => void): () => void; // returns unsubscribe
  providers(): Promise<SignInMethod[]>; // the methods that are on: 'emailCode' first, then providers
  // leaves the page for the provider; comes back signed in to returnTo (a path, default: this page)
  signIn(provider: string, options?: { returnTo?: string }): Promise<void>;
}
export interface SignInMethod { id: string; label: string } // show "Continue with <label>"
```

```ts api
// drobek/auth
import type { JSX, ReactNode } from 'react';
export interface User { id: string; email: string; role: 'user' | 'admin' }
export interface LoginGateProps {
  children: ReactNode | ((user: User) => ReactNode);
  title?: string; // default "Sign in"
  requireAdmin?: boolean; // other signed-in users see "no access" + sign out
  loading?: ReactNode; // shown while the session is checked
}
export function LoginGate(props: LoginGateProps): JSX.Element;
export function useAuth(): { user: User | null; loading: boolean; error: string | null; logout(): Promise<void>; refresh(): Promise<void> };
```

Config (`configure_module`, a JSON merge patch; lists are replaced whole):
`allow.emails` exact addresses, `allow.domains` exact domains (not
subdomains), `allow.anyone` everybody, `adminEmails` may sign in as `admin`,
`providers.emailCode.enabled` the e-mail code (default on),
`providers.<id>` `{ enabled, relinkByEmail?, …the provider's fields }`
(`skill_info('auth')` lists them). Failures reject with `DrobekError`.

## 4. Rules and limits

- `allow.anyone: true`, enabling a provider, changing its identity fields
  (e.g. `issuer`) and `relinkByEmail: true` wait for the owner: you get
  `applied: false` + `confirm_url` — give the user the link. One method
  stays on. Provider secrets are set in the dashboard, never by you.
- The allowlist decides for every method; a provider must confirm the
  address. Turning a method off or changing a provider's identity fields
  signs its sessions out. After an issuer change its users get
  `account_linked` unless the owner confirms `relinkByEmail` (migration).
- The session is an HttpOnly cookie of THIS host (30 days); the app never
  sees a token. Preview (`<slug>--preview.…`) and production (`<slug>.…`)
  are different hosts: sign in on each. Users (ids) are shared.
- A code lives 10 minutes, works once, dies after 5 wrong tries.
- Every module request re-checks the user: removed from `allow` or disabled
  → anonymous at once; the role follows `adminEmails` on the next request.
  Other modules apply their `user` / `owner` / `admin` rules to this user. As the owner you use `list_end_users`, `set_end_user_role` (edits `adminEmails`), `set_end_user_blocked` and, after the user's yes, `sign_out_end_users`.
- Per app: `AUTH_CODES_PER_IP_15MIN` 5, `AUTH_CODES_PER_IP_DAY` 20,
  `AUTH_CODES_PER_EMAIL_HOUR` 3 (more requests answer "sent", send nothing),
  `AUTH_CODES_PER_APP_HOUR` 100 (never above the app's share of the server's
  sign-in budget, 25 by default; then sign-in mail pauses 15 min),
  `AUTH_ATTEMPTS_PER_IP_15MIN` 30, `END_USERS_MAX_PER_APP` 1000;
  server-wide `AUTH_PROVIDER_CALLBACKS_PER_IP_15MIN` 60.
  `skill_info('auth').limits` has this server's values.
- Only the app's own pages can call the routes (the SDK sends `X-Drobek-SDK: 1`).

## 5. Errors → fix

| error | cause | fix |
|---|---|---|
| `email_not_allowed` (403) | address not in `allow` / `adminEmails`, or disabled | `configure_module('auth')`, or tell the user who may sign in |
| `invalid_code` (400) | wrong, used or expired code | re-enter, or `sendCode` again |
| `too_many_attempts` (429) | 5 wrong codes | request a new code |
| `rate_limited` (429) | too many codes/attempts from one IP | wait `Retry-After`; show a message |
| `unavailable` (503) | sign-in mail paused or the mail server down | let the user try again later; a failed send uses up none of their codes |
| `limit_exceeded` (429) | `END_USERS_MAX_PER_APP` reached | the owner removes users |
| `account_linked` (409 page) | the provider account (issuer + subject) is not the one the user's account is linked to | sign in the linked way or with the e-mail code; for an issuer migration the owner confirms `relinkByEmail` |
| `csrf_rejected` (403) | raw `fetch` or another origin | use `drobek.auth` / `<LoginGate>` |
| `provider_not_enabled` (404) | `signIn(id)` of a method that is off, or `sendCode` with the e-mail code off | offer only `providers()` |
| `provider_error` (502) | the provider failed | retry; the owner checks its config and secrets |
| `email_not_verified`, `invalid_state` | pages after the IdP: unverified address; link expired or used | verify at the IdP; start again |
| `unresolved_import` | `drobek.json` lacks `react` for `drobek/auth` | map `react` + `react/jsx-runtime` |
| `invalid_params` | configure_module: bad address/domain, unknown key | read `issues[].path` |

Files in this skill

  • SKILL.md7.3 KB
  • migrations/0000_auth_users.sql1.1 KB
  • migrations/0001_auth_providers.sql1 KB
  • migrations/0002_auth_identities.sql2.7 KB
  • migrations/meta/_journal.json490 B
  • package.json1.1 KB
  • sdk/auth.tsx11.1 KB
  • sdk/drobek.d.ts428 B
  • src/availability.test.ts10.7 KB
  • src/config.test.ts3.1 KB
  • src/config.ts12.9 KB
  • src/current.ts2.9 KB
  • src/flow.ts24.2 KB
  • src/index.test.ts33.7 KB
  • src/index.ts12 KB
  • src/migration.test.ts5.1 KB
  • src/owner.ts7.9 KB
  • src/providers.test.ts51.1 KB
  • src/providers.ts6.5 KB
  • src/routes.ts13.2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.