nuxt-auto-crud 1.25.0 → 1.26.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/module.json +1 -1
- package/dist/runtime/server/api/_meta.get.d.ts +3 -1
- package/dist/runtime/server/api/_meta.get.js +43 -7
- package/dist/runtime/server/utils/auth.js +19 -6
- package/package.json +1 -1
- package/src/runtime/server/api/_meta.get.ts +43 -18
- package/src/runtime/server/utils/auth.ts +29 -4
package/dist/module.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
|
|
1
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<string | {
|
|
2
2
|
architecture: string;
|
|
3
3
|
version: string;
|
|
4
4
|
resources: ({
|
|
5
5
|
resource: string;
|
|
6
6
|
endpoint: string;
|
|
7
7
|
labelField: string;
|
|
8
|
+
methods: string[];
|
|
8
9
|
fields: {
|
|
9
10
|
name: string;
|
|
10
11
|
type: any;
|
|
@@ -13,6 +14,7 @@ declare const _default: import("h3").EventHandler<import("h3").EventHandlerReque
|
|
|
13
14
|
options: any;
|
|
14
15
|
references: any;
|
|
15
16
|
isRelation: boolean;
|
|
17
|
+
isReadOnly: boolean;
|
|
16
18
|
}[];
|
|
17
19
|
} | null)[];
|
|
18
20
|
}>>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { eventHandler } from "h3";
|
|
1
|
+
import { eventHandler, getQuery, getHeader } from "h3";
|
|
2
2
|
import { getTableForModel, getAvailableModels } from "../utils/modelMapper.js";
|
|
3
3
|
import { getTableColumns as getDrizzleTableColumns } from "drizzle-orm";
|
|
4
4
|
import { getTableConfig } from "drizzle-orm/sqlite-core";
|
|
@@ -7,17 +7,17 @@ import { db } from "hub:db";
|
|
|
7
7
|
import { ensureAuthenticated } from "../utils/auth.js";
|
|
8
8
|
export default eventHandler(async (event) => {
|
|
9
9
|
await ensureAuthenticated(event);
|
|
10
|
+
const query = getQuery(event);
|
|
11
|
+
const acceptHeader = getHeader(event, "accept") || "";
|
|
10
12
|
const models = getAvailableModels().length > 0 ? getAvailableModels() : Object.keys(db?.query || {});
|
|
11
13
|
const resources = models.map((model) => {
|
|
12
14
|
try {
|
|
13
15
|
const table = getTableForModel(model);
|
|
14
16
|
const columns = getDrizzleTableColumns(table);
|
|
15
17
|
const config = getTableConfig(table);
|
|
16
|
-
const fields = Object.entries(columns).filter(([name]) => !
|
|
18
|
+
const fields = Object.entries(columns).filter(([name]) => !HIDDEN_FIELDS.includes(name)).map(([name, col]) => {
|
|
17
19
|
let references = null;
|
|
18
|
-
const fk = config?.foreignKeys.find(
|
|
19
|
-
(f) => f.reference().columns[0].name === col.name
|
|
20
|
-
);
|
|
20
|
+
const fk = config?.foreignKeys.find((f) => f.reference().columns[0].name === col.name);
|
|
21
21
|
if (fk) {
|
|
22
22
|
references = fk.reference().foreignTable[Symbol.for("drizzle:Name")];
|
|
23
23
|
} else if (col.referenceConfig?.foreignTable) {
|
|
@@ -32,7 +32,9 @@ export default eventHandler(async (event) => {
|
|
|
32
32
|
isEnum: !!col.enumValues,
|
|
33
33
|
options: col.enumValues || null,
|
|
34
34
|
references,
|
|
35
|
-
isRelation: !!references
|
|
35
|
+
isRelation: !!references,
|
|
36
|
+
// Agentic Hint: Is this field writable by the user/agent?
|
|
37
|
+
isReadOnly: PROTECTED_FIELDS.includes(name)
|
|
36
38
|
};
|
|
37
39
|
});
|
|
38
40
|
const fieldNames = fields.map((f) => f.name);
|
|
@@ -41,15 +43,49 @@ export default eventHandler(async (event) => {
|
|
|
41
43
|
resource: model,
|
|
42
44
|
endpoint: `/api/${model}`,
|
|
43
45
|
labelField,
|
|
46
|
+
methods: ["GET", "POST", "PATCH", "DELETE"],
|
|
44
47
|
fields
|
|
45
48
|
};
|
|
46
49
|
} catch {
|
|
47
50
|
return null;
|
|
48
51
|
}
|
|
49
52
|
}).filter(Boolean);
|
|
50
|
-
|
|
53
|
+
const payload = {
|
|
51
54
|
architecture: "Clifland-NAC",
|
|
52
55
|
version: "1.0.0-agentic",
|
|
53
56
|
resources
|
|
54
57
|
};
|
|
58
|
+
const currentToken = getQuery(event).token || getHeader(event, "authorization")?.split(" ")[1];
|
|
59
|
+
const tokenSuffix = currentToken ? `?token=${currentToken}` : "";
|
|
60
|
+
if (query.format === "md" || acceptHeader.includes("text/markdown")) {
|
|
61
|
+
let markdown = `# ${payload.architecture} API Manifest (v${payload.version})
|
|
62
|
+
|
|
63
|
+
`;
|
|
64
|
+
payload.resources.forEach((res) => {
|
|
65
|
+
if (!res) return;
|
|
66
|
+
markdown += `### Resource: ${res.resource}
|
|
67
|
+
`;
|
|
68
|
+
markdown += `- **Endpoint**: \`${res.endpoint}${tokenSuffix}\`
|
|
69
|
+
`;
|
|
70
|
+
markdown += `- **Methods**: ${res.methods.join(", ")}
|
|
71
|
+
`;
|
|
72
|
+
markdown += `- **Primary Label**: \`${res.labelField}\`
|
|
73
|
+
|
|
74
|
+
`;
|
|
75
|
+
markdown += `| Field | Type | Required | Writable | Details |
|
|
76
|
+
`;
|
|
77
|
+
markdown += `| :--- | :--- | :--- | :--- | :--- |
|
|
78
|
+
`;
|
|
79
|
+
res.fields.forEach((f) => {
|
|
80
|
+
const details = f.isEnum && f.options ? `Options: ${f.options.join(", ")}` : f.references ? `Refs: ${f.references}` : "-";
|
|
81
|
+
markdown += `| ${f.name} | ${f.type} | ${f.required ? "\u2705" : "\u274C"} | ${f.isReadOnly ? "\u274C" : "\u2705"} | ${details} |
|
|
82
|
+
`;
|
|
83
|
+
});
|
|
84
|
+
markdown += `
|
|
85
|
+
---
|
|
86
|
+
`;
|
|
87
|
+
});
|
|
88
|
+
return markdown;
|
|
89
|
+
}
|
|
90
|
+
return payload;
|
|
55
91
|
});
|
|
@@ -7,6 +7,13 @@ export async function checkAdminAccess(event, model, action, context) {
|
|
|
7
7
|
if (!auth?.authentication) {
|
|
8
8
|
return true;
|
|
9
9
|
}
|
|
10
|
+
const authHeader = getHeader(event, "authorization");
|
|
11
|
+
const query = getQuery(event);
|
|
12
|
+
const apiToken = useRuntimeConfig(event).apiSecretToken;
|
|
13
|
+
const token = (authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : null) || query.token;
|
|
14
|
+
if (token && apiToken && token === apiToken) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
10
17
|
if (auth.type === "jwt") {
|
|
11
18
|
if (!auth.jwtSecret) {
|
|
12
19
|
console.warn("JWT Secret is not configured but auth type is jwt");
|
|
@@ -40,8 +47,8 @@ export async function checkAdminAccess(event, model, action, context) {
|
|
|
40
47
|
const hasCreatedBy = "createdBy" in table;
|
|
41
48
|
const hasUserId = "userId" in table;
|
|
42
49
|
if (hasCreatedBy || hasUserId) {
|
|
43
|
-
const
|
|
44
|
-
const record = await
|
|
50
|
+
const query2 = db.select().from(table).where(eq(table.id, Number(context.id)));
|
|
51
|
+
const record = await query2.get();
|
|
45
52
|
if (record) {
|
|
46
53
|
if (hasCreatedBy) {
|
|
47
54
|
if (String(record.createdBy) === String(user.id)) return true;
|
|
@@ -73,12 +80,18 @@ export async function checkAdminAccess(event, model, action, context) {
|
|
|
73
80
|
}
|
|
74
81
|
export async function ensureAuthenticated(event) {
|
|
75
82
|
const { auth } = useAutoCrudConfig();
|
|
83
|
+
const runtimeConfig = useRuntimeConfig(event);
|
|
76
84
|
if (!auth?.authentication) return;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
const authHeader = getHeader(event, "authorization");
|
|
86
|
+
const query = getQuery(event);
|
|
87
|
+
const apiToken = runtimeConfig.apiSecretToken;
|
|
88
|
+
const token = (authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : null) || query.token;
|
|
89
|
+
if (token && apiToken && token === apiToken) {
|
|
81
90
|
return;
|
|
82
91
|
}
|
|
92
|
+
if (auth.type === "jwt" && auth.jwtSecret) {
|
|
93
|
+
if (await verifyJwtToken(event, auth.jwtSecret)) return;
|
|
94
|
+
throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
|
|
95
|
+
}
|
|
83
96
|
await requireUserSession(event);
|
|
84
97
|
}
|
package/package.json
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { eventHandler } from 'h3'
|
|
1
|
+
import { eventHandler, getQuery, getHeader } from 'h3'
|
|
3
2
|
import { getTableForModel, getAvailableModels } from '../utils/modelMapper'
|
|
4
3
|
import { getTableColumns as getDrizzleTableColumns } from 'drizzle-orm'
|
|
5
4
|
import { getTableConfig } from 'drizzle-orm/sqlite-core'
|
|
@@ -11,6 +10,9 @@ import { ensureAuthenticated } from '../utils/auth'
|
|
|
11
10
|
export default eventHandler(async (event) => {
|
|
12
11
|
await ensureAuthenticated(event)
|
|
13
12
|
|
|
13
|
+
const query = getQuery(event)
|
|
14
|
+
const acceptHeader = getHeader(event, 'accept') || ''
|
|
15
|
+
|
|
14
16
|
const models = getAvailableModels().length > 0
|
|
15
17
|
? getAvailableModels()
|
|
16
18
|
: Object.keys(db?.query || {})
|
|
@@ -21,31 +23,24 @@ export default eventHandler(async (event) => {
|
|
|
21
23
|
const columns = getDrizzleTableColumns(table)
|
|
22
24
|
const config = getTableConfig(table)
|
|
23
25
|
|
|
24
|
-
// Map columns to fields
|
|
25
26
|
const fields = Object.entries(columns)
|
|
26
|
-
.filter(([name]) => !
|
|
27
|
+
.filter(([name]) => !HIDDEN_FIELDS.includes(name))
|
|
27
28
|
.map(([name, col]) => {
|
|
28
29
|
let references = null
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
-
const fk = config?.foreignKeys.find((f: any) =>
|
|
33
|
-
f.reference().columns[0].name === col.name,
|
|
34
|
-
)
|
|
30
|
+
// @ts-expect-error - Drizzle foreign key internals
|
|
31
|
+
const fk = config?.foreignKeys.find(f => f.reference().columns[0].name === col.name)
|
|
35
32
|
|
|
36
33
|
if (fk) {
|
|
37
34
|
// @ts-expect-error - Drizzle internals
|
|
38
35
|
references = fk.reference().foreignTable[Symbol.for('drizzle:Name')]
|
|
39
36
|
}
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const foreignTable = (col as any).referenceConfig.foreignTable
|
|
37
|
+
// @ts-expect-error - Drizzle internal referenceConfig
|
|
38
|
+
else if (col.referenceConfig?.foreignTable) {
|
|
39
|
+
// @ts-expect-error - Drizzle internal referenceConfig
|
|
40
|
+
const foreignTable = col.referenceConfig.foreignTable
|
|
45
41
|
references = foreignTable[Symbol.for('drizzle:Name')] || foreignTable.name
|
|
46
42
|
}
|
|
47
43
|
|
|
48
|
-
// Semantic Normalization
|
|
49
44
|
const semanticType = col.columnType.toLowerCase().replace('sqlite', '')
|
|
50
45
|
|
|
51
46
|
return {
|
|
@@ -56,10 +51,11 @@ export default eventHandler(async (event) => {
|
|
|
56
51
|
options: col.enumValues || null,
|
|
57
52
|
references,
|
|
58
53
|
isRelation: !!references,
|
|
54
|
+
// Agentic Hint: Is this field writable by the user/agent?
|
|
55
|
+
isReadOnly: PROTECTED_FIELDS.includes(name),
|
|
59
56
|
}
|
|
60
57
|
})
|
|
61
58
|
|
|
62
|
-
// 3. Implement Clifland Label Heuristic (name > title > email > id)
|
|
63
59
|
const fieldNames = fields.map(f => f.name)
|
|
64
60
|
const labelField = fieldNames.find(n => n === 'name')
|
|
65
61
|
|| fieldNames.find(n => n === 'title')
|
|
@@ -70,6 +66,7 @@ export default eventHandler(async (event) => {
|
|
|
70
66
|
resource: model,
|
|
71
67
|
endpoint: `/api/${model}`,
|
|
72
68
|
labelField,
|
|
69
|
+
methods: ['GET', 'POST', 'PATCH', 'DELETE'],
|
|
73
70
|
fields,
|
|
74
71
|
}
|
|
75
72
|
}
|
|
@@ -78,9 +75,37 @@ export default eventHandler(async (event) => {
|
|
|
78
75
|
}
|
|
79
76
|
}).filter(Boolean)
|
|
80
77
|
|
|
81
|
-
|
|
78
|
+
const payload = {
|
|
82
79
|
architecture: 'Clifland-NAC',
|
|
83
80
|
version: '1.0.0-agentic',
|
|
84
81
|
resources,
|
|
85
82
|
}
|
|
83
|
+
|
|
84
|
+
const currentToken = getQuery(event).token || (getHeader(event, 'authorization')?.split(' ')[1])
|
|
85
|
+
const tokenSuffix = currentToken ? `?token=${currentToken}` : ''
|
|
86
|
+
|
|
87
|
+
// --- CONTENT NEGOTIATION FOR AGENTIC TOOLS ---
|
|
88
|
+
if (query.format === 'md' || acceptHeader.includes('text/markdown')) {
|
|
89
|
+
let markdown = `# ${payload.architecture} API Manifest (v${payload.version})\n\n`
|
|
90
|
+
|
|
91
|
+
payload.resources.forEach((res) => {
|
|
92
|
+
if (!res) return
|
|
93
|
+
markdown += `### Resource: ${res.resource}\n`
|
|
94
|
+
markdown += `- **Endpoint**: \`${res.endpoint}${tokenSuffix}\`\n`
|
|
95
|
+
markdown += `- **Methods**: ${res.methods.join(', ')}\n`
|
|
96
|
+
markdown += `- **Primary Label**: \`${res.labelField}\`\n\n`
|
|
97
|
+
markdown += `| Field | Type | Required | Writable | Details |\n`
|
|
98
|
+
markdown += `| :--- | :--- | :--- | :--- | :--- |\n`
|
|
99
|
+
|
|
100
|
+
res.fields.forEach((f) => {
|
|
101
|
+
const details = f.isEnum && f.options ? `Options: ${f.options.join(', ')}` : (f.references ? `Refs: ${f.references}` : '-')
|
|
102
|
+
markdown += `| ${f.name} | ${f.type} | ${f.required ? '✅' : '❌'} | ${f.isReadOnly ? '❌' : '✅'} | ${details} |\n`
|
|
103
|
+
})
|
|
104
|
+
markdown += `\n---\n`
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
return markdown
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return payload
|
|
86
111
|
})
|
|
@@ -15,6 +15,18 @@ export async function checkAdminAccess(event: H3Event, model: string, action: st
|
|
|
15
15
|
return true
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// 1. Bearer Token or Query Check (Agentic/MCP Tooling)
|
|
19
|
+
const authHeader = getHeader(event, 'authorization')
|
|
20
|
+
const query = getQuery(event)
|
|
21
|
+
const apiToken = useRuntimeConfig(event).apiSecretToken
|
|
22
|
+
|
|
23
|
+
// Extract token from Header or fallback to Query param
|
|
24
|
+
const token = (authHeader?.startsWith('Bearer ') ? authHeader.split(' ')[1] : null) || query.token
|
|
25
|
+
|
|
26
|
+
if (token && apiToken && token === apiToken) {
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
if (auth.type === 'jwt') {
|
|
19
31
|
if (!auth.jwtSecret) {
|
|
20
32
|
console.warn('JWT Secret is not configured but auth type is jwt')
|
|
@@ -113,15 +125,28 @@ export async function checkAdminAccess(event: H3Event, model: string, action: st
|
|
|
113
125
|
|
|
114
126
|
export async function ensureAuthenticated(event: H3Event): Promise<void> {
|
|
115
127
|
const { auth } = useAutoCrudConfig()
|
|
128
|
+
const runtimeConfig = useRuntimeConfig(event)
|
|
116
129
|
|
|
117
130
|
if (!auth?.authentication) return
|
|
118
131
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
132
|
+
// Extract Token: Priority 1: Authorization Header | Priority 2: Query String (?token=)
|
|
133
|
+
const authHeader = getHeader(event, 'authorization')
|
|
134
|
+
const query = getQuery(event)
|
|
135
|
+
const apiToken = runtimeConfig.apiSecretToken
|
|
136
|
+
|
|
137
|
+
const token = (authHeader?.startsWith('Bearer ') ? authHeader.split(' ')[1] : null) || query.token
|
|
138
|
+
|
|
139
|
+
// 1. API Token Check (Agentic/MCP)
|
|
140
|
+
if (token && apiToken && token === apiToken) {
|
|
123
141
|
return
|
|
124
142
|
}
|
|
125
143
|
|
|
144
|
+
// 2. JWT Check
|
|
145
|
+
if (auth.type === 'jwt' && auth.jwtSecret) {
|
|
146
|
+
if (await verifyJwtToken(event, auth.jwtSecret)) return
|
|
147
|
+
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 3. Session Check (Standard UI)
|
|
126
151
|
await (requireUserSession as (event: H3Event) => Promise<void>)(event)
|
|
127
152
|
}
|