mirror of
https://github.com/neynarxyz/create-farcaster-mini-app.git
synced 2025-12-11 11:52:35 -05:00
Compare commits
4 Commits
fix-deploy
...
sc/fix-401
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e115520aa7 | ||
|
|
7dff4cd81a | ||
|
|
d1ec161f47 | ||
|
|
572ab9aa44 |
94
bin/init.js
94
bin/init.js
@@ -219,6 +219,22 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
|
|||||||
|
|
||||||
let answers;
|
let answers;
|
||||||
if (autoAcceptDefaults) {
|
if (autoAcceptDefaults) {
|
||||||
|
// Handle SIWN logic for autoAcceptDefaults
|
||||||
|
let seedPhraseValue = null;
|
||||||
|
let useSponsoredSignerValue = false;
|
||||||
|
|
||||||
|
// Only set seed phrase and sponsored signer if explicitly provided via flags
|
||||||
|
if (seedPhrase) {
|
||||||
|
// Validate the provided seed phrase
|
||||||
|
if (!seedPhrase || seedPhrase.trim().split(' ').length < 12) {
|
||||||
|
console.error('Error: Seed phrase must be at least 12 words');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
seedPhraseValue = seedPhrase;
|
||||||
|
// If sponsoredSigner flag is provided, enable it; otherwise default to false
|
||||||
|
useSponsoredSignerValue = sponsoredSigner;
|
||||||
|
}
|
||||||
|
|
||||||
answers = {
|
answers = {
|
||||||
projectName: projectName || defaultMiniAppName || 'my-farcaster-mini-app',
|
projectName: projectName || defaultMiniAppName || 'my-farcaster-mini-app',
|
||||||
description: 'A Farcaster mini app created with Neynar',
|
description: 'A Farcaster mini app created with Neynar',
|
||||||
@@ -228,8 +244,8 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
|
|||||||
useWallet: !noWallet,
|
useWallet: !noWallet,
|
||||||
useTunnel: true,
|
useTunnel: true,
|
||||||
enableAnalytics: true,
|
enableAnalytics: true,
|
||||||
seedPhrase: null,
|
seedPhrase: seedPhraseValue,
|
||||||
useSponsoredSigner: false,
|
useSponsoredSigner: useSponsoredSignerValue,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// If autoAcceptDefaults is false but we have a projectName, we still need to ask for other options
|
// If autoAcceptDefaults is false but we have a projectName, we still need to ask for other options
|
||||||
@@ -353,39 +369,34 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
|
|||||||
answers.useTunnel = hostingAnswer.useTunnel;
|
answers.useTunnel = hostingAnswer.useTunnel;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ask about Neynar Sponsored Signers / SIWN
|
// Ask about Sign In With Neynar (SIWN) - requires seed phrase
|
||||||
if (sponsoredSigner) {
|
|
||||||
answers.useSponsoredSigner = true;
|
|
||||||
if (seedPhrase) {
|
if (seedPhrase) {
|
||||||
// Validate the provided seed phrase
|
// If --seed-phrase flag is used, validate it
|
||||||
if (!seedPhrase || seedPhrase.trim().split(' ').length < 12) {
|
if (!seedPhrase || seedPhrase.trim().split(' ').length < 12) {
|
||||||
console.error('Error: Seed phrase must be at least 12 words');
|
console.error('Error: Seed phrase must be at least 12 words');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
answers.seedPhrase = seedPhrase;
|
answers.seedPhrase = seedPhrase;
|
||||||
|
// If --sponsored-signer flag is also provided, enable it
|
||||||
|
answers.useSponsoredSigner = sponsoredSigner;
|
||||||
} else {
|
} else {
|
||||||
console.error('Error: --sponsored-signer requires --seed-phrase to be provided');
|
const siwnAnswer = await inquirer.prompt([
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const sponsoredSignerAnswer = await inquirer.prompt([
|
|
||||||
{
|
{
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
name: 'useSponsoredSigner',
|
name: 'useSIWN',
|
||||||
message:
|
message:
|
||||||
'Would you like to write data to Farcaster on behalf of your miniapp users? This involves using Neynar Sponsored Signers and SIWN.\n' +
|
'Would you like to enable Sign In With Neynar (SIWN)? This allows your mini app to write data to Farcaster on behalf of users.\n' +
|
||||||
'\n⚠️ A seed phrase is required for this option.\n',
|
'\n⚠️ A seed phrase is required for this option.\n',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
answers.useSponsoredSigner = sponsoredSignerAnswer.useSponsoredSigner;
|
|
||||||
|
|
||||||
if (answers.useSponsoredSigner) {
|
if (siwnAnswer.useSIWN) {
|
||||||
const { seedPhrase } = await inquirer.prompt([
|
const { seedPhrase } = await inquirer.prompt([
|
||||||
{
|
{
|
||||||
type: 'password',
|
type: 'password',
|
||||||
name: 'seedPhrase',
|
name: 'seedPhrase',
|
||||||
message: 'Enter your Farcaster custody account seed phrase (required for Neynar Sponsored Signers/SIWN):',
|
message: 'Enter your Farcaster custody account seed phrase (required for SIWN):',
|
||||||
validate: (input) => {
|
validate: (input) => {
|
||||||
if (!input || input.trim().split(' ').length < 12) {
|
if (!input || input.trim().split(' ').length < 12) {
|
||||||
return 'Seed phrase must be at least 12 words';
|
return 'Seed phrase must be at least 12 words';
|
||||||
@@ -395,6 +406,23 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
answers.seedPhrase = seedPhrase;
|
answers.seedPhrase = seedPhrase;
|
||||||
|
|
||||||
|
// Ask about sponsor signer if seed phrase is provided
|
||||||
|
const { sponsorSigner } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'sponsorSigner',
|
||||||
|
message:
|
||||||
|
'You have provided a seed phrase, which enables Sign In With Neynar (SIWN).\n' +
|
||||||
|
'Do you want to sponsor the signer? (This will be used in Sign In With Neynar)\n' +
|
||||||
|
'Note: If you choose to sponsor the signer, Neynar will sponsor it for you and you will be charged in CUs.\n' +
|
||||||
|
'For more information, see https://docs.neynar.com/docs/two-ways-to-sponsor-a-farcaster-signer-via-neynar#sponsor-signers',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
answers.useSponsoredSigner = sponsorSigner;
|
||||||
|
} else {
|
||||||
|
answers.useSponsoredSigner = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,8 +548,8 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
|
|||||||
packageJson.dependencies['@neynar/nodejs-sdk'] = '^2.19.0';
|
packageJson.dependencies['@neynar/nodejs-sdk'] = '^2.19.0';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add auth-kit and next-auth dependencies if useSponsoredSigner is true
|
// Add auth-kit and next-auth dependencies if SIWN is enabled (seed phrase is present)
|
||||||
if (answers.useSponsoredSigner) {
|
if (answers.seedPhrase) {
|
||||||
packageJson.dependencies['@farcaster/auth-kit'] = '>=0.6.0 <1.0.0';
|
packageJson.dependencies['@farcaster/auth-kit'] = '>=0.6.0 <1.0.0';
|
||||||
packageJson.dependencies['next-auth'] = '^4.24.11';
|
packageJson.dependencies['next-auth'] = '^4.24.11';
|
||||||
}
|
}
|
||||||
@@ -666,15 +694,16 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
|
|||||||
}
|
}
|
||||||
if (answers.seedPhrase) {
|
if (answers.seedPhrase) {
|
||||||
fs.appendFileSync(envPath, `\nSEED_PHRASE="${answers.seedPhrase}"`);
|
fs.appendFileSync(envPath, `\nSEED_PHRASE="${answers.seedPhrase}"`);
|
||||||
}
|
// Add NextAuth secret for SIWN
|
||||||
fs.appendFileSync(envPath, `\nUSE_TUNNEL="${answers.useTunnel}"`);
|
|
||||||
if (answers.useSponsoredSigner) {
|
|
||||||
fs.appendFileSync(envPath, `\nSPONSOR_SIGNER="true"`);
|
|
||||||
fs.appendFileSync(
|
fs.appendFileSync(
|
||||||
envPath,
|
envPath,
|
||||||
`\nNEXTAUTH_SECRET="${crypto.randomBytes(32).toString('hex')}"`
|
`\nNEXTAUTH_SECRET="${crypto.randomBytes(32).toString('hex')}"`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
fs.appendFileSync(envPath, `\nUSE_TUNNEL="${answers.useTunnel}"`);
|
||||||
|
if (answers.useSponsoredSigner) {
|
||||||
|
fs.appendFileSync(envPath, `\nSPONSOR_SIGNER="true"`);
|
||||||
|
}
|
||||||
|
|
||||||
fs.unlinkSync(envExamplePath);
|
fs.unlinkSync(envExamplePath);
|
||||||
} else {
|
} else {
|
||||||
@@ -718,9 +747,9 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
|
|||||||
fs.rmSync(binPath, { recursive: true, force: true });
|
fs.rmSync(binPath, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove NeynarAuthButton directory, NextAuth API routes, and auth directory if useSponsoredSigner is false
|
// Remove NeynarAuthButton directory, NextAuth API routes, and auth directory if SIWN is not enabled (no seed phrase)
|
||||||
if (!answers.useSponsoredSigner) {
|
if (!answers.seedPhrase) {
|
||||||
console.log('\nRemoving NeynarAuthButton directory, NextAuth API routes, and auth directory (useSponsoredSigner is false)...');
|
console.log('\nRemoving NeynarAuthButton directory, NextAuth API routes, and auth directory (SIWN not enabled)...');
|
||||||
const neynarAuthButtonPath = path.join(projectPath, 'src', 'components', 'ui', 'NeynarAuthButton');
|
const neynarAuthButtonPath = path.join(projectPath, 'src', 'components', 'ui', 'NeynarAuthButton');
|
||||||
if (fs.existsSync(neynarAuthButtonPath)) {
|
if (fs.existsSync(neynarAuthButtonPath)) {
|
||||||
fs.rmSync(neynarAuthButtonPath, { recursive: true, force: true });
|
fs.rmSync(neynarAuthButtonPath, { recursive: true, force: true });
|
||||||
@@ -742,6 +771,21 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
|
|||||||
if (fs.existsSync(authFilePath)) {
|
if (fs.existsSync(authFilePath)) {
|
||||||
fs.rmSync(authFilePath, { force: true });
|
fs.rmSync(authFilePath, { force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replace NeynarAuthButton import in ActionsTab.tsx with null component
|
||||||
|
const actionsTabPath = path.join(projectPath, 'src', 'components', 'ui', 'tabs', 'ActionsTab.tsx');
|
||||||
|
if (fs.existsSync(actionsTabPath)) {
|
||||||
|
let actionsTabContent = fs.readFileSync(actionsTabPath, 'utf8');
|
||||||
|
|
||||||
|
// Replace the dynamic import of NeynarAuthButton with a null component
|
||||||
|
actionsTabContent = actionsTabContent.replace(
|
||||||
|
/const NeynarAuthButton = dynamic\([\s\S]*?\);/,
|
||||||
|
'// NeynarAuthButton disabled - SIWN not enabled\nconst NeynarAuthButton = () => {\n return null;\n};'
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync(actionsTabPath, actionsTabContent);
|
||||||
|
console.log('✅ Replaced NeynarAuthButton import in ActionsTab.tsx with null component');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize git repository
|
// Initialize git repository
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@neynar/create-farcaster-mini-app",
|
"name": "@neynar/create-farcaster-mini-app",
|
||||||
"version": "1.7.11",
|
"version": "1.7.14",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": false,
|
"private": false,
|
||||||
"access": "public",
|
"access": "public",
|
||||||
|
|||||||
@@ -116,13 +116,14 @@ async function checkRequiredEnvVars(): Promise<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ask about sponsor signer if SEED_PHRASE is provided
|
// Ask about SIWN if SEED_PHRASE is provided
|
||||||
if (!process.env.SPONSOR_SIGNER) {
|
if (process.env.SEED_PHRASE && !process.env.SPONSOR_SIGNER) {
|
||||||
const { sponsorSigner } = await inquirer.prompt([
|
const { sponsorSigner } = await inquirer.prompt([
|
||||||
{
|
{
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
name: 'sponsorSigner',
|
name: 'sponsorSigner',
|
||||||
message:
|
message:
|
||||||
|
'You have provided a seed phrase, which enables Sign In With Neynar (SIWN).\n' +
|
||||||
'Do you want to sponsor the signer? (This will be used in Sign In With Neynar)\n' +
|
'Do you want to sponsor the signer? (This will be used in Sign In With Neynar)\n' +
|
||||||
'Note: If you choose to sponsor the signer, Neynar will sponsor it for you and you will be charged in CUs.\n' +
|
'Note: If you choose to sponsor the signer, Neynar will sponsor it for you and you will be charged in CUs.\n' +
|
||||||
'For more information, see https://docs.neynar.com/docs/two-ways-to-sponsor-a-farcaster-signer-via-neynar#sponsor-signers',
|
'For more information, see https://docs.neynar.com/docs/two-ways-to-sponsor-a-farcaster-signer-via-neynar#sponsor-signers',
|
||||||
@@ -132,14 +133,12 @@ async function checkRequiredEnvVars(): Promise<void> {
|
|||||||
|
|
||||||
process.env.SPONSOR_SIGNER = sponsorSigner.toString();
|
process.env.SPONSOR_SIGNER = sponsorSigner.toString();
|
||||||
|
|
||||||
if (process.env.SEED_PHRASE) {
|
|
||||||
fs.appendFileSync(
|
fs.appendFileSync(
|
||||||
'.env.local',
|
'.env.local',
|
||||||
`\nSPONSOR_SIGNER="${sponsorSigner}"`,
|
`\nSPONSOR_SIGNER="${sponsorSigner}"`,
|
||||||
);
|
);
|
||||||
console.log('✅ Sponsor signer preference stored in .env.local');
|
console.log('✅ Sponsor signer preference stored in .env.local');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Ask about required chains
|
// Ask about required chains
|
||||||
const { useRequiredChains } = await inquirer.prompt([
|
const { useRequiredChains } = await inquirer.prompt([
|
||||||
@@ -193,7 +192,7 @@ async function checkRequiredEnvVars(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load SPONSOR_SIGNER from .env.local if SEED_PHRASE exists but SPONSOR_SIGNER doesn't
|
// Load SPONSOR_SIGNER from .env.local if SEED_PHRASE exists (SIWN enabled) but SPONSOR_SIGNER doesn't
|
||||||
if (
|
if (
|
||||||
process.env.SEED_PHRASE &&
|
process.env.SEED_PHRASE &&
|
||||||
!process.env.SPONSOR_SIGNER &&
|
!process.env.SPONSOR_SIGNER &&
|
||||||
@@ -732,8 +731,9 @@ async function deployToVercel(useGitHub = false): Promise<void> {
|
|||||||
SPONSOR_SIGNER: process.env.SPONSOR_SIGNER,
|
SPONSOR_SIGNER: process.env.SPONSOR_SIGNER,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Include NextAuth environment variables if SEED_PHRASE is present or SPONSOR_SIGNER is true
|
// Include NextAuth environment variables if SEED_PHRASE is present (SIWN enabled)
|
||||||
...((process.env.SEED_PHRASE || process.env.SPONSOR_SIGNER === 'true') && {
|
...(process.env.SEED_PHRASE && {
|
||||||
|
SEED_PHRASE: process.env.SEED_PHRASE,
|
||||||
NEXTAUTH_SECRET: nextAuthSecret,
|
NEXTAUTH_SECRET: nextAuthSecret,
|
||||||
AUTH_SECRET: nextAuthSecret,
|
AUTH_SECRET: nextAuthSecret,
|
||||||
NEXTAUTH_URL: `https://${domain}`,
|
NEXTAUTH_URL: `https://${domain}`,
|
||||||
@@ -834,8 +834,8 @@ async function deployToVercel(useGitHub = false): Promise<void> {
|
|||||||
NEXT_PUBLIC_URL: `https://${actualDomain}`,
|
NEXT_PUBLIC_URL: `https://${actualDomain}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Include NextAuth URL if SEED_PHRASE is present or SPONSOR_SIGNER is true
|
// Include NextAuth URL if SEED_PHRASE is present (SIWN enabled)
|
||||||
if (process.env.SEED_PHRASE || process.env.SPONSOR_SIGNER === 'true') {
|
if (process.env.SEED_PHRASE) {
|
||||||
updatedEnv.NEXTAUTH_URL = `https://${actualDomain}`;
|
updatedEnv.NEXTAUTH_URL = `https://${actualDomain}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
85
src/auth.ts
85
src/auth.ts
@@ -217,74 +217,6 @@ function getDomainFromUrl(urlString: string | undefined): string {
|
|||||||
export const authOptions: AuthOptions = {
|
export const authOptions: AuthOptions = {
|
||||||
// Configure one or more authentication providers
|
// Configure one or more authentication providers
|
||||||
providers: [
|
providers: [
|
||||||
CredentialsProvider({
|
|
||||||
id: 'farcaster',
|
|
||||||
name: 'Sign in with Farcaster',
|
|
||||||
credentials: {
|
|
||||||
message: {
|
|
||||||
label: 'Message',
|
|
||||||
type: 'text',
|
|
||||||
placeholder: '0x0',
|
|
||||||
},
|
|
||||||
signature: {
|
|
||||||
label: 'Signature',
|
|
||||||
type: 'text',
|
|
||||||
placeholder: '0x0',
|
|
||||||
},
|
|
||||||
nonce: {
|
|
||||||
label: 'Nonce',
|
|
||||||
type: 'text',
|
|
||||||
placeholder: 'Custom nonce (optional)',
|
|
||||||
},
|
|
||||||
// In a production app with a server, these should be fetched from
|
|
||||||
// your Farcaster data indexer rather than have them accepted as part
|
|
||||||
// of credentials.
|
|
||||||
// question: should these natively use the Neynar API?
|
|
||||||
name: {
|
|
||||||
label: 'Name',
|
|
||||||
type: 'text',
|
|
||||||
placeholder: '0x0',
|
|
||||||
},
|
|
||||||
pfp: {
|
|
||||||
label: 'Pfp',
|
|
||||||
type: 'text',
|
|
||||||
placeholder: '0x0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async authorize(credentials, req) {
|
|
||||||
const nonce = req?.body?.csrfToken;
|
|
||||||
|
|
||||||
if (!nonce) {
|
|
||||||
console.error('No nonce or CSRF token provided');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const appClient = createAppClient({
|
|
||||||
ethereum: viemConnector(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const domain = getDomainFromUrl(process.env.NEXTAUTH_URL);
|
|
||||||
|
|
||||||
const verifyResponse = await appClient.verifySignInMessage({
|
|
||||||
message: credentials?.message as string,
|
|
||||||
signature: credentials?.signature as `0x${string}`,
|
|
||||||
domain,
|
|
||||||
nonce,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { success, fid } = verifyResponse;
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: fid.toString(),
|
|
||||||
name: credentials?.name || `User ${fid}`,
|
|
||||||
image: credentials?.pfp || null,
|
|
||||||
provider: 'farcaster',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
CredentialsProvider({
|
CredentialsProvider({
|
||||||
id: 'neynar',
|
id: 'neynar',
|
||||||
name: 'Sign in with Neynar',
|
name: 'Sign in with Neynar',
|
||||||
@@ -333,10 +265,18 @@ export const authOptions: AuthOptions = {
|
|||||||
try {
|
try {
|
||||||
// Validate the signature using Farcaster's auth client (same as Farcaster provider)
|
// Validate the signature using Farcaster's auth client (same as Farcaster provider)
|
||||||
const appClient = createAppClient({
|
const appClient = createAppClient({
|
||||||
|
// USE your own RPC URL or else you might get 401 error
|
||||||
ethereum: viemConnector(),
|
ethereum: viemConnector(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const domain = getDomainFromUrl(process.env.NEXTAUTH_URL);
|
const baseUrl =
|
||||||
|
process.env.VERCEL_ENV === 'production'
|
||||||
|
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
|
||||||
|
: process.env.VERCEL_URL
|
||||||
|
? `https://${process.env.VERCEL_URL}`
|
||||||
|
: `http://localhost:${process.env.PORT ?? 3000}`;
|
||||||
|
|
||||||
|
const domain = getDomainFromUrl(baseUrl);
|
||||||
|
|
||||||
const verifyResponse = await appClient.verifySignInMessage({
|
const verifyResponse = await appClient.verifySignInMessage({
|
||||||
message: credentials?.message as string,
|
message: credentials?.message as string,
|
||||||
@@ -377,12 +317,7 @@ export const authOptions: AuthOptions = {
|
|||||||
// Set provider at the root level
|
// Set provider at the root level
|
||||||
session.provider = token.provider as string;
|
session.provider = token.provider as string;
|
||||||
|
|
||||||
if (token.provider === 'farcaster') {
|
if (token.provider === 'neynar') {
|
||||||
// For Farcaster, simple structure
|
|
||||||
session.user = {
|
|
||||||
fid: parseInt(token.sub ?? ''),
|
|
||||||
};
|
|
||||||
} else if (token.provider === 'neynar') {
|
|
||||||
// For Neynar, use full user data structure from user
|
// For Neynar, use full user data structure from user
|
||||||
session.user = token.user as typeof session.user;
|
session.user = token.user as typeof session.user;
|
||||||
session.signers = token.signers as typeof session.signers;
|
session.signers = token.signers as typeof session.signers;
|
||||||
|
|||||||
Reference in New Issue
Block a user