Skip to main content

Auth providers

Your mod publishes a way to sign in, under an id, and the rest of the app resolves sessions through it.

import { function authProvider(input: AuthProviderInput): AuthProvider
Declare a sign-in provider for a service this mod talks to. Put the result in the mod's `auth` array. From then on any mod, this one included, gets a token by calling `ctx.auth.getSession(id, scopes, { createIfNone: true })`. That is the consuming side. This is the producing side, and you only write it for a service the host does not already cover. The three callbacks split cleanly: `getSessions` is the silent path and must never prompt, `createSession` is the interactive path and is the only one that may, `removeSession` cleans up.
@exampleA token the user pastes in once ```ts authProvider({ id: 'my-service', label: 'My Service', getSessions: async (scopes, options, ctx) => sessionsFor(await ctx.secrets.get('token')), createSession: async (scopes, options, ctx) => { const token = await ctx.prompt.input({ title: 'API token', password: true }); await ctx.secrets.store('token', requireToken(token)); return sessionFor(token); }, removeSession: async (sessionId, ctx) => ctx.secrets.delete('token'), }); ```@categoryAuthoring
authProvider
, function defineMod<const TMod extends ModDefinition>(definition: TMod & ValidateModHooks<TMod>): Mod
The entry point of the SDK. A mod's module default-exports one `defineMod` call and nothing else. The returned value is data. It registers nothing, starts nothing, and touches no host state. The host folds it twice. At build time it runs the module in a throwaway sandbox and reads the value to derive the serializable manifest. At runtime it folds the same value again to wire your handlers into their lanes: ext-host for commands, tools, effects, and hooks; the sandboxed renderer for views and widgets. That double fold is why the module must be pure. It is re-executed to produce the manifest, so anything at the top level, a file read, a network call, a timer, a `console` line, runs during a build with no host attached. Put behavior inside handlers: `run`, `activate`, `handle`. The `ValidateModHooks` constraint makes the call fail to typecheck when a `hook.beforeAction` targets an action that is neither a known host action nor `command:` plus one of this mod's own command ids.
@example```ts export default defineMod({ metadata: { displayName: 'Review', description: 'Review helpers for the current change.', }, commands: [ command({ id: 'review.open', title: 'Review: Open', run: openPanel }), slashCommand({ id: 'review', description: 'Review this change', run: review }), ], ai: { tools: [reviewTool], }, }); ```@categoryAuthoring
defineMod
} from '@modular/sdk';
export default
defineMod<{
    readonly metadata: {
        readonly displayName: "Linear";
        readonly description: "Linear integration";
    };
    readonly auth: readonly [AuthProvider];
}>(definition: {
    readonly metadata: {
        readonly displayName: "Linear";
        readonly description: "Linear integration";
    };
    readonly auth: readonly [AuthProvider];
}): Mod
The entry point of the SDK. A mod's module default-exports one `defineMod` call and nothing else. The returned value is data. It registers nothing, starts nothing, and touches no host state. The host folds it twice. At build time it runs the module in a throwaway sandbox and reads the value to derive the serializable manifest. At runtime it folds the same value again to wire your handlers into their lanes: ext-host for commands, tools, effects, and hooks; the sandboxed renderer for views and widgets. That double fold is why the module must be pure. It is re-executed to produce the manifest, so anything at the top level, a file read, a network call, a timer, a `console` line, runs during a build with no host attached. Put behavior inside handlers: `run`, `activate`, `handle`. The `ValidateModHooks` constraint makes the call fail to typecheck when a `hook.beforeAction` targets an action that is neither a known host action nor `command:` plus one of this mod's own command ids.
@example```ts export default defineMod({ metadata: { displayName: 'Review', description: 'Review helpers for the current change.', }, commands: [ command({ id: 'review.open', title: 'Review: Open', run: openPanel }), slashCommand({ id: 'review', description: 'Review this change', run: review }), ], ai: { tools: [reviewTool], }, }); ```@categoryAuthoring
defineMod
({
metadata: {
    readonly displayName: "Linear";
    readonly description: "Linear integration";
}
metadata
: { displayName: "Linear"displayName: 'Linear', description: "Linear integration"description: 'Linear integration' },
auth: readonly [AuthProvider]auth: [ function authProvider(input: AuthProviderInput): AuthProvider
Declare a sign-in provider for a service this mod talks to. Put the result in the mod's `auth` array. From then on any mod, this one included, gets a token by calling `ctx.auth.getSession(id, scopes, { createIfNone: true })`. That is the consuming side. This is the producing side, and you only write it for a service the host does not already cover. The three callbacks split cleanly: `getSessions` is the silent path and must never prompt, `createSession` is the interactive path and is the only one that may, `removeSession` cleans up.
@exampleA token the user pastes in once ```ts authProvider({ id: 'my-service', label: 'My Service', getSessions: async (scopes, options, ctx) => sessionsFor(await ctx.secrets.get('token')), createSession: async (scopes, options, ctx) => { const token = await ctx.prompt.input({ title: 'API token', password: true }); await ctx.secrets.store('token', requireToken(token)); return sessionFor(token); }, removeSession: async (sessionId, ctx) => ctx.secrets.delete('token'), }); ```@categoryAuthoring
authProvider
({
id: string
Provider id callers pass to `ctx.auth.getSession`.
id
: 'linear',
label: string
Name of the service as the user sees it, for example `GitHub`.
label
: 'Linear',
getSessions: (scopes: readonly string[] | undefined, options: AuthProviderSessionOptions, ctx: AuthProviderContext) => Promise<readonly AuthenticationSession[]>
Return sessions already signed in, without any user interaction. Called first on every lookup. Return an empty array when there is nothing stored. Do not sign the user in from here.
getSessions
: async (scopes: readonly string[] | undefinedscopes, options: AuthProviderSessionOptionsoptions, ctx: AuthProviderContextctx) => {
const const token: string | undefinedtoken = await ctx: AuthProviderContextctx.AuthProviderContext.secrets: SecretStorage
Where tokens go.
secrets
.SecretStorage.get(key: string): Promise<string | undefined>get('linear.token');
if (const token: string | undefinedtoken === var undefinedundefined) { return []; } return [ { AuthenticationSession.id: stringid: 'linear', AuthenticationSession.accessToken: stringaccessToken: const token: stringtoken, AuthenticationSession.account: AuthenticationAccountaccount: { AuthenticationAccount.id: stringid: 'linear', AuthenticationAccount.label: stringlabel: 'Linear' }, AuthenticationSession.scopes: readonly string[]scopes: scopes: readonly string[] | undefinedscopes ?? [], }, ]; }, createSession: (scopes: readonly string[], options: AuthProviderSessionOptions, ctx: AuthProviderContext) => Promise<AuthenticationSession>
Run the interactive sign-in and return the new session. Only called when {@link AuthProvider.getSessions } came back empty and the caller asked for `createIfNone`. This is where prompting and browser redirects belong. Store the token in `ctx.secrets` before returning.
createSession
: async (scopes: readonly string[]scopes, options: AuthProviderSessionOptionsoptions, ctx: AuthProviderContextctx) => {
const const token: string | undefinedtoken = await ctx: AuthProviderContextctx.AuthProviderContext.prompt: AuthPrompt
Ask the user for a token or a code.
prompt
.AuthPrompt.input(request: AuthInputRequest): Promise<string | undefined>
Resolves to what the user typed, or `undefined` if they dismissed it.
input
({
AuthInputRequest.title: string
Title of the input box.
title
: 'Sign in to Linear',
AuthInputRequest.prompt?: string | undefined
Longer explanation shown under the title.
prompt
: 'Linear API token',
}); if (const token: string | undefinedtoken === var undefinedundefined) { throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
('Sign-in cancelled.');
} await ctx: AuthProviderContextctx.AuthProviderContext.secrets: SecretStorage
Where tokens go.
secrets
.SecretStorage.store(key: string, value: string): Promise<void>store('linear.token', const token: stringtoken);
return { AuthenticationSession.id: stringid: 'linear', AuthenticationSession.accessToken: stringaccessToken: const token: stringtoken, AuthenticationSession.account: AuthenticationAccountaccount: { AuthenticationAccount.id: stringid: 'linear', AuthenticationAccount.label: stringlabel: 'Linear' }, AuthenticationSession.scopes: readonly string[]scopes, }; }, removeSession: (sessionId: string, ctx: AuthProviderContext) => Promise<void>
Sign out. Delete the stored credential and revoke it if the service allows.
removeSession
: async (sessionId: stringsessionId, ctx: AuthProviderContextctx) => {
await ctx: AuthProviderContextctx.AuthProviderContext.secrets: SecretStorage
Where tokens go.
secrets
.SecretStorage.delete(key: string): Promise<void>delete('linear.token');
}, }), ], });

getSessions runs first on every lookup and must never prompt. createSession is the only place interaction belongs, and it is reached only when getSessions came back empty and the caller asked for one.

Note the key: authProvider() feeds Mod.auth, not Mod.authProviders. That mismatch between factory name and key name is why this page is called Auth providers and the key is auth.

Members

AuthProviderid, label, kind, createSession, getSessions, removeSession.

Three verbs and two names. The verbs are the provider's whole job:

MemberCalled when
createSessionsomeone signs in
getSessionssomething needs the current sessions
removeSessionsomeone signs out

The id is claimed, not namespaced

id is a global name. Two mods declaring the same provider id are competing for it, and one of them wins — so pick an id that names the service, and expect that a second mod claiming linear will not get it.

This is the clearest case of a mod publishing a capability for others to consume, which is why the page sits in Connections rather than in Packaging.