Skip to main content
By the end of this guide, your function will reject requests without a valid token.

Quick start

1

Enable auth in your function

index.ts
import { define, z } from "@jelou/functions";

export default define({
  description: "Protected API",
  input: z.object({ query: z.string() }),
  auth: true,
  handler: async (input, ctx) => {
    return { results: [], authenticated: ctx.auth?.authenticated };
  },
});
2

Configure the secret

jelou secrets set my-function AUTH_SECRET=my-secret-token
When you use auth: true, the platform automatically looks for a secret named AUTH_SECRET.
3

Call with the token

curl -X POST https://my-function.fn.jelou.ai \
  -H "Authorization: Bearer my-secret-token" \
  -H "Content-Type: application/json" \
  -d '{"query": "test"}'
Successful response:
{ "results": [], "authenticated": true }
Without token or with incorrect token:
{ "error": "Unauthorized" }

auth variants

ValueBehavior
trueLooks for the AUTH_SECRET secret. Rejects with 401 if missing or mismatched.
falseNo authentication (default value).
{ secret: "API_TOKEN" }Looks for a secret with that specific name.
{ secret: ["API_TOKEN", "API_TOKEN_OLD"] }Accepts any of the secrets (useful for key rotation).
{ secret: "API_TOKEN", required: false }Optional auth — never returns 401.

ctx.auth

Result of the authentication check available inside the handler.
FieldTypeDescription
ctx.authAuthResult | nullnull when auth is not active or is optional without token
ctx.auth.authenticatedbooleantrue if the token was verified

Platform behavior

ScenarioResult
Valid tokenHandler executes, ctx.auth = { authenticated: true }
Invalid or missing token401 Unauthorized + WWW-Authenticate: Bearer header
Secret not configured in env500 fail-closed (never allows access if the secret does not exist)
Cron triggerAuth is skipped — crons have their own cryptographic signature
Token > 4 KBRejected with 401
Fail-closed: if you forget to configure the secret with jelou secrets set, the platform returns 500 instead of allowing access. This is intentional — it never degrades to “no auth”.

Key rotation

Accept the old and new token simultaneously during migration:
export default define({
  description: "API with key rotation",
  input: z.object({ id: z.string() }),
  auth: { secret: ["API_TOKEN", "API_TOKEN_OLD"] },
  handler: async (input, ctx) => {
    return { id: input.id };
  },
});
jelou secrets set my-function API_TOKEN=new-token-v2
jelou secrets set my-function API_TOKEN_OLD=previous-token-v1
Once all clients use the new token, remove API_TOKEN_OLD from the array and from the secret.

Optional auth

Public endpoints with optional premium access:
export default define({
  description: "Catalog with optional premium pricing",
  input: z.object({ category: z.string() }),
  auth: { secret: "API_TOKEN", required: false },
  handler: async (input, ctx) => {
    const products = await searchProducts(input.category);

    if (ctx.auth?.authenticated) {
      return { products, prices: await wholesalePrices(products) };
    }

    return { products };
  },
});
With required: false:
  • Valid token → ctx.auth = { authenticated: true }
  • No token → ctx.auth = null, handler executes normally
  • Invalid token → ctx.auth = null (does not return 401)

Auth in app()

Global auth

import { app, define, z } from "@jelou/functions";

export default app({
  config: { auth: { secret: "API_TOKEN" } },
  tools: {
    search: define({
      description: "Search records",
      input: z.object({ q: z.string() }),
      handler: async (input) => ({ results: [] }),
    }),
    create: define({
      description: "Create record",
      input: z.object({ name: z.string() }),
      handler: async (input) => ({ id: "abc-123" }),
    }),
  },
});
All tools inherit the global auth.

Per-tool auth + opt-out

export default app({
  config: { auth: { secret: "API_TOKEN" } },
  tools: {
    healthCheck: define({
      description: "Public health check",
      input: z.object({}),
      auth: false,
      handler: async () => ({ status: "ok" }),
    }),
    admin: define({
      description: "Admin operation",
      input: z.object({ action: z.string() }),
      auth: { secret: "ADMIN_TOKEN" },
      handler: async (input) => ({ done: true }),
    }),
  },
});
ToolAuth
healthCheckNo auth (auth: false overrides the global)
adminUses ADMIN_TOKEN instead of the global
  • Cron bypass: cron triggers never go through auth. If your function uses isCron, verify business logic internally.
  • Never hardcode secrets in code. Always use jelou secrets set.
  • Fail-closed: a missing secret causes 500, not open access.

Testing

import { assertEquals } from "jsr:@std/assert";
import {
  createMockAuthContext,
  createMockAuthRequest,
  createMockContext,
  createMockRequest,
} from "@jelou/functions/testing";

Deno.test("handler receives auth when token is valid", async () => {
  const ctx = createMockAuthContext();
  const req = createMockAuthRequest("my-secret-token", { query: "test" });

  const result = await fn.handler({ query: "test" }, ctx, req);
  assertEquals(result.authenticated, true);
});

Deno.test("handler without auth returns ctx.auth null", async () => {
  const ctx = createMockContext({ auth: null });
  const req = createMockRequest({ query: "test" });

  const result = await fn.handler({ query: "test" }, ctx, req);
  assertEquals(result.authenticated, undefined);
});

createMockAuthContext()

Full reference for authentication mocks.