agent-manifest 3.2.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -43,7 +43,7 @@ const program = new commander_1.Command();
43
43
  program
44
44
  .name('agentjson')
45
45
  .description('Universal agent manifest compiler — generates agent.json for any web app')
46
- .version('3.1.0')
46
+ .version('4.0.0')
47
47
  .option('-p, --path <path>', 'path to the project root', '.')
48
48
  .option('-o, --output <output>', 'output path for agent.json', './public/agent.json')
49
49
  .option('--author <author>', 'author name or organization')
@@ -51,6 +51,7 @@ program
51
51
  .option('--auth-type <type>', 'override detected auth type: none | bearer | api-key | oauth2 | basic | farcaster-frame | cookie')
52
52
  .option('--auth-header <header>', 'auth header name (default: Authorization)')
53
53
  .option('--auth-docs <url>', 'URL where agents can obtain credentials')
54
+ .option('--base-url <url>', 'canonical base URL for the app (e.g. https://myapp.com)')
54
55
  .action(async (options) => {
55
56
  const projectPath = path.resolve(options.path);
56
57
  const outputPath = path.resolve(options.output);
@@ -59,18 +60,71 @@ program
59
60
  const files = await discovery.findRelevantFiles();
60
61
  console.log(`🔍 Found ${files.length} relevant files.`);
61
62
  const tsParser = new index_1.TSParser(projectPath);
62
- const expressParser = new index_1.ExpressParser(tsParser.getProject());
63
+ const sharedProject = tsParser.getProject();
64
+ const expressParser = new index_1.ExpressParser(sharedProject);
65
+ const socketParser = new index_1.SocketIOParser(sharedProject);
66
+ const trpcParser = new index_1.TRPCParser(sharedProject);
67
+ const sseParser = new index_1.SSEParser(sharedProject);
68
+ const remixParser = new index_1.RemixParser(sharedProject);
69
+ const wsParser = new index_1.WebSocketParser(sharedProject);
70
+ const openApiParser = new index_1.OpenAPIParser();
71
+ const prismaParser = new index_1.PrismaParser();
63
72
  const actions = [];
73
+ const dataModel = {};
74
+ // Determine which files are OpenAPI / Prisma by extension/name
75
+ const openApiExts = new Set(['.json', '.yaml', '.yml']);
76
+ const prismaExt = '.prisma';
64
77
  for (const file of files) {
65
78
  console.log(`📄 Parsing: ${path.relative(projectPath, file)}`);
66
- const fileActions = await tsParser.parseFile(file);
67
- actions.push(...fileActions);
68
- if (file.endsWith('.ts') || file.endsWith('.js')) {
79
+ const ext = path.extname(file).toLowerCase();
80
+ const base = path.basename(file).toLowerCase();
81
+ // ── OpenAPI spec ──────────────────────────────────────────────────────
82
+ if (openApiExts.has(ext) && index_1.OPENAPI_PATTERNS.some(p => {
83
+ const pat = p.replace('**/', '').replace('*', '');
84
+ return base.includes(pat.split('.')[0]);
85
+ })) {
86
+ try {
87
+ actions.push(...openApiParser.parseFile(file, projectPath));
88
+ }
89
+ catch { /* ignore */ }
90
+ continue;
91
+ }
92
+ // ── Prisma schema ─────────────────────────────────────────────────────
93
+ if (ext === prismaExt || base === 'schema.prisma') {
94
+ try {
95
+ const prismaResult = prismaParser.parseFile(file);
96
+ Object.assign(dataModel, prismaResult.dataModel);
97
+ }
98
+ catch { /* ignore */ }
99
+ continue;
100
+ }
101
+ // ── TypeScript / JavaScript source files ──────────────────────────────
102
+ if (ext === '.ts' || ext === '.tsx' || ext === '.js' || ext === '.jsx') {
103
+ // Standard TS server-action / contract / annotation parsing
104
+ try {
105
+ const fileActions = await tsParser.parseFile(file);
106
+ actions.push(...fileActions);
107
+ }
108
+ catch { /* ignore */ }
69
109
  try {
70
110
  const content = fs.readFileSync(file, 'utf8');
71
111
  if ((0, index_1.looksLikeRouteFile)(content)) {
72
- const expressActions = await expressParser.parseFile(file, projectPath);
73
- actions.push(...expressActions);
112
+ actions.push(...await expressParser.parseFile(file, projectPath));
113
+ }
114
+ if ((0, index_1.looksLikeSocketIOFile)(content)) {
115
+ actions.push(...await socketParser.parseFile(file, projectPath));
116
+ }
117
+ if ((0, index_1.looksLikeTRPCFile)(content)) {
118
+ actions.push(...await trpcParser.parseFile(file, projectPath));
119
+ }
120
+ if ((0, index_1.looksLikeSSEFile)(content)) {
121
+ actions.push(...await sseParser.parseFile(file, projectPath));
122
+ }
123
+ if ((0, index_1.looksLikeRemixRouteFile)(content)) {
124
+ actions.push(...await remixParser.parseFile(file, projectPath));
125
+ }
126
+ if ((0, index_1.looksLikeWebSocketFile)(content)) {
127
+ actions.push(...await wsParser.parseFile(file, projectPath));
74
128
  }
75
129
  }
76
130
  catch { /* ignore */ }
@@ -80,7 +134,7 @@ program
80
134
  const uniqueActions = new Map();
81
135
  for (const action of actions) {
82
136
  const existing = uniqueActions.get(action.name);
83
- if (!existing || Object.keys(action.inputs).length > Object.keys(existing.inputs).length) {
137
+ if (!existing || Object.keys(action.parameters?.properties ?? {}).length > Object.keys(existing.parameters?.properties ?? {}).length) {
84
138
  uniqueActions.set(action.name, action);
85
139
  }
86
140
  }
@@ -97,11 +151,18 @@ program
97
151
  ...(options.authDocs && { docsUrl: options.authDocs }),
98
152
  };
99
153
  const generator = new index_1.ManifestGenerator();
100
- const manifest = generator.generate(Array.from(uniqueActions.values()), appMetadata, tsParser.getCapabilities(), auth);
154
+ const manifest = generator.generate(Array.from(uniqueActions.values()), appMetadata, tsParser.getCapabilities(), auth, '1.0.0', {
155
+ baseUrl: options.baseUrl ?? appMetadata.url,
156
+ dataModel,
157
+ });
101
158
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
102
159
  fs.writeFileSync(outputPath, JSON.stringify(manifest, null, 2));
160
+ const uiCount = Array.from(uniqueActions.values()).filter(a => a.type === 'ui').length;
161
+ const modelCount = Object.keys(dataModel).length;
103
162
  console.log(`✅ agent.json generated at: ${outputPath}`);
104
- console.log(` ${uniqueActions.size} actions · ${manifest.capabilities.length} capabilities · auth: ${auth.type}`);
163
+ console.log(` ${uniqueActions.size} actions${uiCount ? ` · ${uiCount} ui-interactions` : ''} · ${manifest.capabilities.length} capabilities · auth: ${auth.type}`);
164
+ if (modelCount > 0)
165
+ console.log(` dataModel: ${modelCount} models`);
105
166
  });
106
167
  // ─── validate ────────────────────────────────────────────────────────────────
107
168
  program
@@ -135,8 +196,12 @@ program
135
196
  });
136
197
  // ─── Structural validator ────────────────────────────────────────────────────
137
198
  const SAFETY_LEVELS = new Set(['read', 'write', 'financial', 'destructive', 'confidential']);
138
- const ACTION_TYPES = new Set(['api', 'contract', 'function']);
139
- const AUTH_TYPES = new Set(['none', 'bearer', 'api-key', 'oauth2', 'basic', 'farcaster-frame', 'cookie']);
199
+ const ACTION_TYPES = new Set(['api', 'contract', 'function', 'socket', 'ui']);
200
+ const AUTH_TYPES = new Set([
201
+ 'none', 'bearer', 'api-key', 'oauth2', 'basic', 'cookie',
202
+ 'siwe', 'farcaster-siwf', 'farcaster-frame',
203
+ 'clerk', 'privy', 'dynamic', 'magic', 'passkey', 'saml', 'supabase',
204
+ ]);
140
205
  const INTENT_RE = /^[a-z][a-z0-9]*\.[a-z][a-z0-9]*$/;
141
206
  function validateManifest(m) {
142
207
  const errors = [];
@@ -160,13 +225,19 @@ function validateManifest(m) {
160
225
  else if (!INTENT_RE.test(action.intent))
161
226
  errors.push(`${prefix}: \`intent\` must match domain.verb format`);
162
227
  if (!ACTION_TYPES.has(action.type))
163
- errors.push(`${prefix}: \`type\` must be one of api|contract|function`);
228
+ errors.push(`${prefix}: \`type\` must be one of api|contract|function|socket|ui`);
164
229
  if (!SAFETY_LEVELS.has(action.safety))
165
230
  errors.push(`${prefix}: \`safety\` must be one of read|write|financial|destructive|confidential`);
166
231
  if (typeof action.agentSafe !== 'boolean')
167
232
  errors.push(`${prefix}: \`agentSafe\` must be a boolean`);
168
233
  if (!action.requiredAuth)
169
234
  errors.push(`${prefix}: \`requiredAuth\` is missing`);
235
+ else if (!['public', 'required', 'farcaster-signed'].includes(action.requiredAuth.required))
236
+ errors.push(`${prefix}: \`requiredAuth.required\` must be "public", "required", or "farcaster-signed"`);
237
+ if (!action.parameters || typeof action.parameters.properties !== 'object')
238
+ errors.push(`${prefix}: \`parameters.properties\` must be an object`);
239
+ if (!action.returns || typeof action.returns.type !== 'string')
240
+ errors.push(`${prefix}: \`returns.type\` must be a string`);
170
241
  });
171
242
  }
172
243
  return errors;
@@ -39,6 +39,13 @@ const path = __importStar(require("path"));
39
39
  const crypto = __importStar(require("crypto"));
40
40
  const tinyglobby_1 = require("tinyglobby");
41
41
  const express_parser_1 = require("../parser/express-parser");
42
+ const socketio_parser_1 = require("../parser/socketio-parser");
43
+ const trpc_parser_1 = require("../parser/trpc-parser");
44
+ const sse_parser_1 = require("../parser/sse-parser");
45
+ const remix_parser_1 = require("../parser/remix-parser");
46
+ const websocket_parser_1 = require("../parser/websocket-parser");
47
+ const openapi_parser_1 = require("../parser/openapi-parser");
48
+ const prisma_parser_1 = require("../parser/prisma-parser");
42
49
  /** SHA-1 of a file's content — used for change detection caching. */
43
50
  function fileHash(filePath) {
44
51
  try {
@@ -60,12 +67,20 @@ const ALWAYS_EXCLUDE = [
60
67
  '!**/out/**',
61
68
  '!**/build/**',
62
69
  '!**/.vercel/**',
63
- // Never scan env files — they may contain secrets
64
70
  '!**/.env',
65
71
  '!**/.env.*',
66
72
  '!**/secrets/**',
67
73
  '!**/credentials/**',
68
74
  ];
75
+ /**
76
+ * Checks if a path is strictly within the project directory.
77
+ * Prevents path traversal via symbolic links or malformed paths.
78
+ */
79
+ function isPathWithinProject(filePath, projectPath) {
80
+ const resolved = path.resolve(filePath);
81
+ const root = path.resolve(projectPath);
82
+ return resolved.startsWith(root);
83
+ }
69
84
  class DiscoveryService {
70
85
  projectPath;
71
86
  cache = {};
@@ -94,14 +109,14 @@ class DiscoveryService {
94
109
  }
95
110
  async findRelevantFiles() {
96
111
  const relevantFiles = [];
97
- // 0. Farcaster manifest (app identity / metadata)
112
+ // 0. Farcaster manifest
98
113
  const manifests = await (0, tinyglobby_1.glob)([
99
114
  '.well-known/farcaster.json',
100
115
  'public/.well-known/farcaster.json',
101
116
  '**/public/.well-known/farcaster.json',
102
117
  ], { cwd: this.projectPath, absolute: true });
103
118
  relevantFiles.push(...manifests);
104
- // 0.5. ABI JSON files (smart contract definitions)
119
+ // 0.5. ABI JSON files
105
120
  const abis = await (0, tinyglobby_1.glob)([
106
121
  '**/*ABI.json',
107
122
  '**/abi/*.json',
@@ -111,34 +126,50 @@ class DiscoveryService {
111
126
  ...ALWAYS_EXCLUDE,
112
127
  ], { cwd: this.projectPath, absolute: true });
113
128
  relevantFiles.push(...abis);
114
- // 1. API routes — support monorepo layouts (apps/*/src/app/api, apps/*/pages/api, etc.)
129
+ // 0.6. OpenAPI / Swagger specs
130
+ const openApiFiles = await (0, tinyglobby_1.glob)([
131
+ ...openapi_parser_1.OPENAPI_PATTERNS,
132
+ ...ALWAYS_EXCLUDE,
133
+ ], { cwd: this.projectPath, absolute: true });
134
+ relevantFiles.push(...openApiFiles);
135
+ // 0.7. Prisma schema files
136
+ const prismaFiles = await (0, tinyglobby_1.glob)([
137
+ ...prisma_parser_1.PRISMA_PATTERNS,
138
+ ...ALWAYS_EXCLUDE,
139
+ ], { cwd: this.projectPath, absolute: true });
140
+ relevantFiles.push(...prismaFiles);
141
+ // 1. Next.js API routes
115
142
  const apiRoutes = await (0, tinyglobby_1.glob)([
116
- // Next.js App Router
117
143
  '**/app/api/**/*.{ts,js,tsx,jsx}',
118
- // Next.js Pages Router
119
144
  '**/pages/api/**/*.{ts,js,tsx,jsx}',
120
- // Generic api/ folder
121
145
  '**/api/**/*.{ts,js,tsx,jsx}',
122
146
  ...ALWAYS_EXCLUDE,
123
147
  ], { cwd: this.projectPath, absolute: true });
124
148
  relevantFiles.push(...apiRoutes);
125
- // 2. Scan all TS/TSX files for signal keywords
149
+ // 2. Remix route files
150
+ const remixRoutes = await (0, tinyglobby_1.glob)([
151
+ '**/app/routes/**/*.{ts,js,tsx,jsx}',
152
+ '**/routes/**/*.{ts,js,tsx,jsx}',
153
+ ...ALWAYS_EXCLUDE,
154
+ ], { cwd: this.projectPath, absolute: true });
155
+ relevantFiles.push(...remixRoutes);
156
+ // 3. Scan all TS/TSX/JS files for signal keywords
126
157
  const allTsFiles = await (0, tinyglobby_1.glob)([
127
- '**/*.{ts,tsx}',
158
+ '**/*.{ts,tsx,js,jsx}',
128
159
  ...ALWAYS_EXCLUDE,
129
160
  ], { cwd: this.projectPath, absolute: true });
130
161
  for (const file of allTsFiles) {
162
+ if (!isPathWithinProject(file, this.projectPath))
163
+ continue;
131
164
  if (relevantFiles.includes(file))
132
165
  continue;
133
166
  const hash = fileHash(file);
134
167
  const cached = this.cache[file];
135
- // Cache hit: file unchanged, reuse previous relevance decision
136
168
  if (cached && cached.hash === hash) {
137
169
  if (cached.relevant)
138
170
  relevantFiles.push(file);
139
171
  continue;
140
172
  }
141
- // Cache miss: read and classify
142
173
  const content = fs.readFileSync(file, 'utf8');
143
174
  const relevant = content.includes('@agent-action') ||
144
175
  content.includes('useWriteContract') ||
@@ -146,7 +177,12 @@ class DiscoveryService {
146
177
  content.includes('writeContract') ||
147
178
  content.includes("'use server'") ||
148
179
  content.includes('"use server"') ||
149
- (0, express_parser_1.looksLikeRouteFile)(content);
180
+ (0, express_parser_1.looksLikeRouteFile)(content) ||
181
+ (0, socketio_parser_1.looksLikeSocketIOFile)(content) ||
182
+ (0, trpc_parser_1.looksLikeTRPCFile)(content) ||
183
+ (0, sse_parser_1.looksLikeSSEFile)(content) ||
184
+ (0, remix_parser_1.looksLikeRemixRouteFile)(content) ||
185
+ (0, websocket_parser_1.looksLikeWebSocketFile)(content);
150
186
  this.cache[file] = { hash, relevant };
151
187
  this.cacheModified = true;
152
188
  if (relevant)
@@ -2,13 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ManifestGenerator = void 0;
4
4
  class ManifestGenerator {
5
- generate(actions, metadata = {}, capabilities = [], auth = { type: 'none' }, version = '1.0.0') {
5
+ generate(actions, metadata = {}, capabilities = [], auth = { type: 'none' }, version = '1.0.0', options = {}) {
6
6
  return {
7
7
  name: metadata.name ?? 'Web App',
8
8
  description: metadata.description ?? 'Auto-generated agent manifest',
9
9
  version,
10
10
  ...(metadata.author && { author: metadata.author }),
11
11
  ...(metadata.url && { url: metadata.url }),
12
+ ...(options.baseUrl && { baseUrl: options.baseUrl }),
12
13
  auth,
13
14
  metadata: {
14
15
  ...(metadata.iconUrl && { iconUrl: metadata.iconUrl }),
@@ -19,6 +20,7 @@ class ManifestGenerator {
19
20
  },
20
21
  capabilities,
21
22
  actions,
23
+ ...(options.dataModel && Object.keys(options.dataModel).length > 0 && { dataModel: options.dataModel }),
22
24
  };
23
25
  }
24
26
  }
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.CapabilityDetector = exports.AuthDetector = exports.ManifestGenerator = exports.ZodExtractor = exports.ContractParser = exports.looksLikeRouteFile = exports.ExpressParser = exports.TSParser = exports.DiscoveryService = void 0;
17
+ exports.CapabilityDetector = exports.AuthDetector = exports.ManifestGenerator = exports.ZodExtractor = exports.ContractParser = exports.PRISMA_PATTERNS = exports.PrismaParser = exports.OPENAPI_PATTERNS = exports.OpenAPIParser = exports.looksLikeWebSocketFile = exports.WebSocketParser = exports.looksLikeRemixRouteFile = exports.RemixParser = exports.looksLikeSSEFile = exports.SSEParser = exports.looksLikeTRPCFile = exports.TRPCParser = exports.looksLikeSocketIOFile = exports.SocketIOParser = exports.looksLikeRouteFile = exports.ExpressParser = exports.TSParser = exports.DiscoveryService = void 0;
18
18
  __exportStar(require("./types"), exports);
19
19
  var service_1 = require("./discovery/service");
20
20
  Object.defineProperty(exports, "DiscoveryService", { enumerable: true, get: function () { return service_1.DiscoveryService; } });
@@ -23,6 +23,27 @@ Object.defineProperty(exports, "TSParser", { enumerable: true, get: function ()
23
23
  var express_parser_1 = require("./parser/express-parser");
24
24
  Object.defineProperty(exports, "ExpressParser", { enumerable: true, get: function () { return express_parser_1.ExpressParser; } });
25
25
  Object.defineProperty(exports, "looksLikeRouteFile", { enumerable: true, get: function () { return express_parser_1.looksLikeRouteFile; } });
26
+ var socketio_parser_1 = require("./parser/socketio-parser");
27
+ Object.defineProperty(exports, "SocketIOParser", { enumerable: true, get: function () { return socketio_parser_1.SocketIOParser; } });
28
+ Object.defineProperty(exports, "looksLikeSocketIOFile", { enumerable: true, get: function () { return socketio_parser_1.looksLikeSocketIOFile; } });
29
+ var trpc_parser_1 = require("./parser/trpc-parser");
30
+ Object.defineProperty(exports, "TRPCParser", { enumerable: true, get: function () { return trpc_parser_1.TRPCParser; } });
31
+ Object.defineProperty(exports, "looksLikeTRPCFile", { enumerable: true, get: function () { return trpc_parser_1.looksLikeTRPCFile; } });
32
+ var sse_parser_1 = require("./parser/sse-parser");
33
+ Object.defineProperty(exports, "SSEParser", { enumerable: true, get: function () { return sse_parser_1.SSEParser; } });
34
+ Object.defineProperty(exports, "looksLikeSSEFile", { enumerable: true, get: function () { return sse_parser_1.looksLikeSSEFile; } });
35
+ var remix_parser_1 = require("./parser/remix-parser");
36
+ Object.defineProperty(exports, "RemixParser", { enumerable: true, get: function () { return remix_parser_1.RemixParser; } });
37
+ Object.defineProperty(exports, "looksLikeRemixRouteFile", { enumerable: true, get: function () { return remix_parser_1.looksLikeRemixRouteFile; } });
38
+ var websocket_parser_1 = require("./parser/websocket-parser");
39
+ Object.defineProperty(exports, "WebSocketParser", { enumerable: true, get: function () { return websocket_parser_1.WebSocketParser; } });
40
+ Object.defineProperty(exports, "looksLikeWebSocketFile", { enumerable: true, get: function () { return websocket_parser_1.looksLikeWebSocketFile; } });
41
+ var openapi_parser_1 = require("./parser/openapi-parser");
42
+ Object.defineProperty(exports, "OpenAPIParser", { enumerable: true, get: function () { return openapi_parser_1.OpenAPIParser; } });
43
+ Object.defineProperty(exports, "OPENAPI_PATTERNS", { enumerable: true, get: function () { return openapi_parser_1.OPENAPI_PATTERNS; } });
44
+ var prisma_parser_1 = require("./parser/prisma-parser");
45
+ Object.defineProperty(exports, "PrismaParser", { enumerable: true, get: function () { return prisma_parser_1.PrismaParser; } });
46
+ Object.defineProperty(exports, "PRISMA_PATTERNS", { enumerable: true, get: function () { return prisma_parser_1.PRISMA_PATTERNS; } });
26
47
  var contract_parser_1 = require("./parser/contract-parser");
27
48
  Object.defineProperty(exports, "ContractParser", { enumerable: true, get: function () { return contract_parser_1.ContractParser; } });
28
49
  var zod_extractor_1 = require("./parser/zod-extractor");
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.AuthDetector = void 0;
36
+ exports.AuthFlowInferrer = exports.AuthDetector = void 0;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const AUTH_SIGNALS = [
@@ -42,6 +42,15 @@ const AUTH_SIGNALS = [
42
42
  { pattern: '@farcaster/frame-node', type: 'farcaster-frame', priority: 100 },
43
43
  { pattern: 'validateFrameMessage', type: 'farcaster-frame', priority: 100 },
44
44
  { pattern: 'frameActionPayload', type: 'farcaster-frame', priority: 100 },
45
+ // Farcaster SIWF — must be before SIWE (higher priority)
46
+ { pattern: '@farcaster/auth-kit', type: 'farcaster-siwf', priority: 98 },
47
+ { pattern: 'verifySignInMessage', type: 'farcaster-siwf', priority: 98 },
48
+ { pattern: 'useSignIn', type: 'farcaster-siwf', priority: 95 },
49
+ // SIWE
50
+ { pattern: 'SiweMessage', type: 'siwe', priority: 95 },
51
+ { pattern: "from 'siwe'", type: 'siwe', priority: 95 },
52
+ { pattern: 'verifySiweMessage', type: 'siwe', priority: 95 },
53
+ { pattern: 'generateNonce', type: 'siwe', priority: 90 },
45
54
  // OAuth2 / NextAuth / Auth.js
46
55
  { pattern: 'NextAuth', type: 'oauth2', priority: 90 },
47
56
  { pattern: 'next-auth', type: 'oauth2', priority: 90 },
@@ -51,8 +60,14 @@ const AUTH_SIGNALS = [
51
60
  { pattern: 'oauth2', type: 'oauth2', priority: 80 },
52
61
  { pattern: 'access_token', type: 'oauth2', priority: 75 },
53
62
  { pattern: 'refresh_token', type: 'oauth2', priority: 75 },
54
- { pattern: 'clerkMiddleware', type: 'bearer', scheme: 'Bearer', priority: 88 },
55
- { pattern: 'currentUser', type: 'bearer', scheme: 'Bearer', priority: 70 },
63
+ // SAML
64
+ { pattern: 'samlify', type: 'saml', priority: 92 },
65
+ { pattern: 'passport-saml', type: 'saml', priority: 92 },
66
+ { pattern: '@node-saml', type: 'saml', priority: 92 },
67
+ { pattern: 'SAMLResponse', type: 'saml', priority: 88 },
68
+ // Clerk
69
+ { pattern: 'clerkMiddleware', type: 'clerk', priority: 88 },
70
+ { pattern: 'currentUser', type: 'clerk', priority: 70 },
56
71
  // JWT / Bearer
57
72
  { pattern: 'jwt.verify', type: 'bearer', header: 'Authorization', scheme: 'Bearer', priority: 70 },
58
73
  { pattern: 'jsonwebtoken', type: 'bearer', header: 'Authorization', scheme: 'Bearer', priority: 70 },
@@ -61,6 +76,25 @@ const AUTH_SIGNALS = [
61
76
  { pattern: 'authorization.split', type: 'bearer', header: 'Authorization', scheme: 'Bearer', priority: 70 },
62
77
  { pattern: "headers.get('authorization')", type: 'bearer', header: 'Authorization', scheme: 'Bearer', priority: 65 },
63
78
  { pattern: "req.headers.authorization", type: 'bearer', header: 'Authorization', scheme: 'Bearer', priority: 65 },
79
+ // Privy
80
+ { pattern: '@privy-io/react-auth', type: 'privy', priority: 85 },
81
+ { pattern: 'usePrivy', type: 'privy', priority: 85 },
82
+ { pattern: 'PrivyProvider', type: 'privy', priority: 85 },
83
+ // Dynamic
84
+ { pattern: '@dynamic-labs/sdk-react-core', type: 'dynamic', priority: 85 },
85
+ { pattern: 'DynamicContextProvider', type: 'dynamic', priority: 85 },
86
+ { pattern: 'useDynamicContext', type: 'dynamic', priority: 80 },
87
+ // Magic
88
+ { pattern: 'magic-sdk', type: 'magic', priority: 80 },
89
+ { pattern: '@magic-sdk', type: 'magic', priority: 80 },
90
+ { pattern: 'new Magic(', type: 'magic', priority: 80 },
91
+ // Passkey / WebAuthn
92
+ { pattern: '@simplewebauthn/browser', type: 'passkey', priority: 78 },
93
+ { pattern: 'startAuthentication', type: 'passkey', priority: 78 },
94
+ { pattern: 'startRegistration', type: 'passkey', priority: 78 },
95
+ { pattern: 'navigator.credentials.create', type: 'passkey', priority: 75 },
96
+ // Supabase
97
+ { pattern: 'supabase.auth', type: 'supabase', priority: 75 },
64
98
  // API key
65
99
  { pattern: "'x-api-key'", type: 'api-key', header: 'X-API-Key', priority: 60 },
66
100
  { pattern: '"x-api-key"', type: 'api-key', header: 'X-API-Key', priority: 60 },
@@ -80,6 +114,7 @@ const AUTH_SIGNALS = [
80
114
  ];
81
115
  class AuthDetector {
82
116
  best = null;
117
+ _flowInferrer = new AuthFlowInferrer();
83
118
  scanContent(content) {
84
119
  for (const signal of AUTH_SIGNALS) {
85
120
  if (content.includes(signal.pattern)) {
@@ -88,6 +123,7 @@ class AuthDetector {
88
123
  }
89
124
  }
90
125
  }
126
+ this._flowInferrer.addContent(content);
91
127
  }
92
128
  /** Read package.json to detect auth libraries in dependencies */
93
129
  readPackageJson(projectPath) {
@@ -99,8 +135,24 @@ class AuthDetector {
99
135
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
100
136
  if (deps['next-auth'] || deps['@auth/core'])
101
137
  this.applySignal({ pattern: 'next-auth', type: 'oauth2', priority: 90 });
138
+ if (deps['@farcaster/auth-kit'])
139
+ this.applySignal({ pattern: 'farcaster-siwf', type: 'farcaster-siwf', priority: 98 });
140
+ if (deps['siwe'])
141
+ this.applySignal({ pattern: 'siwe', type: 'siwe', priority: 95 });
142
+ if (deps['samlify'] || deps['passport-saml'])
143
+ this.applySignal({ pattern: 'saml', type: 'saml', priority: 92 });
102
144
  if (deps['@clerk/nextjs'] || deps['@clerk/clerk-sdk-node'])
103
- this.applySignal({ pattern: 'clerk', type: 'bearer', header: 'Authorization', scheme: 'Bearer', priority: 88 });
145
+ this.applySignal({ pattern: 'clerk', type: 'clerk', priority: 88 });
146
+ if (deps['@privy-io/react-auth'])
147
+ this.applySignal({ pattern: 'privy', type: 'privy', priority: 85 });
148
+ if (deps['@dynamic-labs/sdk-react-core'])
149
+ this.applySignal({ pattern: 'dynamic', type: 'dynamic', priority: 85 });
150
+ if (deps['magic-sdk'] || deps['@magic-sdk/admin'])
151
+ this.applySignal({ pattern: 'magic', type: 'magic', priority: 80 });
152
+ if (deps['@simplewebauthn/browser'])
153
+ this.applySignal({ pattern: 'passkey', type: 'passkey', priority: 78 });
154
+ if (deps['@supabase/supabase-js'])
155
+ this.applySignal({ pattern: 'supabase', type: 'supabase', priority: 75 });
104
156
  if (deps['jsonwebtoken'] || deps['jose'])
105
157
  this.applySignal({ pattern: 'jwt', type: 'bearer', header: 'Authorization', scheme: 'Bearer', priority: 70 });
106
158
  if (deps['iron-session'])
@@ -118,14 +170,44 @@ class AuthDetector {
118
170
  getAuth() {
119
171
  if (!this.best)
120
172
  return { type: 'none' };
121
- const config = { type: this.best.type };
173
+ const type = this.best.type;
174
+ const config = { type };
122
175
  if (this.best.header)
123
176
  config.header = this.best.header;
124
177
  if (this.best.scheme)
125
178
  config.scheme = this.best.scheme;
126
179
  if (this.best.queryParam)
127
180
  config.queryParam = this.best.queryParam;
128
- return config;
181
+ const flowFields = this._flowInferrer.infer(type);
182
+ return { ...config, ...flowFields };
129
183
  }
130
184
  }
131
185
  exports.AuthDetector = AuthDetector;
186
+ class AuthFlowInferrer {
187
+ scannedContents = [];
188
+ addContent(content) {
189
+ this.scannedContents.push(content);
190
+ }
191
+ infer(authType) {
192
+ const result = {};
193
+ for (const content of this.scannedContents) {
194
+ if (!result.nonceUrl && (authType === 'siwe' || authType === 'farcaster-siwf')) {
195
+ const m = content.match(/['"`](\/api\/auth\/nonce[^'"`\s]*)['"`]/);
196
+ if (m)
197
+ result.nonceUrl = m[1];
198
+ }
199
+ if (!result.callbackUrl && authType === 'oauth2') {
200
+ const m = content.match(/['"`](\/api\/auth\/callback[^'"`\s]*)['"`]/);
201
+ if (m)
202
+ result.callbackUrl = m[1];
203
+ }
204
+ if (!result.loginUrl) {
205
+ const m = content.match(/['"`](\/sign-in|\/login|\/auth\/login)['"`]/);
206
+ if (m)
207
+ result.loginUrl = m[1];
208
+ }
209
+ }
210
+ return result;
211
+ }
212
+ }
213
+ exports.AuthFlowInferrer = AuthFlowInferrer;
@@ -93,10 +93,10 @@ class ContractParser {
93
93
  abiFunction: item.name,
94
94
  isReadOnly,
95
95
  safety,
96
- agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety),
96
+ agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety, item.name),
97
97
  requiredAuth: (0, intent_classifier_1.inferActionAuth)({ safety, isReadOnly, type: 'contract' }),
98
- inputs: this.mapAbiInputs(item.inputs ?? []),
99
- outputs: {
98
+ parameters: { properties: this.mapAbiInputs(item.inputs ?? []) },
99
+ returns: {
100
100
  type: this.mapAbiOutputs(item.outputs ?? []),
101
101
  description: '',
102
102
  },
@@ -172,10 +172,10 @@ class ContractParser {
172
172
  ...(chainId !== undefined ? { chainId } : {}),
173
173
  ...(contractAddress !== undefined ? { contractAddress } : {}),
174
174
  safety,
175
- agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety),
175
+ agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety, functionName),
176
176
  requiredAuth: (0, intent_classifier_1.inferActionAuth)({ safety, isReadOnly: false, type: 'contract' }),
177
- inputs: parameters,
178
- outputs: { type: 'any' },
177
+ parameters: { properties: parameters },
178
+ returns: { type: 'any' },
179
179
  });
180
180
  });
181
181
  return actions;
@@ -113,10 +113,10 @@ class ExpressParser {
113
113
  location: routePath,
114
114
  method: httpMethod,
115
115
  safety,
116
- agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety),
116
+ agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety, actionName),
117
117
  requiredAuth: (0, intent_classifier_1.inferActionAuth)({ safety, httpMethod, type: 'api' }),
118
- inputs: this.extractRouteParams(routePath),
119
- outputs: { type: 'any' },
118
+ parameters: { properties: this.extractRouteParams(routePath) },
119
+ returns: { type: 'any' },
120
120
  });
121
121
  });
122
122
  return actions;
@@ -9,7 +9,7 @@ const INTENT_RULES = [
9
9
  // getUsers don't have a word boundary after the verb segment. Leading \b is kept
10
10
  // to avoid matching mid-word (e.g. "undo" matching "do").
11
11
  // Game
12
- { pattern: /\b(play|flip|roll|spin|bet|guess|draw|move|attack|defend|claim(?:Prize|Reward|Win))/i, intent: 'game.play' },
12
+ { pattern: /\b(play|flip|roll|spin|bet|guess|draw|move|make(?:Move)?|drop(?:Piece)?|attack|defend|claim(?:Prize|Reward|Win))/i, intent: 'game.play' },
13
13
  { pattern: /\b(score|leaderboard|rank|highscore)/i, intent: 'game.score' },
14
14
  { pattern: /\b(join(?:Game|Room|Lobby)|create(?:Game|Room)|start(?:Game|Round))/i, intent: 'game.join' },
15
15
  // Finance / DeFi
@@ -25,16 +25,16 @@ const INTENT_RULES = [
25
25
  { pattern: /\b(listNFT|sellNFT|listFor(?:Sale)?)/i, intent: 'nft.list' },
26
26
  { pattern: /\b(buyNFT|purchaseNFT)/i, intent: 'nft.buy' },
27
27
  { pattern: /\b(burnNFT|burn)/i, intent: 'nft.burn' },
28
+ // Governance (must come BEFORE Social so castVote → governance.vote, not social.cast)
29
+ { pattern: /\b(vote|castVote|submitVote)/i, intent: 'governance.vote' },
30
+ { pattern: /\b(propose|createProposal|submitProposal)/i, intent: 'governance.propose' },
31
+ { pattern: /\b(delegate|undelegate)/i, intent: 'governance.delegate' },
28
32
  // Social (Farcaster-native)
29
- { pattern: /\b(cast|compose(?:Cast)?|post(?:Cast)?)/i, intent: 'social.cast' },
33
+ { pattern: /\b(cast(?!Vote)|compose(?:Cast)?|post(?:Cast)?)/i, intent: 'social.cast' },
30
34
  { pattern: /\b(follow|unfollow|subscribe)/i, intent: 'social.follow' },
31
35
  { pattern: /\b(like|react|upvote|downvote)/i, intent: 'social.react' },
32
36
  { pattern: /\b(comment|reply)/i, intent: 'social.reply' },
33
37
  { pattern: /\b(share|recast|repost)/i, intent: 'social.share' },
34
- // Governance
35
- { pattern: /\b(vote|castVote|submitVote)/i, intent: 'governance.vote' },
36
- { pattern: /\b(propose|createProposal|submitProposal)/i, intent: 'governance.propose' },
37
- { pattern: /\b(delegate|undelegate)/i, intent: 'governance.delegate' },
38
38
  // Auth
39
39
  { pattern: /\b(login|logout|signIn|signOut|connect|disconnect)/i, intent: 'auth.session' },
40
40
  { pattern: /\b(register|signup|createAccount)/i, intent: 'auth.register' },
@@ -73,7 +73,9 @@ const DESTRUCTIVE_VERBS = /\b(delete|remove|destroy|burn|archive|purge|wipe|clea
73
73
  * updateSsn, exportPrivateKey, getMedicalRecord.
74
74
  */
75
75
  // No \b — these nouns appear anywhere in camelCase (resetPassword, submitKyc, getSsn)
76
- const CONFIDENTIAL_NOUNS = /(password|credential|privateKey|secretKey|biometric|ssn|taxId|pii|kyc|medicalRecord|healthRecord|passport|driverLicense|creditCard|cvv|encryptedData|identityVerif)/i;
76
+ const CONFIDENTIAL_NOUNS = /(password|credential|privateKey|secretKey|biometric|ssn|taxId|tax_id|pii|kyc|medical|health|passport|license|creditCard|cvv|encrypted|identity|dob|birthDate|address|phone|mobile|email)/i;
77
+ /** Verbs that imply high-privilege administrative or sensitive state changes. */
78
+ const SENSITIVE_WRITE_VERBS = /\b(admin|role|permission|password|owner|ban|block|lock|unlock|grant|revoke|delegate|sudo|root|secret)/i;
77
79
  /**
78
80
  * Classify the safety level of an action.
79
81
  *
@@ -97,19 +99,40 @@ function classifySafety(opts) {
97
99
  return 'confidential';
98
100
  return 'write';
99
101
  }
102
+ // Financial and confidential always win regardless of type
100
103
  if (FINANCIAL_VERBS.test(name))
101
104
  return 'financial';
102
105
  if (CONFIDENTIAL_NOUNS.test(name))
103
106
  return 'confidential';
107
+ if (type === 'function' || type === 'ui') {
108
+ if (DESTRUCTIVE_VERBS.test(name))
109
+ return 'destructive';
110
+ // Auth functions are confidential (handle credentials)
111
+ if (/\b(login|logout|signIn|signOut|register|signup|createAccount)/i.test(name))
112
+ return 'confidential';
113
+ // Social/game → write
114
+ return 'write';
115
+ }
104
116
  if (httpMethod === 'GET')
105
117
  return 'read';
118
+ if (isReadOnly)
119
+ return 'read';
106
120
  if (DESTRUCTIVE_VERBS.test(name))
107
121
  return 'destructive';
108
122
  return 'write';
109
123
  }
110
- /** Derive agentSafe from safety level. Non-read/write levels require human confirmation. */
111
- function deriveAgentSafe(safety) {
112
- return safety === 'read' || safety === 'write';
124
+ /**
125
+ * Derive agentSafe from safety level.
126
+ * Only 'read' and non-sensitive 'write' actions are safe for autonomous execution.
127
+ */
128
+ function deriveAgentSafe(safety, name) {
129
+ if (safety === 'read')
130
+ return true;
131
+ if (safety === 'write') {
132
+ // Narrowing: check if the write action name contains high-privilege keywords
133
+ return !SENSITIVE_WRITE_VERBS.test(name);
134
+ }
135
+ return false;
113
136
  }
114
137
  /**
115
138
  * Infer per-action auth requirement.
@@ -149,8 +172,10 @@ function inferActionAuth(opts) {
149
172
  if (safety === 'destructive') {
150
173
  return { required: 'required' };
151
174
  }
152
- // Read-only GET on a public (no-auth) app → public
153
- if (httpMethod === 'GET' && safety === 'read' && (appAuthType === 'none' || !appAuthType)) {
175
+ // No-auth app read and write actions are public (non-contract only).
176
+ // Financial/confidential/destructive retain their required auth even on no-auth apps
177
+ // (those actions handle money or PII and should always require confirmation).
178
+ if (type !== 'contract' && (appAuthType === 'none' || !appAuthType) && (safety === 'read' || safety === 'write')) {
154
179
  return { required: 'public' };
155
180
  }
156
181
  return { required: 'required' };