By the end of this guide, your function will reject requests without a valid token.
Quick start
Enable auth in your function
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 };
} ,
}) ;
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.
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
Value Behavior 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.
Field Type Description ctx.authAuthResult | nullnull when auth is not active or is optional without tokenctx.auth.authenticatedbooleantrue if the token was verified
Scenario Result Valid token Handler executes, ctx.auth = { authenticated: true } Invalid or missing token 401 Unauthorized + WWW-Authenticate: Bearer headerSecret not configured in env 500 fail-closed (never allows access if the secret does not exist)Cron trigger Auth is skipped — crons have their own cryptographic signature Token > 4 KB Rejected 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.
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 }),
}),
} ,
}) ;
Tool Auth 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.