kavachos 0.0.2 → 0.0.3
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/agent/index.d.ts +4 -5
- package/dist/agent/index.js +2 -2
- package/dist/audit/index.d.ts +3 -4
- package/dist/audit/index.js +2 -2
- package/dist/auth/index.d.ts +1719 -2
- package/dist/auth/index.js +2 -1
- package/dist/{chunk-I4J4KKKK.js → chunk-5DT4DN4Y.js} +9 -3
- package/dist/chunk-5DT4DN4Y.js.map +1 -0
- package/dist/chunk-KL6XW4S4.js +10774 -0
- package/dist/chunk-KL6XW4S4.js.map +1 -0
- package/dist/{chunk-DEVV32BE.js → chunk-OVGNZ5OX.js} +3 -3
- package/dist/{chunk-DEVV32BE.js.map → chunk-OVGNZ5OX.js.map} +1 -1
- package/dist/{chunk-N7VZO6SP.js → chunk-SJGSPIAD.js} +3 -3
- package/dist/{chunk-N7VZO6SP.js.map → chunk-SJGSPIAD.js.map} +1 -1
- package/dist/chunk-V66UUIA7.js +480 -0
- package/dist/chunk-V66UUIA7.js.map +1 -0
- package/dist/index.d.ts +1125 -14
- package/dist/index.js +2986 -111
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts +2 -2
- package/dist/permission/index.d.ts +4 -5
- package/dist/permission/index.js +2 -2
- package/dist/types-Xk83hv4O.d.ts +7759 -0
- package/dist/{types-B4sQA44H.d.ts → types-mwupB57A.d.ts} +5 -5
- package/package.json +1 -1
- package/dist/chunk-7RKVTHFC.js +0 -96
- package/dist/chunk-7RKVTHFC.js.map +0 -1
- package/dist/chunk-I4J4KKKK.js.map +0 -1
- package/dist/chunk-UEE7OYLG.js +0 -161
- package/dist/chunk-UEE7OYLG.js.map +0 -1
- package/dist/types-WP-mKSdQ.d.ts +0 -2349
- package/dist/types-_7hIICee.d.ts +0 -52
package/dist/index.js
CHANGED
|
@@ -1,19 +1,319 @@
|
|
|
1
|
-
import { createAgentModule } from './chunk-
|
|
2
|
-
export { createAgentModule } from './chunk-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { createAgentModule } from './chunk-5DT4DN4Y.js';
|
|
2
|
+
export { createAgentModule } from './chunk-5DT4DN4Y.js';
|
|
3
|
+
import { createSessionManager, createMagicLinkModule, createEmailOtpModule, createTotpModule, createPasskeyModule, createOrgModule, createSsoModule, createAdminModule, createApiKeyManagerModule, createUsernameAuthModule, createPhoneAuthModule, createCaptchaModule, createWebhookModule } from './chunk-KL6XW4S4.js';
|
|
4
|
+
export { HibpApiError, HibpBreachedError, OAuthProxyError, OneTapVerifyError, SSO_ERROR, SsoError, additionalFields, admin, anonymousAuth, apiKeys, bearerAuth, createAdditionalFieldsModule, createAdminModule, createAnonymousAuthModule, createApiKeyManagerModule, createCaptchaModule, createCustomSessionModule, createDeviceAuthModule, createEmailOtpModule, createGdprModule, createHibpModule, createJwtSessionModule, createLastLoginModule, createMagicLinkModule, createOAuthProxyModule, createOidcProviderModule, createOneTapModule, createOneTimeTokenModule, createOpenApiModule, createOrgModule, createPasskeyModule, createPhoneAuthModule, createPolarModule, createRateLimiter, createScimModule, createSessionManager, createSiweModule, createSsoModule, createStripeModule, createTotpModule, createTrustedDeviceModule, createUsernameAuthModule, customAuth, customSession, deviceAuth, deviceLabelFromRequest, emailOtp, gdpr, headerAuth, magicLink, oauthProxy, oneTap, organization, passkey, polar, scim, siwe, stripe, twoFactor, withRateLimit } from './chunk-KL6XW4S4.js';
|
|
5
|
+
import { createPermissionEngine } from './chunk-OVGNZ5OX.js';
|
|
6
|
+
export { createPermissionEngine, getPermissionTemplate, permissionTemplates } from './chunk-OVGNZ5OX.js';
|
|
7
|
+
import { createAuditModule } from './chunk-SJGSPIAD.js';
|
|
8
|
+
export { createAuditModule } from './chunk-SJGSPIAD.js';
|
|
9
|
+
import { schema_exports, approvalRequests, sessions, delegationChains, agentDids, mcpServers, budgetPolicies, agents, permissions, auditLogs, tenants, trustScores } from './chunk-V66UUIA7.js';
|
|
10
|
+
export { agentCards, agentDids, agents, apiKeys as apiKeysTable, approvalRequests, auditLogs, budgetPolicies, delegationChains, emailOtps, magicLinks, mcpServers, oauthAccessTokens, oauthAuthorizationCodes, oauthClients, orgInvitations, orgMembers, orgRoles, organizations, passkeyChallenges, passkeyCredentials, permissions, rateLimits, sessions, ssoConnections, tenants, totpRecords, trustScores, users } from './chunk-V66UUIA7.js';
|
|
10
11
|
import './chunk-PZ5AY32C.js';
|
|
12
|
+
import { eq, and, lt, ne, or, isNull, gte } from 'drizzle-orm';
|
|
13
|
+
import { randomUUID } from 'crypto';
|
|
11
14
|
import BetterSqlite3 from 'better-sqlite3';
|
|
12
15
|
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
|
13
|
-
import {
|
|
14
|
-
import { and, eq } from 'drizzle-orm';
|
|
15
|
-
import { SignJWT, jwtVerify } from 'jose';
|
|
16
|
+
import { generateKeyPair, exportJWK, importJWK, SignJWT, jwtVerify } from 'jose';
|
|
16
17
|
|
|
18
|
+
var DEFAULT_LOOKBACK_DAYS = 30;
|
|
19
|
+
function isWildcard(value) {
|
|
20
|
+
return value === "*" || value.endsWith(":*") || value.endsWith("/*");
|
|
21
|
+
}
|
|
22
|
+
function deriveScore(findings) {
|
|
23
|
+
const hasCritical = findings.some((f) => f.severity === "critical");
|
|
24
|
+
const wildcardCount = findings.filter((f) => f.type === "wildcard_permission").length;
|
|
25
|
+
const warningCount = findings.filter((f) => f.severity === "warning").length;
|
|
26
|
+
if (hasCritical || wildcardCount >= 2) return "wildcard-heavy";
|
|
27
|
+
if (wildcardCount === 1 || warningCount >= 2) return "over-permissioned";
|
|
28
|
+
if (findings.length === 0) return "minimal";
|
|
29
|
+
return "appropriate";
|
|
30
|
+
}
|
|
31
|
+
function buildRecommendations(findings, usedResources) {
|
|
32
|
+
const recs = [];
|
|
33
|
+
for (const finding of findings) {
|
|
34
|
+
if (finding.type === "wildcard_permission" && finding.permission) {
|
|
35
|
+
const { resource, actions } = finding.permission;
|
|
36
|
+
const wildcardBase = resource.replace(/:?\*$/, "");
|
|
37
|
+
const relevantUsed = [...usedResources].filter(
|
|
38
|
+
(r) => wildcardBase ? r.startsWith(wildcardBase) : true
|
|
39
|
+
);
|
|
40
|
+
if (relevantUsed.length > 0) {
|
|
41
|
+
recs.push(`Narrow \`${resource}\` to \`${relevantUsed.join(", ")}\``);
|
|
42
|
+
} else {
|
|
43
|
+
recs.push(`Remove unused wildcard permission \`${resource}\``);
|
|
44
|
+
}
|
|
45
|
+
if (actions.includes("*")) {
|
|
46
|
+
const usedActions = ["read"];
|
|
47
|
+
recs.push(
|
|
48
|
+
`Replace wildcard actions on \`${resource}\` with explicit actions: ${usedActions.join(", ")}`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (finding.type === "unused_permission" && finding.permission) {
|
|
53
|
+
recs.push(
|
|
54
|
+
`Remove unused permission \`${finding.permission.resource}\` (no activity in last ${DEFAULT_LOOKBACK_DAYS} days)`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
if (finding.type === "overly_broad" && finding.permission) {
|
|
58
|
+
const { resource } = finding.permission;
|
|
59
|
+
const relevantUsed = [...usedResources].filter((r) => {
|
|
60
|
+
const prefix = resource.replace(/:?\*$/, "");
|
|
61
|
+
return r.startsWith(prefix);
|
|
62
|
+
});
|
|
63
|
+
if (relevantUsed.length > 0) {
|
|
64
|
+
recs.push(
|
|
65
|
+
`Narrow \`${resource}\` to the specific resources used: \`${relevantUsed.join(", ")}\``
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (finding.type === "no_constraints") {
|
|
70
|
+
recs.push("Add rate limits or approval gates to sensitive permissions");
|
|
71
|
+
}
|
|
72
|
+
if (finding.type === "no_expiry") {
|
|
73
|
+
recs.push("Set an expiry date on this agent to enforce periodic credential rotation");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return [...new Set(recs)];
|
|
77
|
+
}
|
|
78
|
+
function createPrivilegeAnalyzer(db) {
|
|
79
|
+
async function analyzeAgent(agentId, options) {
|
|
80
|
+
const agentRows = await db.select({ id: agents.id, name: agents.name, expiresAt: agents.expiresAt }).from(agents).where(eq(agents.id, agentId)).limit(1);
|
|
81
|
+
const agent = agentRows[0];
|
|
82
|
+
if (!agent) {
|
|
83
|
+
return {
|
|
84
|
+
agentId,
|
|
85
|
+
agentName: "unknown",
|
|
86
|
+
score: "appropriate",
|
|
87
|
+
findings: [],
|
|
88
|
+
recommendations: []
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const permRows = await db.select({
|
|
92
|
+
resource: permissions.resource,
|
|
93
|
+
actions: permissions.actions,
|
|
94
|
+
constraints: permissions.constraints
|
|
95
|
+
}).from(permissions).where(eq(permissions.agentId, agentId));
|
|
96
|
+
const agentPermissions = permRows.map((r) => ({
|
|
97
|
+
resource: r.resource,
|
|
98
|
+
actions: r.actions,
|
|
99
|
+
constraints: r.constraints ?? void 0
|
|
100
|
+
}));
|
|
101
|
+
const since = options?.since ?? new Date(Date.now() - DEFAULT_LOOKBACK_DAYS * 24 * 60 * 60 * 1e3);
|
|
102
|
+
const auditRows = await db.select({ resource: auditLogs.resource, action: auditLogs.action }).from(auditLogs).where(and(eq(auditLogs.agentId, agentId), gte(auditLogs.timestamp, since)));
|
|
103
|
+
const usedResources = new Set(auditRows.map((r) => r.resource));
|
|
104
|
+
const findings = [];
|
|
105
|
+
for (const perm of agentPermissions) {
|
|
106
|
+
const hasWildcardResource = isWildcard(perm.resource);
|
|
107
|
+
const hasWildcardAction = perm.actions.includes("*");
|
|
108
|
+
if (hasWildcardResource || hasWildcardAction) {
|
|
109
|
+
findings.push({
|
|
110
|
+
type: "wildcard_permission",
|
|
111
|
+
severity: "critical",
|
|
112
|
+
description: hasWildcardResource ? `Permission resource \`${perm.resource}\` uses a wildcard` : `Permission \`${perm.resource}\` has wildcard action \`*\``,
|
|
113
|
+
permission: { resource: perm.resource, actions: perm.actions }
|
|
114
|
+
});
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const wasUsed = [...usedResources].some((used) => {
|
|
118
|
+
if (perm.resource === used) return true;
|
|
119
|
+
const permBase = perm.resource.replace(/:?\*$/, "");
|
|
120
|
+
return used.startsWith(permBase);
|
|
121
|
+
});
|
|
122
|
+
if (!wasUsed) {
|
|
123
|
+
findings.push({
|
|
124
|
+
type: "unused_permission",
|
|
125
|
+
severity: "warning",
|
|
126
|
+
description: `Permission \`${perm.resource}\` has not been used in the last ${DEFAULT_LOOKBACK_DAYS} days`,
|
|
127
|
+
permission: { resource: perm.resource, actions: perm.actions }
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (perm.resource.includes(":")) {
|
|
131
|
+
const permBase = perm.resource.replace(/:?\*$/, "");
|
|
132
|
+
const coveredUsed = [...usedResources].filter((r) => r.startsWith(permBase));
|
|
133
|
+
if (coveredUsed.length > 0 && coveredUsed.length < 3) {
|
|
134
|
+
const segments = perm.resource.split(":");
|
|
135
|
+
if (segments.length <= 2 && coveredUsed.every((r) => r.split(":").length > segments.length)) {
|
|
136
|
+
findings.push({
|
|
137
|
+
type: "overly_broad",
|
|
138
|
+
severity: "warning",
|
|
139
|
+
description: `Permission \`${perm.resource}\` is broader than necessary; only \`${coveredUsed.join(", ")}\` was actually used`,
|
|
140
|
+
permission: { resource: perm.resource, actions: perm.actions }
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const hasConstraints = perm.constraints && (perm.constraints.maxCallsPerHour !== void 0 || perm.constraints.timeWindow !== void 0 || perm.constraints.requireApproval === true || perm.constraints.ipAllowlist && perm.constraints.ipAllowlist.length > 0);
|
|
146
|
+
if (!hasConstraints) {
|
|
147
|
+
findings.push({
|
|
148
|
+
type: "no_constraints",
|
|
149
|
+
severity: "info",
|
|
150
|
+
description: `Permission \`${perm.resource}\` has no rate limits, time windows, or approval gates`,
|
|
151
|
+
permission: { resource: perm.resource, actions: perm.actions }
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!agent.expiresAt) {
|
|
156
|
+
findings.push({
|
|
157
|
+
type: "no_expiry",
|
|
158
|
+
severity: "info",
|
|
159
|
+
description: "Agent has no expiry date set"
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const score = deriveScore(findings);
|
|
163
|
+
const recommendations = buildRecommendations(findings, usedResources);
|
|
164
|
+
return {
|
|
165
|
+
agentId,
|
|
166
|
+
agentName: agent.name,
|
|
167
|
+
score,
|
|
168
|
+
findings,
|
|
169
|
+
recommendations
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
async function analyzeAll(options) {
|
|
173
|
+
const activeAgents = await db.select({ id: agents.id }).from(agents).where(eq(agents.status, "active"));
|
|
174
|
+
const results = await Promise.all(activeAgents.map((a) => analyzeAgent(a.id, options)));
|
|
175
|
+
return results;
|
|
176
|
+
}
|
|
177
|
+
async function getSummary() {
|
|
178
|
+
const analyses = await analyzeAll();
|
|
179
|
+
const byScore = {};
|
|
180
|
+
let criticalFindings = 0;
|
|
181
|
+
for (const analysis of analyses) {
|
|
182
|
+
byScore[analysis.score] = (byScore[analysis.score] ?? 0) + 1;
|
|
183
|
+
criticalFindings += analysis.findings.filter((f) => f.severity === "critical").length;
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
total: analyses.length,
|
|
187
|
+
byScore,
|
|
188
|
+
criticalFindings
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return { analyzeAgent, analyzeAll, getSummary };
|
|
192
|
+
}
|
|
193
|
+
function rowToApproval(row) {
|
|
194
|
+
return {
|
|
195
|
+
id: row.id,
|
|
196
|
+
agentId: row.agentId,
|
|
197
|
+
userId: row.userId,
|
|
198
|
+
action: row.action,
|
|
199
|
+
resource: row.resource,
|
|
200
|
+
arguments: row.arguments ?? void 0,
|
|
201
|
+
status: row.status,
|
|
202
|
+
expiresAt: row.expiresAt,
|
|
203
|
+
respondedAt: row.respondedAt ?? void 0,
|
|
204
|
+
respondedBy: row.respondedBy ?? void 0,
|
|
205
|
+
createdAt: row.createdAt
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
async function notifyWebhook(url, approvalRequest) {
|
|
209
|
+
try {
|
|
210
|
+
await fetch(url, {
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers: { "Content-Type": "application/json" },
|
|
213
|
+
body: JSON.stringify({
|
|
214
|
+
event: "approval_needed",
|
|
215
|
+
request: {
|
|
216
|
+
...approvalRequest,
|
|
217
|
+
expiresAt: approvalRequest.expiresAt.toISOString(),
|
|
218
|
+
createdAt: approvalRequest.createdAt.toISOString()
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
});
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function createApprovalModule(config, db) {
|
|
226
|
+
const ttlSeconds = config.ttl ?? 300;
|
|
227
|
+
async function request(input) {
|
|
228
|
+
const now = /* @__PURE__ */ new Date();
|
|
229
|
+
const id = `apr_${randomUUID()}`;
|
|
230
|
+
const expiresAt = new Date(now.getTime() + ttlSeconds * 1e3);
|
|
231
|
+
await db.insert(approvalRequests).values({
|
|
232
|
+
id,
|
|
233
|
+
agentId: input.agentId,
|
|
234
|
+
userId: input.userId,
|
|
235
|
+
action: input.action,
|
|
236
|
+
resource: input.resource,
|
|
237
|
+
arguments: input.arguments ?? null,
|
|
238
|
+
status: "pending",
|
|
239
|
+
expiresAt,
|
|
240
|
+
respondedAt: null,
|
|
241
|
+
respondedBy: null,
|
|
242
|
+
createdAt: now
|
|
243
|
+
});
|
|
244
|
+
const approvalRequest = {
|
|
245
|
+
id,
|
|
246
|
+
agentId: input.agentId,
|
|
247
|
+
userId: input.userId,
|
|
248
|
+
action: input.action,
|
|
249
|
+
resource: input.resource,
|
|
250
|
+
arguments: input.arguments,
|
|
251
|
+
status: "pending",
|
|
252
|
+
expiresAt,
|
|
253
|
+
createdAt: now
|
|
254
|
+
};
|
|
255
|
+
if (config.webhookUrl) {
|
|
256
|
+
void notifyWebhook(config.webhookUrl, approvalRequest);
|
|
257
|
+
}
|
|
258
|
+
if (config.onApprovalNeeded) {
|
|
259
|
+
void config.onApprovalNeeded(approvalRequest);
|
|
260
|
+
}
|
|
261
|
+
return approvalRequest;
|
|
262
|
+
}
|
|
263
|
+
async function resolve(requestId, newStatus, respondedBy) {
|
|
264
|
+
const rows = await db.select().from(approvalRequests).where(eq(approvalRequests.id, requestId)).limit(1);
|
|
265
|
+
const row = rows[0];
|
|
266
|
+
if (!row) {
|
|
267
|
+
throw new Error(`Approval request "${requestId}" not found`);
|
|
268
|
+
}
|
|
269
|
+
if (row.status !== "pending") {
|
|
270
|
+
throw new Error(
|
|
271
|
+
`Approval request "${requestId}" is already ${row.status} and cannot be updated`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
const now = /* @__PURE__ */ new Date();
|
|
275
|
+
await db.update(approvalRequests).set({ status: newStatus, respondedAt: now, respondedBy: respondedBy ?? null }).where(eq(approvalRequests.id, requestId));
|
|
276
|
+
return rowToApproval({
|
|
277
|
+
...row,
|
|
278
|
+
status: newStatus,
|
|
279
|
+
respondedAt: now,
|
|
280
|
+
respondedBy: respondedBy ?? null
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
async function approve(requestId, respondedBy) {
|
|
284
|
+
return resolve(requestId, "approved", respondedBy);
|
|
285
|
+
}
|
|
286
|
+
async function deny(requestId, respondedBy) {
|
|
287
|
+
return resolve(requestId, "denied", respondedBy);
|
|
288
|
+
}
|
|
289
|
+
async function get(requestId) {
|
|
290
|
+
const rows = await db.select().from(approvalRequests).where(eq(approvalRequests.id, requestId)).limit(1);
|
|
291
|
+
const row = rows[0];
|
|
292
|
+
if (!row) return null;
|
|
293
|
+
return rowToApproval(row);
|
|
294
|
+
}
|
|
295
|
+
async function listPending(userId) {
|
|
296
|
+
const conditions = [eq(approvalRequests.status, "pending")];
|
|
297
|
+
if (userId) conditions.push(eq(approvalRequests.userId, userId));
|
|
298
|
+
const rows = await db.select().from(approvalRequests).where(and(...conditions));
|
|
299
|
+
return rows.map(rowToApproval);
|
|
300
|
+
}
|
|
301
|
+
async function cleanup() {
|
|
302
|
+
const now = /* @__PURE__ */ new Date();
|
|
303
|
+
const expiredRows = await db.select({ id: approvalRequests.id }).from(approvalRequests).where(and(eq(approvalRequests.status, "pending"), lt(approvalRequests.expiresAt, now)));
|
|
304
|
+
if (expiredRows.length === 0) return { expired: 0 };
|
|
305
|
+
await db.update(approvalRequests).set({ status: "expired" }).where(and(eq(approvalRequests.status, "pending"), lt(approvalRequests.expiresAt, now)));
|
|
306
|
+
return { expired: expiredRows.length };
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
request,
|
|
310
|
+
approve,
|
|
311
|
+
deny,
|
|
312
|
+
get,
|
|
313
|
+
listPending,
|
|
314
|
+
cleanup
|
|
315
|
+
};
|
|
316
|
+
}
|
|
17
317
|
async function createDatabase(config) {
|
|
18
318
|
if (config.provider === "sqlite") {
|
|
19
319
|
const sqlite = new BetterSqlite3(config.url);
|
|
@@ -71,14 +371,43 @@ function buildStatements(provider) {
|
|
|
71
371
|
// kavach_users
|
|
72
372
|
// ------------------------------------------------------------------
|
|
73
373
|
`CREATE TABLE ${ifne} kavach_users (
|
|
74
|
-
id
|
|
75
|
-
email
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
374
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
375
|
+
email TEXT NOT NULL UNIQUE,
|
|
376
|
+
username TEXT UNIQUE,
|
|
377
|
+
name TEXT,
|
|
378
|
+
external_id TEXT,
|
|
379
|
+
external_provider TEXT,
|
|
380
|
+
metadata ${json},
|
|
381
|
+
banned ${bool} NOT NULL DEFAULT ${isPostgres ? "FALSE" : "0"},
|
|
382
|
+
ban_reason TEXT,
|
|
383
|
+
ban_expires_at ${tsNull},
|
|
384
|
+
force_password_reset ${bool} NOT NULL DEFAULT ${isPostgres ? "FALSE" : "0"},
|
|
385
|
+
stripe_customer_id TEXT UNIQUE,
|
|
386
|
+
stripe_subscription_id TEXT,
|
|
387
|
+
stripe_subscription_status TEXT,
|
|
388
|
+
stripe_price_id TEXT,
|
|
389
|
+
stripe_current_period_end ${tsNull},
|
|
390
|
+
stripe_cancel_at_period_end ${bool} NOT NULL DEFAULT ${isPostgres ? "FALSE" : "0"},
|
|
391
|
+
polar_customer_id TEXT UNIQUE,
|
|
392
|
+
polar_subscription_id TEXT,
|
|
393
|
+
polar_subscription_status TEXT,
|
|
394
|
+
polar_product_id TEXT,
|
|
395
|
+
polar_current_period_end ${tsNull},
|
|
396
|
+
polar_cancel_at_period_end ${bool} NOT NULL DEFAULT ${isPostgres ? "FALSE" : "0"},
|
|
397
|
+
created_at ${ts} NOT NULL,
|
|
398
|
+
updated_at ${ts} NOT NULL
|
|
399
|
+
)`,
|
|
400
|
+
// ------------------------------------------------------------------
|
|
401
|
+
// kavach_tenants (must come before kavach_agents – agents FK to tenants)
|
|
402
|
+
// ------------------------------------------------------------------
|
|
403
|
+
`CREATE TABLE ${ifne} kavach_tenants (
|
|
404
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
405
|
+
name TEXT NOT NULL,
|
|
406
|
+
slug TEXT NOT NULL UNIQUE,
|
|
407
|
+
settings ${json},
|
|
408
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
409
|
+
created_at ${ts} NOT NULL,
|
|
410
|
+
updated_at ${ts} NOT NULL
|
|
82
411
|
)`,
|
|
83
412
|
// ------------------------------------------------------------------
|
|
84
413
|
// kavach_agents
|
|
@@ -86,6 +415,7 @@ function buildStatements(provider) {
|
|
|
86
415
|
`CREATE TABLE ${ifne} kavach_agents (
|
|
87
416
|
id TEXT NOT NULL PRIMARY KEY,
|
|
88
417
|
owner_id TEXT NOT NULL REFERENCES kavach_users(id),
|
|
418
|
+
tenant_id TEXT REFERENCES kavach_tenants(id),
|
|
89
419
|
name TEXT NOT NULL,
|
|
90
420
|
type TEXT NOT NULL,
|
|
91
421
|
status TEXT NOT NULL DEFAULT 'active',
|
|
@@ -223,7 +553,328 @@ function buildStatements(provider) {
|
|
|
223
553
|
resource TEXT,
|
|
224
554
|
expires_at ${ts} NOT NULL,
|
|
225
555
|
created_at ${ts} NOT NULL
|
|
226
|
-
)
|
|
556
|
+
)`,
|
|
557
|
+
// ------------------------------------------------------------------
|
|
558
|
+
// kavach_budget_policies
|
|
559
|
+
// ------------------------------------------------------------------
|
|
560
|
+
`CREATE TABLE ${ifne} kavach_budget_policies (
|
|
561
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
562
|
+
agent_id TEXT REFERENCES kavach_agents(id) ON DELETE CASCADE,
|
|
563
|
+
user_id TEXT REFERENCES kavach_users(id),
|
|
564
|
+
tenant_id TEXT REFERENCES kavach_tenants(id),
|
|
565
|
+
limits ${json} NOT NULL,
|
|
566
|
+
current_usage ${json} NOT NULL,
|
|
567
|
+
action TEXT NOT NULL DEFAULT 'warn',
|
|
568
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
569
|
+
created_at ${ts} NOT NULL
|
|
570
|
+
)`,
|
|
571
|
+
// ------------------------------------------------------------------
|
|
572
|
+
// kavach_agent_cards (A2A discovery)
|
|
573
|
+
// ------------------------------------------------------------------
|
|
574
|
+
`CREATE TABLE ${ifne} kavach_agent_cards (
|
|
575
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
576
|
+
agent_id TEXT NOT NULL REFERENCES kavach_agents(id) ON DELETE CASCADE,
|
|
577
|
+
name TEXT NOT NULL,
|
|
578
|
+
description TEXT,
|
|
579
|
+
version TEXT NOT NULL,
|
|
580
|
+
protocols ${json} NOT NULL,
|
|
581
|
+
capabilities ${json} NOT NULL,
|
|
582
|
+
auth_requirements ${json} NOT NULL,
|
|
583
|
+
endpoint TEXT,
|
|
584
|
+
metadata ${json},
|
|
585
|
+
created_at ${ts} NOT NULL,
|
|
586
|
+
updated_at ${ts} NOT NULL
|
|
587
|
+
)`,
|
|
588
|
+
// ------------------------------------------------------------------
|
|
589
|
+
// kavach_approval_requests (CIBA async approval flows)
|
|
590
|
+
// ------------------------------------------------------------------
|
|
591
|
+
`CREATE TABLE ${ifne} kavach_approval_requests (
|
|
592
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
593
|
+
agent_id TEXT NOT NULL REFERENCES kavach_agents(id) ON DELETE CASCADE,
|
|
594
|
+
user_id TEXT NOT NULL REFERENCES kavach_users(id),
|
|
595
|
+
action TEXT NOT NULL,
|
|
596
|
+
resource TEXT NOT NULL,
|
|
597
|
+
arguments ${json},
|
|
598
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
599
|
+
expires_at ${ts} NOT NULL,
|
|
600
|
+
responded_at ${tsNull},
|
|
601
|
+
responded_by TEXT,
|
|
602
|
+
created_at ${ts} NOT NULL
|
|
603
|
+
)`,
|
|
604
|
+
// ------------------------------------------------------------------
|
|
605
|
+
// kavach_trust_scores (graduated autonomy scoring)
|
|
606
|
+
// ------------------------------------------------------------------
|
|
607
|
+
`CREATE TABLE ${ifne} kavach_trust_scores (
|
|
608
|
+
agent_id TEXT NOT NULL PRIMARY KEY REFERENCES kavach_agents(id) ON DELETE CASCADE,
|
|
609
|
+
score INTEGER NOT NULL,
|
|
610
|
+
level TEXT NOT NULL,
|
|
611
|
+
factors ${json} NOT NULL,
|
|
612
|
+
computed_at ${ts} NOT NULL
|
|
613
|
+
)`,
|
|
614
|
+
// ------------------------------------------------------------------
|
|
615
|
+
// kavach_organizations
|
|
616
|
+
// ------------------------------------------------------------------
|
|
617
|
+
`CREATE TABLE ${ifne} kavach_organizations (
|
|
618
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
619
|
+
name TEXT NOT NULL,
|
|
620
|
+
slug TEXT NOT NULL UNIQUE,
|
|
621
|
+
owner_id TEXT NOT NULL REFERENCES kavach_users(id),
|
|
622
|
+
metadata ${json},
|
|
623
|
+
created_at ${ts} NOT NULL,
|
|
624
|
+
updated_at ${ts} NOT NULL
|
|
625
|
+
)`,
|
|
626
|
+
// ------------------------------------------------------------------
|
|
627
|
+
// kavach_org_members
|
|
628
|
+
// ------------------------------------------------------------------
|
|
629
|
+
`CREATE TABLE ${ifne} kavach_org_members (
|
|
630
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
631
|
+
org_id TEXT NOT NULL REFERENCES kavach_organizations(id) ON DELETE CASCADE,
|
|
632
|
+
user_id TEXT NOT NULL REFERENCES kavach_users(id),
|
|
633
|
+
role TEXT NOT NULL DEFAULT 'member',
|
|
634
|
+
joined_at ${ts} NOT NULL,
|
|
635
|
+
UNIQUE(org_id, user_id)
|
|
636
|
+
)`,
|
|
637
|
+
// ------------------------------------------------------------------
|
|
638
|
+
// kavach_org_invitations
|
|
639
|
+
// ------------------------------------------------------------------
|
|
640
|
+
`CREATE TABLE ${ifne} kavach_org_invitations (
|
|
641
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
642
|
+
org_id TEXT NOT NULL REFERENCES kavach_organizations(id) ON DELETE CASCADE,
|
|
643
|
+
email TEXT NOT NULL,
|
|
644
|
+
role TEXT NOT NULL DEFAULT 'member',
|
|
645
|
+
invited_by TEXT NOT NULL REFERENCES kavach_users(id),
|
|
646
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
647
|
+
expires_at ${ts} NOT NULL,
|
|
648
|
+
created_at ${ts} NOT NULL
|
|
649
|
+
)`,
|
|
650
|
+
// ------------------------------------------------------------------
|
|
651
|
+
// kavach_org_roles
|
|
652
|
+
// ------------------------------------------------------------------
|
|
653
|
+
`CREATE TABLE ${ifne} kavach_org_roles (
|
|
654
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
655
|
+
org_id TEXT NOT NULL REFERENCES kavach_organizations(id) ON DELETE CASCADE,
|
|
656
|
+
name TEXT NOT NULL,
|
|
657
|
+
permissions ${json} NOT NULL,
|
|
658
|
+
UNIQUE(org_id, name)
|
|
659
|
+
)`,
|
|
660
|
+
// ------------------------------------------------------------------
|
|
661
|
+
// kavach_passkey_credentials (WebAuthn / FIDO2 passkeys)
|
|
662
|
+
// ------------------------------------------------------------------
|
|
663
|
+
`CREATE TABLE ${ifne} kavach_passkey_credentials (
|
|
664
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
665
|
+
user_id TEXT NOT NULL REFERENCES kavach_users(id),
|
|
666
|
+
credential_id TEXT NOT NULL UNIQUE,
|
|
667
|
+
public_key TEXT NOT NULL,
|
|
668
|
+
counter INTEGER NOT NULL DEFAULT 0,
|
|
669
|
+
device_name TEXT,
|
|
670
|
+
transports TEXT,
|
|
671
|
+
created_at ${ts} NOT NULL,
|
|
672
|
+
last_used_at ${ts} NOT NULL
|
|
673
|
+
)`,
|
|
674
|
+
// ------------------------------------------------------------------
|
|
675
|
+
// kavach_passkey_challenges (short-lived WebAuthn challenges)
|
|
676
|
+
// ------------------------------------------------------------------
|
|
677
|
+
`CREATE TABLE ${ifne} kavach_passkey_challenges (
|
|
678
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
679
|
+
challenge TEXT NOT NULL UNIQUE,
|
|
680
|
+
user_id TEXT,
|
|
681
|
+
type TEXT NOT NULL,
|
|
682
|
+
expires_at ${ts} NOT NULL,
|
|
683
|
+
created_at ${ts} NOT NULL
|
|
684
|
+
)`,
|
|
685
|
+
// ------------------------------------------------------------------
|
|
686
|
+
// kavach_one_time_tokens (email verify, password reset, invitation)
|
|
687
|
+
// ------------------------------------------------------------------
|
|
688
|
+
`CREATE TABLE ${ifne} kavach_one_time_tokens (
|
|
689
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
690
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
691
|
+
purpose TEXT NOT NULL,
|
|
692
|
+
identifier TEXT NOT NULL,
|
|
693
|
+
metadata ${json},
|
|
694
|
+
used ${bool} NOT NULL DEFAULT ${isPostgres ? "FALSE" : "0"},
|
|
695
|
+
expires_at ${ts} NOT NULL,
|
|
696
|
+
created_at ${ts} NOT NULL
|
|
697
|
+
)`,
|
|
698
|
+
// ------------------------------------------------------------------
|
|
699
|
+
// kavach_agent_dids (W3C Decentralized Identifiers per agent)
|
|
700
|
+
// ------------------------------------------------------------------
|
|
701
|
+
`CREATE TABLE ${ifne} kavach_agent_dids (
|
|
702
|
+
agent_id TEXT NOT NULL PRIMARY KEY REFERENCES kavach_agents(id) ON DELETE CASCADE,
|
|
703
|
+
did TEXT NOT NULL UNIQUE,
|
|
704
|
+
method TEXT NOT NULL,
|
|
705
|
+
public_key_jwk TEXT NOT NULL,
|
|
706
|
+
did_document TEXT NOT NULL,
|
|
707
|
+
created_at ${ts} NOT NULL
|
|
708
|
+
)`,
|
|
709
|
+
// ------------------------------------------------------------------
|
|
710
|
+
// kavach_magic_links (passwordless email login)
|
|
711
|
+
// ------------------------------------------------------------------
|
|
712
|
+
`CREATE TABLE ${ifne} kavach_magic_links (
|
|
713
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
714
|
+
email TEXT NOT NULL,
|
|
715
|
+
token TEXT NOT NULL UNIQUE,
|
|
716
|
+
expires_at ${ts} NOT NULL,
|
|
717
|
+
used ${bool} NOT NULL DEFAULT ${isPostgres ? "FALSE" : "0"},
|
|
718
|
+
created_at ${ts} NOT NULL
|
|
719
|
+
)`,
|
|
720
|
+
// ------------------------------------------------------------------
|
|
721
|
+
// kavach_email_otps (one-time password login)
|
|
722
|
+
// ------------------------------------------------------------------
|
|
723
|
+
`CREATE TABLE ${ifne} kavach_email_otps (
|
|
724
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
725
|
+
email TEXT NOT NULL,
|
|
726
|
+
code_hash TEXT NOT NULL,
|
|
727
|
+
expires_at ${ts} NOT NULL,
|
|
728
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
729
|
+
created_at ${ts} NOT NULL
|
|
730
|
+
)`,
|
|
731
|
+
// ------------------------------------------------------------------
|
|
732
|
+
// kavach_totp (TOTP two-factor authentication)
|
|
733
|
+
// ------------------------------------------------------------------
|
|
734
|
+
`CREATE TABLE ${ifne} kavach_totp (
|
|
735
|
+
user_id TEXT NOT NULL PRIMARY KEY REFERENCES kavach_users(id),
|
|
736
|
+
secret TEXT NOT NULL,
|
|
737
|
+
enabled ${bool} NOT NULL DEFAULT ${isPostgres ? "FALSE" : "0"},
|
|
738
|
+
backup_codes ${json} NOT NULL,
|
|
739
|
+
created_at ${ts} NOT NULL,
|
|
740
|
+
updated_at ${ts} NOT NULL
|
|
741
|
+
)`,
|
|
742
|
+
// ------------------------------------------------------------------
|
|
743
|
+
// kavach_sso_connections (SAML 2.0 / OIDC enterprise SSO)
|
|
744
|
+
// ------------------------------------------------------------------
|
|
745
|
+
`CREATE TABLE ${ifne} kavach_sso_connections (
|
|
746
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
747
|
+
org_id TEXT NOT NULL,
|
|
748
|
+
provider_id TEXT NOT NULL,
|
|
749
|
+
type TEXT NOT NULL,
|
|
750
|
+
domain TEXT NOT NULL UNIQUE,
|
|
751
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
752
|
+
created_at ${ts} NOT NULL
|
|
753
|
+
)`,
|
|
754
|
+
// ------------------------------------------------------------------
|
|
755
|
+
// kavach_api_keys (static bearer tokens with permission scopes)
|
|
756
|
+
// ------------------------------------------------------------------
|
|
757
|
+
`CREATE TABLE ${ifne} kavach_api_keys (
|
|
758
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
759
|
+
user_id TEXT NOT NULL REFERENCES kavach_users(id),
|
|
760
|
+
name TEXT NOT NULL,
|
|
761
|
+
key_hash TEXT NOT NULL,
|
|
762
|
+
key_prefix TEXT NOT NULL,
|
|
763
|
+
permissions ${json} NOT NULL,
|
|
764
|
+
expires_at ${tsNull},
|
|
765
|
+
last_used_at ${tsNull},
|
|
766
|
+
created_at ${ts} NOT NULL
|
|
767
|
+
)`,
|
|
768
|
+
// ------------------------------------------------------------------
|
|
769
|
+
// kavach_username_accounts (username + password auth)
|
|
770
|
+
// ------------------------------------------------------------------
|
|
771
|
+
`CREATE TABLE ${ifne} kavach_username_accounts (
|
|
772
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
773
|
+
user_id TEXT NOT NULL REFERENCES kavach_users(id) ON DELETE CASCADE,
|
|
774
|
+
username TEXT NOT NULL UNIQUE,
|
|
775
|
+
password_hash TEXT NOT NULL,
|
|
776
|
+
created_at ${ts} NOT NULL,
|
|
777
|
+
updated_at ${ts} NOT NULL
|
|
778
|
+
)`,
|
|
779
|
+
// ------------------------------------------------------------------
|
|
780
|
+
// kavach_phone_verifications (SMS OTP)
|
|
781
|
+
// ------------------------------------------------------------------
|
|
782
|
+
`CREATE TABLE ${ifne} kavach_phone_verifications (
|
|
783
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
784
|
+
phone_number TEXT NOT NULL,
|
|
785
|
+
code_hash TEXT NOT NULL,
|
|
786
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
787
|
+
expires_at ${ts} NOT NULL,
|
|
788
|
+
created_at ${ts} NOT NULL
|
|
789
|
+
)`,
|
|
790
|
+
// ------------------------------------------------------------------
|
|
791
|
+
// kavach_trusted_devices (skip 2FA on trusted devices for a window)
|
|
792
|
+
// ------------------------------------------------------------------
|
|
793
|
+
`CREATE TABLE ${ifne} kavach_trusted_devices (
|
|
794
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
795
|
+
user_id TEXT NOT NULL REFERENCES kavach_users(id) ON DELETE CASCADE,
|
|
796
|
+
fingerprint TEXT NOT NULL,
|
|
797
|
+
label TEXT NOT NULL,
|
|
798
|
+
trusted_at ${ts} NOT NULL,
|
|
799
|
+
expires_at ${ts} NOT NULL
|
|
800
|
+
)`,
|
|
801
|
+
// ------------------------------------------------------------------
|
|
802
|
+
// kavach_login_history (last-login method tracking per user)
|
|
803
|
+
// ------------------------------------------------------------------
|
|
804
|
+
`CREATE TABLE ${ifne} kavach_login_history (
|
|
805
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
806
|
+
user_id TEXT NOT NULL REFERENCES kavach_users(id) ON DELETE CASCADE,
|
|
807
|
+
method TEXT NOT NULL,
|
|
808
|
+
ip TEXT,
|
|
809
|
+
user_agent TEXT,
|
|
810
|
+
timestamp ${ts} NOT NULL
|
|
811
|
+
)`,
|
|
812
|
+
`CREATE INDEX ${ifne} kavach_login_history_user_ts
|
|
813
|
+
ON kavach_login_history (user_id, timestamp DESC)`,
|
|
814
|
+
// ------------------------------------------------------------------
|
|
815
|
+
// kavach_oidc_clients (OIDC Provider — registered relying parties)
|
|
816
|
+
// ------------------------------------------------------------------
|
|
817
|
+
`CREATE TABLE ${ifne} kavach_oidc_clients (
|
|
818
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
819
|
+
client_id TEXT NOT NULL UNIQUE,
|
|
820
|
+
client_secret_hash TEXT NOT NULL,
|
|
821
|
+
client_name TEXT NOT NULL,
|
|
822
|
+
redirect_uris ${json} NOT NULL,
|
|
823
|
+
grant_types ${json} NOT NULL,
|
|
824
|
+
response_types ${json} NOT NULL,
|
|
825
|
+
scopes ${json} NOT NULL,
|
|
826
|
+
token_endpoint_auth_method TEXT NOT NULL DEFAULT 'client_secret_post',
|
|
827
|
+
created_at ${ts} NOT NULL,
|
|
828
|
+
updated_at ${ts} NOT NULL
|
|
829
|
+
)`,
|
|
830
|
+
// ------------------------------------------------------------------
|
|
831
|
+
// kavach_oidc_auth_codes (OIDC Provider — authorization codes)
|
|
832
|
+
// ------------------------------------------------------------------
|
|
833
|
+
`CREATE TABLE ${ifne} kavach_oidc_auth_codes (
|
|
834
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
835
|
+
code_hash TEXT NOT NULL UNIQUE,
|
|
836
|
+
client_id TEXT NOT NULL,
|
|
837
|
+
user_id TEXT NOT NULL,
|
|
838
|
+
redirect_uri TEXT NOT NULL,
|
|
839
|
+
scopes TEXT NOT NULL,
|
|
840
|
+
nonce TEXT,
|
|
841
|
+
code_challenge TEXT,
|
|
842
|
+
code_challenge_method TEXT,
|
|
843
|
+
used ${bool} NOT NULL DEFAULT ${isPostgres ? "FALSE" : "0"},
|
|
844
|
+
expires_at ${ts} NOT NULL,
|
|
845
|
+
created_at ${ts} NOT NULL
|
|
846
|
+
)`,
|
|
847
|
+
// ------------------------------------------------------------------
|
|
848
|
+
// kavach_oidc_refresh_tokens (OIDC Provider — refresh tokens)
|
|
849
|
+
// ------------------------------------------------------------------
|
|
850
|
+
`CREATE TABLE ${ifne} kavach_oidc_refresh_tokens (
|
|
851
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
852
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
853
|
+
client_id TEXT NOT NULL,
|
|
854
|
+
user_id TEXT NOT NULL,
|
|
855
|
+
scopes TEXT NOT NULL,
|
|
856
|
+
revoked ${bool} NOT NULL DEFAULT ${isPostgres ? "FALSE" : "0"},
|
|
857
|
+
expires_at ${ts} NOT NULL,
|
|
858
|
+
created_at ${ts} NOT NULL
|
|
859
|
+
)`,
|
|
860
|
+
// ------------------------------------------------------------------
|
|
861
|
+
// kavach_jwt_refresh_tokens (JWT session plugin — general purpose)
|
|
862
|
+
// ------------------------------------------------------------------
|
|
863
|
+
`CREATE TABLE ${ifne} kavach_jwt_refresh_tokens (
|
|
864
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
865
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
866
|
+
user_id TEXT NOT NULL REFERENCES kavach_users(id) ON DELETE CASCADE,
|
|
867
|
+
used ${bool} NOT NULL DEFAULT ${isPostgres ? "FALSE" : "0"},
|
|
868
|
+
expires_at ${ts} NOT NULL,
|
|
869
|
+
created_at ${ts} NOT NULL
|
|
870
|
+
)`,
|
|
871
|
+
`CREATE INDEX ${ifne} kavach_jwt_refresh_tokens_user_id
|
|
872
|
+
ON kavach_jwt_refresh_tokens (user_id)`
|
|
873
|
+
// ------------------------------------------------------------------
|
|
874
|
+
// kavach_users ban columns (ALTER TABLE IF NOT EXISTS — safe no-ops)
|
|
875
|
+
// These are appended as separate ALTER statements for existing DBs.
|
|
876
|
+
// For SQLite we use a separate migration path since SQLite ALTER is limited.
|
|
877
|
+
// ------------------------------------------------------------------
|
|
227
878
|
];
|
|
228
879
|
}
|
|
229
880
|
async function createTables(db, provider) {
|
|
@@ -269,10 +920,10 @@ async function createTables(db, provider) {
|
|
|
269
920
|
}
|
|
270
921
|
function isPermissionSubset(parentPerms, childPerms) {
|
|
271
922
|
for (const childPerm of childPerms) {
|
|
272
|
-
const parentMatch = parentPerms.find((
|
|
273
|
-
if (!isResourceSubset(
|
|
923
|
+
const parentMatch = parentPerms.find((p2) => {
|
|
924
|
+
if (!isResourceSubset(p2.resource, childPerm.resource)) return false;
|
|
274
925
|
for (const action of childPerm.actions) {
|
|
275
|
-
if (!
|
|
926
|
+
if (!p2.actions.includes(action) && !p2.actions.includes("*")) return false;
|
|
276
927
|
}
|
|
277
928
|
return true;
|
|
278
929
|
});
|
|
@@ -315,9 +966,9 @@ function createDelegationModule(config) {
|
|
|
315
966
|
id,
|
|
316
967
|
fromAgentId: input.fromAgent,
|
|
317
968
|
toAgentId: input.toAgent,
|
|
318
|
-
permissions: input.permissions.map((
|
|
319
|
-
resource:
|
|
320
|
-
actions:
|
|
969
|
+
permissions: input.permissions.map((p2) => ({
|
|
970
|
+
resource: p2.resource,
|
|
971
|
+
actions: p2.actions
|
|
321
972
|
})),
|
|
322
973
|
depth: currentDepth,
|
|
323
974
|
maxDepth,
|
|
@@ -370,9 +1021,9 @@ function createDelegationModule(config) {
|
|
|
370
1021
|
id: c.id,
|
|
371
1022
|
fromAgent: c.fromAgentId,
|
|
372
1023
|
toAgent: c.toAgentId,
|
|
373
|
-
permissions: c.permissions.map((
|
|
374
|
-
resource:
|
|
375
|
-
actions:
|
|
1024
|
+
permissions: c.permissions.map((p2) => ({
|
|
1025
|
+
resource: p2.resource,
|
|
1026
|
+
actions: p2.actions
|
|
376
1027
|
})),
|
|
377
1028
|
expiresAt: c.expiresAt,
|
|
378
1029
|
depth: c.depth,
|
|
@@ -381,80 +1032,1469 @@ function createDelegationModule(config) {
|
|
|
381
1032
|
}
|
|
382
1033
|
return { delegate, revokeDelegation, getEffectivePermissions, listChains };
|
|
383
1034
|
}
|
|
384
|
-
var
|
|
385
|
-
function
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const
|
|
392
|
-
|
|
1035
|
+
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
1036
|
+
function base58btcEncode(bytes) {
|
|
1037
|
+
let leadingZeros = 0;
|
|
1038
|
+
for (const byte of bytes) {
|
|
1039
|
+
if (byte !== 0) break;
|
|
1040
|
+
leadingZeros++;
|
|
1041
|
+
}
|
|
1042
|
+
const digits = [0];
|
|
1043
|
+
for (const byte of bytes) {
|
|
1044
|
+
let carry = byte;
|
|
1045
|
+
for (let i = 0; i < digits.length; i++) {
|
|
1046
|
+
carry += (digits[i] ?? 0) * 256;
|
|
1047
|
+
digits[i] = carry % 58;
|
|
1048
|
+
carry = Math.floor(carry / 58);
|
|
1049
|
+
}
|
|
1050
|
+
while (carry > 0) {
|
|
1051
|
+
digits.push(carry % 58);
|
|
1052
|
+
carry = Math.floor(carry / 58);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
const result = digits.reverse().map((d) => BASE58_ALPHABET[d] ?? "1").join("");
|
|
1056
|
+
return "1".repeat(leadingZeros) + result;
|
|
1057
|
+
}
|
|
1058
|
+
function base64urlToBytes(b64url) {
|
|
1059
|
+
const padded = b64url.replace(/-/g, "+").replace(/_/g, "/");
|
|
1060
|
+
const padLen = (4 - padded.length % 4) % 4;
|
|
1061
|
+
const b64 = padded + "=".repeat(padLen);
|
|
1062
|
+
const binary = atob(b64);
|
|
1063
|
+
const bytes = new Uint8Array(binary.length);
|
|
1064
|
+
for (let i = 0; i < binary.length; i++) {
|
|
1065
|
+
bytes[i] = binary.charCodeAt(i);
|
|
1066
|
+
}
|
|
1067
|
+
return bytes;
|
|
1068
|
+
}
|
|
1069
|
+
function publicKeyJwkToDidKey(publicKeyJwk) {
|
|
1070
|
+
if (!publicKeyJwk.x) {
|
|
1071
|
+
throw new Error("Ed25519 JWK must have an 'x' parameter");
|
|
1072
|
+
}
|
|
1073
|
+
const rawKey = base64urlToBytes(publicKeyJwk.x);
|
|
1074
|
+
const prefix = new Uint8Array([237, 1]);
|
|
1075
|
+
const multicodecKey = new Uint8Array(prefix.length + rawKey.length);
|
|
1076
|
+
multicodecKey.set(prefix);
|
|
1077
|
+
multicodecKey.set(rawKey, prefix.length);
|
|
1078
|
+
return `did:key:z${base58btcEncode(multicodecKey)}`;
|
|
1079
|
+
}
|
|
1080
|
+
function buildDidDocument(did, publicKeyJwk) {
|
|
1081
|
+
const keyId = `${did}#${did.slice("did:key:".length)}`;
|
|
1082
|
+
const verificationMethod = {
|
|
1083
|
+
id: keyId,
|
|
1084
|
+
type: "JsonWebKey2020",
|
|
1085
|
+
controller: did,
|
|
1086
|
+
publicKeyJwk
|
|
1087
|
+
};
|
|
1088
|
+
return {
|
|
1089
|
+
"@context": ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/jws-2020/v1"],
|
|
1090
|
+
id: did,
|
|
1091
|
+
controller: did,
|
|
1092
|
+
verificationMethod: [verificationMethod],
|
|
1093
|
+
authentication: [keyId],
|
|
1094
|
+
assertionMethod: [keyId],
|
|
1095
|
+
capabilityInvocation: [keyId],
|
|
1096
|
+
capabilityDelegation: [keyId]
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
async function generateDidKey() {
|
|
1100
|
+
const { publicKey, privateKey } = await generateKeyPair("EdDSA", {
|
|
1101
|
+
crv: "Ed25519",
|
|
1102
|
+
extractable: true
|
|
1103
|
+
});
|
|
1104
|
+
const publicKeyJwk = await exportJWK(publicKey);
|
|
1105
|
+
const privateKeyJwk = await exportJWK(privateKey);
|
|
1106
|
+
publicKeyJwk.crv = "Ed25519";
|
|
1107
|
+
publicKeyJwk.kty = "OKP";
|
|
1108
|
+
privateKeyJwk.crv = "Ed25519";
|
|
1109
|
+
privateKeyJwk.kty = "OKP";
|
|
1110
|
+
const did = publicKeyJwkToDidKey(publicKeyJwk);
|
|
1111
|
+
const didDocument = buildDidDocument(did, publicKeyJwk);
|
|
1112
|
+
return {
|
|
1113
|
+
did,
|
|
1114
|
+
publicKeyJwk,
|
|
1115
|
+
privateKeyJwk,
|
|
1116
|
+
didDocument
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
function resolveDidKey(did) {
|
|
1120
|
+
if (!did.startsWith("did:key:z")) return null;
|
|
1121
|
+
const keyId = `${did}#${did.slice("did:key:".length)}`;
|
|
1122
|
+
const verificationMethod = {
|
|
1123
|
+
id: keyId,
|
|
1124
|
+
type: "JsonWebKey2020",
|
|
1125
|
+
controller: did,
|
|
1126
|
+
// Public key JWK is not reconstructed here — callers who need to verify
|
|
1127
|
+
// signatures supply the JWK separately via verifyPayload().
|
|
1128
|
+
publicKeyJwk: { kty: "OKP", crv: "Ed25519" }
|
|
1129
|
+
};
|
|
1130
|
+
return {
|
|
1131
|
+
"@context": ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/jws-2020/v1"],
|
|
1132
|
+
id: did,
|
|
1133
|
+
controller: did,
|
|
1134
|
+
verificationMethod: [verificationMethod],
|
|
1135
|
+
authentication: [keyId],
|
|
1136
|
+
assertionMethod: [keyId],
|
|
1137
|
+
capabilityInvocation: [keyId],
|
|
1138
|
+
capabilityDelegation: [keyId]
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
async function signPayload(payload, privateKeyJwk, did) {
|
|
1142
|
+
const privateKey = await importJWK(privateKeyJwk, "EdDSA");
|
|
1143
|
+
const kid = `${did}#${did.split(":").pop() ?? "key-1"}`;
|
|
1144
|
+
const jws = await new SignJWT(payload).setProtectedHeader({ alg: "EdDSA", kid }).setIssuer(did).setIssuedAt().sign(privateKey);
|
|
1145
|
+
return {
|
|
1146
|
+
jws,
|
|
1147
|
+
payload,
|
|
1148
|
+
issuer: did
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
async function verifyPayload(jws, publicKeyJwk) {
|
|
1152
|
+
try {
|
|
1153
|
+
const publicKey = await importJWK(publicKeyJwk, "EdDSA");
|
|
1154
|
+
const { payload } = await jwtVerify(jws, publicKey);
|
|
1155
|
+
const issuer = typeof payload.iss === "string" ? payload.iss : void 0;
|
|
1156
|
+
const { iss, iat, exp, nbf, jti, aud, sub, ...rest } = payload;
|
|
1157
|
+
void iss;
|
|
1158
|
+
void iat;
|
|
1159
|
+
void exp;
|
|
1160
|
+
void nbf;
|
|
1161
|
+
void jti;
|
|
1162
|
+
void aud;
|
|
1163
|
+
void sub;
|
|
1164
|
+
return {
|
|
1165
|
+
valid: true,
|
|
1166
|
+
payload: rest,
|
|
1167
|
+
issuer
|
|
1168
|
+
};
|
|
1169
|
+
} catch (err) {
|
|
393
1170
|
return {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
expiresAt: row.expiresAt,
|
|
397
|
-
createdAt: row.createdAt,
|
|
398
|
-
...row.metadata !== null && { metadata: row.metadata }
|
|
1171
|
+
valid: false,
|
|
1172
|
+
error: err instanceof Error ? err.message : "Verification failed"
|
|
399
1173
|
};
|
|
400
1174
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
1175
|
+
}
|
|
1176
|
+
async function createPresentation(options) {
|
|
1177
|
+
const { agentId, did, privateKeyJwk, capabilities, audience, expiresIn = 300 } = options;
|
|
1178
|
+
const privateKey = await importJWK(privateKeyJwk, "EdDSA");
|
|
1179
|
+
const kid = `${did}#${did.split(":").pop() ?? "key-1"}`;
|
|
1180
|
+
const builder = new SignJWT({
|
|
1181
|
+
agentId,
|
|
1182
|
+
capabilities,
|
|
1183
|
+
type: "VerifiablePresentation"
|
|
1184
|
+
}).setProtectedHeader({ alg: "EdDSA", kid }).setIssuer(did).setSubject(agentId).setIssuedAt().setExpirationTime(Math.floor(Date.now() / 1e3) + expiresIn);
|
|
1185
|
+
if (audience) {
|
|
1186
|
+
builder.setAudience(audience);
|
|
1187
|
+
}
|
|
1188
|
+
return builder.sign(privateKey);
|
|
1189
|
+
}
|
|
1190
|
+
async function verifyPresentation(jwt, publicKeyJwk) {
|
|
1191
|
+
try {
|
|
1192
|
+
const publicKey = await importJWK(publicKeyJwk, "EdDSA");
|
|
1193
|
+
const { payload } = await jwtVerify(jwt, publicKey);
|
|
1194
|
+
const agentId = typeof payload.agentId === "string" ? payload.agentId : void 0;
|
|
1195
|
+
const did = typeof payload.iss === "string" ? payload.iss : void 0;
|
|
1196
|
+
const capabilities = Array.isArray(payload.capabilities) ? payload.capabilities : void 0;
|
|
1197
|
+
return {
|
|
1198
|
+
valid: true,
|
|
1199
|
+
agentId,
|
|
1200
|
+
did,
|
|
1201
|
+
capabilities
|
|
1202
|
+
};
|
|
1203
|
+
} catch (err) {
|
|
1204
|
+
return {
|
|
1205
|
+
valid: false,
|
|
1206
|
+
error: err instanceof Error ? err.message : "Presentation verification failed"
|
|
419
1207
|
};
|
|
420
|
-
return { session, token };
|
|
421
1208
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
431
|
-
const now = /* @__PURE__ */ new Date();
|
|
432
|
-
const rows = await db.select().from(sessions).where(and(eq(sessions.id, sessionId)));
|
|
433
|
-
const row = rows[0];
|
|
434
|
-
if (!row) return null;
|
|
435
|
-
if (row.expiresAt <= now) {
|
|
436
|
-
await db.delete(sessions).where(eq(sessions.id, sessionId));
|
|
437
|
-
return null;
|
|
438
|
-
}
|
|
439
|
-
return rowToSession(row);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// src/did/web-method.ts
|
|
1212
|
+
function buildDidWeb(config, agentId) {
|
|
1213
|
+
const domain = config.domain.replace(/\//g, ":");
|
|
1214
|
+
if (config.path) {
|
|
1215
|
+
const path = config.path.replace(/\//g, ":").replace(/^:|:$/g, "");
|
|
1216
|
+
return `did:web:${domain}:${path}:${agentId}`;
|
|
440
1217
|
}
|
|
441
|
-
|
|
442
|
-
|
|
1218
|
+
return `did:web:${domain}:${agentId}`;
|
|
1219
|
+
}
|
|
1220
|
+
async function generateDidWeb(config, agentId) {
|
|
1221
|
+
const { publicKeyJwk, privateKeyJwk } = await generateDidKey();
|
|
1222
|
+
const did = buildDidWeb(config, agentId);
|
|
1223
|
+
const didDocument = buildDidDocument(did, publicKeyJwk);
|
|
1224
|
+
return {
|
|
1225
|
+
did,
|
|
1226
|
+
publicKeyJwk,
|
|
1227
|
+
privateKeyJwk,
|
|
1228
|
+
didDocument
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
function getDidWebUrl(did) {
|
|
1232
|
+
if (!did.startsWith("did:web:")) {
|
|
1233
|
+
throw new Error(`Not a did:web identifier: ${did}`);
|
|
443
1234
|
}
|
|
444
|
-
|
|
445
|
-
|
|
1235
|
+
const methodSpecific = did.slice("did:web:".length);
|
|
1236
|
+
const parts = methodSpecific.split(":");
|
|
1237
|
+
const decoded = parts.map((p2) => decodeURIComponent(p2));
|
|
1238
|
+
if (decoded.length === 1) {
|
|
1239
|
+
return `https://${decoded[0]}/.well-known/did.json`;
|
|
446
1240
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
1241
|
+
const domain = decoded[0];
|
|
1242
|
+
const pathSegments = decoded.slice(1);
|
|
1243
|
+
return `https://${domain}/${pathSegments.join("/")}/did.json`;
|
|
1244
|
+
}
|
|
1245
|
+
async function resolveDidWeb(did) {
|
|
1246
|
+
let url;
|
|
1247
|
+
try {
|
|
1248
|
+
url = getDidWebUrl(did);
|
|
1249
|
+
} catch {
|
|
1250
|
+
return null;
|
|
1251
|
+
}
|
|
1252
|
+
try {
|
|
1253
|
+
const response = await fetch(url, {
|
|
1254
|
+
headers: { Accept: "application/json" }
|
|
1255
|
+
});
|
|
1256
|
+
if (!response.ok) return null;
|
|
1257
|
+
const doc = await response.json();
|
|
1258
|
+
if (!doc["@context"] || !doc.id) return null;
|
|
1259
|
+
return doc;
|
|
1260
|
+
} catch {
|
|
1261
|
+
return null;
|
|
451
1262
|
}
|
|
452
|
-
return { create, validate, revoke, revokeAll, list };
|
|
453
1263
|
}
|
|
454
1264
|
|
|
455
|
-
// src/
|
|
456
|
-
|
|
457
|
-
|
|
1265
|
+
// src/did/module.ts
|
|
1266
|
+
function createDidModule(db, config) {
|
|
1267
|
+
async function generateKey(agentId) {
|
|
1268
|
+
const keyPair = await generateDidKey();
|
|
1269
|
+
const now = /* @__PURE__ */ new Date();
|
|
1270
|
+
await db.insert(agentDids).values({
|
|
1271
|
+
agentId,
|
|
1272
|
+
did: keyPair.did,
|
|
1273
|
+
method: "key",
|
|
1274
|
+
publicKeyJwk: JSON.stringify(keyPair.publicKeyJwk),
|
|
1275
|
+
didDocument: JSON.stringify(keyPair.didDocument),
|
|
1276
|
+
createdAt: now
|
|
1277
|
+
});
|
|
1278
|
+
const agentDid = {
|
|
1279
|
+
agentId,
|
|
1280
|
+
did: keyPair.did,
|
|
1281
|
+
method: "key",
|
|
1282
|
+
publicKeyJwk: keyPair.publicKeyJwk,
|
|
1283
|
+
didDocument: keyPair.didDocument,
|
|
1284
|
+
createdAt: now
|
|
1285
|
+
};
|
|
1286
|
+
return { agentDid, privateKeyJwk: keyPair.privateKeyJwk };
|
|
1287
|
+
}
|
|
1288
|
+
async function generateWeb(agentId) {
|
|
1289
|
+
if (!config?.web) {
|
|
1290
|
+
throw new Error(
|
|
1291
|
+
"did:web requires a web config (domain). Pass { web: { domain: 'example.com' } } to createDidModule()."
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
const keyPair = await generateDidWeb(config.web, agentId);
|
|
1295
|
+
const now = /* @__PURE__ */ new Date();
|
|
1296
|
+
await db.insert(agentDids).values({
|
|
1297
|
+
agentId,
|
|
1298
|
+
did: keyPair.did,
|
|
1299
|
+
method: "web",
|
|
1300
|
+
publicKeyJwk: JSON.stringify(keyPair.publicKeyJwk),
|
|
1301
|
+
didDocument: JSON.stringify(keyPair.didDocument),
|
|
1302
|
+
createdAt: now
|
|
1303
|
+
});
|
|
1304
|
+
const agentDid = {
|
|
1305
|
+
agentId,
|
|
1306
|
+
did: keyPair.did,
|
|
1307
|
+
method: "web",
|
|
1308
|
+
publicKeyJwk: keyPair.publicKeyJwk,
|
|
1309
|
+
didDocument: keyPair.didDocument,
|
|
1310
|
+
createdAt: now
|
|
1311
|
+
};
|
|
1312
|
+
return { agentDid, privateKeyJwk: keyPair.privateKeyJwk };
|
|
1313
|
+
}
|
|
1314
|
+
async function resolve(did) {
|
|
1315
|
+
if (did.startsWith("did:key:")) {
|
|
1316
|
+
return resolveDidKey(did);
|
|
1317
|
+
}
|
|
1318
|
+
if (did.startsWith("did:web:")) {
|
|
1319
|
+
return resolveDidWeb(did);
|
|
1320
|
+
}
|
|
1321
|
+
return null;
|
|
1322
|
+
}
|
|
1323
|
+
async function getAgentDid(agentId) {
|
|
1324
|
+
const rows = await db.select().from(agentDids).where(eq(agentDids.agentId, agentId));
|
|
1325
|
+
const row = rows[0];
|
|
1326
|
+
if (!row) return null;
|
|
1327
|
+
return {
|
|
1328
|
+
agentId: row.agentId,
|
|
1329
|
+
did: row.did,
|
|
1330
|
+
method: row.method,
|
|
1331
|
+
publicKeyJwk: JSON.parse(row.publicKeyJwk),
|
|
1332
|
+
didDocument: JSON.parse(row.didDocument),
|
|
1333
|
+
createdAt: row.createdAt
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
async function sign(agentId, payload, privateKeyJwk) {
|
|
1337
|
+
const agentDid = await getAgentDid(agentId);
|
|
1338
|
+
if (!agentDid) {
|
|
1339
|
+
throw new Error(`No DID found for agent "${agentId}". Call generateKey() first.`);
|
|
1340
|
+
}
|
|
1341
|
+
return signPayload(payload, privateKeyJwk, agentDid.did);
|
|
1342
|
+
}
|
|
1343
|
+
async function verify(jws, did) {
|
|
1344
|
+
if (!did) {
|
|
1345
|
+
return {
|
|
1346
|
+
valid: false,
|
|
1347
|
+
error: "A DID is required to look up the public key for verification."
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
const rows = await db.select().from(agentDids).where(eq(agentDids.did, did));
|
|
1351
|
+
const row = rows[0];
|
|
1352
|
+
if (!row) {
|
|
1353
|
+
return {
|
|
1354
|
+
valid: false,
|
|
1355
|
+
error: `No stored public key found for DID "${did}"`
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
const publicKeyJwk = JSON.parse(row.publicKeyJwk);
|
|
1359
|
+
return verifyPayload(jws, publicKeyJwk);
|
|
1360
|
+
}
|
|
1361
|
+
async function createPresentationForAgent(options) {
|
|
1362
|
+
const agentDid = await getAgentDid(options.agentId);
|
|
1363
|
+
if (!agentDid) {
|
|
1364
|
+
throw new Error(`No DID found for agent "${options.agentId}". Call generateKey() first.`);
|
|
1365
|
+
}
|
|
1366
|
+
return createPresentation({
|
|
1367
|
+
agentId: options.agentId,
|
|
1368
|
+
did: agentDid.did,
|
|
1369
|
+
privateKeyJwk: options.privateKeyJwk,
|
|
1370
|
+
capabilities: options.capabilities,
|
|
1371
|
+
audience: options.audience,
|
|
1372
|
+
expiresIn: options.expiresIn
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
async function verifyPresentationForAgent(jwt) {
|
|
1376
|
+
const parts = jwt.split(".");
|
|
1377
|
+
if (parts.length !== 3) {
|
|
1378
|
+
return { valid: false, error: "Malformed JWT: expected 3 parts" };
|
|
1379
|
+
}
|
|
1380
|
+
let issuerDid;
|
|
1381
|
+
try {
|
|
1382
|
+
const payloadPart = parts[1] ?? "";
|
|
1383
|
+
const padded = payloadPart.replace(/-/g, "+").replace(/_/g, "/");
|
|
1384
|
+
const padLen = (4 - padded.length % 4) % 4;
|
|
1385
|
+
const decoded = atob(padded + "=".repeat(padLen));
|
|
1386
|
+
const claims = JSON.parse(decoded);
|
|
1387
|
+
issuerDid = typeof claims.iss === "string" ? claims.iss : void 0;
|
|
1388
|
+
} catch {
|
|
1389
|
+
return { valid: false, error: "Failed to decode JWT payload" };
|
|
1390
|
+
}
|
|
1391
|
+
if (!issuerDid) {
|
|
1392
|
+
return { valid: false, error: "JWT missing 'iss' claim" };
|
|
1393
|
+
}
|
|
1394
|
+
const rows = await db.select().from(agentDids).where(eq(agentDids.did, issuerDid));
|
|
1395
|
+
const row = rows[0];
|
|
1396
|
+
if (!row) {
|
|
1397
|
+
return {
|
|
1398
|
+
valid: false,
|
|
1399
|
+
error: `No stored public key found for DID "${issuerDid}"`
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
const publicKeyJwk = JSON.parse(row.publicKeyJwk);
|
|
1403
|
+
const result = await verifyPresentation(jwt, publicKeyJwk);
|
|
1404
|
+
if (!result.valid) {
|
|
1405
|
+
return { valid: false, error: result.error };
|
|
1406
|
+
}
|
|
1407
|
+
return {
|
|
1408
|
+
valid: true,
|
|
1409
|
+
issuer: result.did,
|
|
1410
|
+
payload: void 0,
|
|
1411
|
+
capabilities: result.capabilities
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
return {
|
|
1415
|
+
generateKey,
|
|
1416
|
+
generateWeb,
|
|
1417
|
+
resolve,
|
|
1418
|
+
getAgentDid,
|
|
1419
|
+
sign,
|
|
1420
|
+
verify,
|
|
1421
|
+
createPresentation: createPresentationForAgent,
|
|
1422
|
+
verifyPresentation: verifyPresentationForAgent
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// src/email/templates.ts
|
|
1427
|
+
var OUTER_STYLES = 'font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#f4f4f5;margin:0;padding:0;';
|
|
1428
|
+
var CONTAINER_STYLES = "max-width:560px;margin:32px auto;background:#ffffff;border-radius:8px;overflow:hidden;";
|
|
1429
|
+
var HEADER_STYLES = "background:#C9A84C;padding:24px 32px;";
|
|
1430
|
+
var HEADER_H1_STYLES = "color:#ffffff;margin:0;font-size:20px;font-weight:600;letter-spacing:-0.3px;";
|
|
1431
|
+
var BODY_STYLES = "padding:32px;";
|
|
1432
|
+
var P_STYLES = "margin:0 0 16px;color:#3f3f46;font-size:15px;line-height:1.6;";
|
|
1433
|
+
var CODE_STYLES = "display:inline-block;background:#fef9ec;border:1px solid #e9c97e;border-radius:6px;padding:12px 24px;font-family:JetBrains Mono,monospace;font-size:24px;font-weight:700;letter-spacing:4px;color:#8B6914;";
|
|
1434
|
+
var BUTTON_STYLES = "display:inline-block;background:#C9A84C;color:#ffffff;text-decoration:none;padding:12px 24px;border-radius:6px;font-size:15px;font-weight:600;";
|
|
1435
|
+
var FOOTER_STYLES = "border-top:1px solid #e4e4e7;padding:16px 32px;color:#a1a1aa;font-size:13px;";
|
|
1436
|
+
function html(appName, title, body) {
|
|
1437
|
+
return `<!DOCTYPE html>
|
|
1438
|
+
<html lang="en">
|
|
1439
|
+
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${title}</title></head>
|
|
1440
|
+
<body style="${OUTER_STYLES}">
|
|
1441
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td>
|
|
1442
|
+
<div style="${CONTAINER_STYLES}">
|
|
1443
|
+
<div style="${HEADER_STYLES}"><h1 style="${HEADER_H1_STYLES}">${appName}</h1></div>
|
|
1444
|
+
<div style="${BODY_STYLES}">${body}</div>
|
|
1445
|
+
<div style="${FOOTER_STYLES}">You received this email because of activity on your ${appName} account.</div>
|
|
1446
|
+
</div>
|
|
1447
|
+
</td></tr></table>
|
|
1448
|
+
</body>
|
|
1449
|
+
</html>`;
|
|
1450
|
+
}
|
|
1451
|
+
function p(content) {
|
|
1452
|
+
return `<p style="${P_STYLES}">${content}</p>`;
|
|
1453
|
+
}
|
|
1454
|
+
function button(url, label) {
|
|
1455
|
+
return `<p style="margin:24px 0;"><a href="${url}" style="${BUTTON_STYLES}">${label}</a></p>`;
|
|
1456
|
+
}
|
|
1457
|
+
function code(value) {
|
|
1458
|
+
return `<p style="margin:24px 0;"><span style="${CODE_STYLES}">${value}</span></p>`;
|
|
1459
|
+
}
|
|
1460
|
+
function verificationTemplate(appName, appUrl, vars) {
|
|
1461
|
+
const email = vars.email ?? "";
|
|
1462
|
+
const verifyUrl = vars.verifyUrl ?? `${appUrl}/verify?token=${vars.token ?? ""}`;
|
|
1463
|
+
return {
|
|
1464
|
+
subject: `Verify your email - ${appName}`,
|
|
1465
|
+
text: [
|
|
1466
|
+
`Verify your email address`,
|
|
1467
|
+
``,
|
|
1468
|
+
`Hi${email ? ` ${email}` : ""},`,
|
|
1469
|
+
``,
|
|
1470
|
+
`Please verify your email address by visiting the link below:`,
|
|
1471
|
+
``,
|
|
1472
|
+
verifyUrl,
|
|
1473
|
+
``,
|
|
1474
|
+
`This link expires in 24 hours. If you did not create an account, you can ignore this email.`
|
|
1475
|
+
].join("\n"),
|
|
1476
|
+
html: html(
|
|
1477
|
+
appName,
|
|
1478
|
+
`Verify your email`,
|
|
1479
|
+
[
|
|
1480
|
+
p(`Hi${email ? ` <strong>${email}</strong>` : ""},`),
|
|
1481
|
+
p("Please verify your email address to complete your sign-up."),
|
|
1482
|
+
button(verifyUrl, "Verify email"),
|
|
1483
|
+
p(`Or copy this link: <a href="${verifyUrl}" style="color:#C9A84C;">${verifyUrl}</a>`),
|
|
1484
|
+
p(
|
|
1485
|
+
`This link expires in 24 hours. If you did not create an account, you can safely ignore this email.`
|
|
1486
|
+
)
|
|
1487
|
+
].join("")
|
|
1488
|
+
)
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
function passwordResetTemplate(appName, appUrl, vars) {
|
|
1492
|
+
const email = vars.email ?? "";
|
|
1493
|
+
const resetUrl = vars.resetUrl ?? `${appUrl}/reset-password?token=${vars.token ?? ""}`;
|
|
1494
|
+
return {
|
|
1495
|
+
subject: `Reset your password - ${appName}`,
|
|
1496
|
+
text: [
|
|
1497
|
+
`Reset your password`,
|
|
1498
|
+
``,
|
|
1499
|
+
`Hi${email ? ` ${email}` : ""},`,
|
|
1500
|
+
``,
|
|
1501
|
+
`We received a request to reset your password. Click the link below to proceed:`,
|
|
1502
|
+
``,
|
|
1503
|
+
resetUrl,
|
|
1504
|
+
``,
|
|
1505
|
+
`This link expires in 1 hour. If you did not request a password reset, you can ignore this email.`
|
|
1506
|
+
].join("\n"),
|
|
1507
|
+
html: html(
|
|
1508
|
+
appName,
|
|
1509
|
+
`Reset your password`,
|
|
1510
|
+
[
|
|
1511
|
+
p(`Hi${email ? ` <strong>${email}</strong>` : ""},`),
|
|
1512
|
+
p("We received a request to reset your password."),
|
|
1513
|
+
button(resetUrl, "Reset password"),
|
|
1514
|
+
p(`Or copy this link: <a href="${resetUrl}" style="color:#C9A84C;">${resetUrl}</a>`),
|
|
1515
|
+
p(
|
|
1516
|
+
`This link expires in 1 hour. If you did not request a password reset, you can safely ignore this email.`
|
|
1517
|
+
)
|
|
1518
|
+
].join("")
|
|
1519
|
+
)
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
function magicLinkTemplate(appName, _appUrl, vars) {
|
|
1523
|
+
const email = vars.email ?? "";
|
|
1524
|
+
const url = vars.url ?? "";
|
|
1525
|
+
return {
|
|
1526
|
+
subject: `Sign in to ${appName}`,
|
|
1527
|
+
text: [
|
|
1528
|
+
`Sign in to ${appName}`,
|
|
1529
|
+
``,
|
|
1530
|
+
`Hi${email ? ` ${email}` : ""},`,
|
|
1531
|
+
``,
|
|
1532
|
+
`Click the link below to sign in to your account. This link expires in 15 minutes and can only be used once.`,
|
|
1533
|
+
``,
|
|
1534
|
+
url
|
|
1535
|
+
].join("\n"),
|
|
1536
|
+
html: html(
|
|
1537
|
+
appName,
|
|
1538
|
+
`Sign in to ${appName}`,
|
|
1539
|
+
[
|
|
1540
|
+
p(`Hi${email ? ` <strong>${email}</strong>` : ""},`),
|
|
1541
|
+
p(
|
|
1542
|
+
"Click the button below to sign in. This link expires in 15 minutes and can only be used once."
|
|
1543
|
+
),
|
|
1544
|
+
button(url, `Sign in to ${appName}`),
|
|
1545
|
+
p(`Or copy this link: <a href="${url}" style="color:#C9A84C;">${url}</a>`)
|
|
1546
|
+
].join("")
|
|
1547
|
+
)
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
function emailOtpTemplate(appName, _appUrl, vars) {
|
|
1551
|
+
const email = vars.email ?? "";
|
|
1552
|
+
const otpCode = vars.code ?? "";
|
|
1553
|
+
return {
|
|
1554
|
+
subject: `Your verification code: ${otpCode}`,
|
|
1555
|
+
text: [
|
|
1556
|
+
`Your verification code`,
|
|
1557
|
+
``,
|
|
1558
|
+
`Hi${email ? ` ${email}` : ""},`,
|
|
1559
|
+
``,
|
|
1560
|
+
`Your ${appName} verification code is:`,
|
|
1561
|
+
``,
|
|
1562
|
+
otpCode,
|
|
1563
|
+
``,
|
|
1564
|
+
`This code expires in 10 minutes. Do not share it with anyone.`
|
|
1565
|
+
].join("\n"),
|
|
1566
|
+
html: html(
|
|
1567
|
+
appName,
|
|
1568
|
+
`Your verification code`,
|
|
1569
|
+
[
|
|
1570
|
+
p(`Hi${email ? ` <strong>${email}</strong>` : ""},`),
|
|
1571
|
+
p(`Your ${appName} verification code is:`),
|
|
1572
|
+
code(otpCode),
|
|
1573
|
+
p("This code expires in 10 minutes. Do not share it with anyone.")
|
|
1574
|
+
].join("")
|
|
1575
|
+
)
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
function invitationTemplate(appName, _appUrl, vars) {
|
|
1579
|
+
const email = vars.email ?? "";
|
|
1580
|
+
const orgName = vars.orgName ?? "an organization";
|
|
1581
|
+
const inviteUrl = vars.inviteUrl ?? "";
|
|
1582
|
+
return {
|
|
1583
|
+
subject: `You've been invited to ${orgName}`,
|
|
1584
|
+
text: [
|
|
1585
|
+
`You've been invited to ${orgName}`,
|
|
1586
|
+
``,
|
|
1587
|
+
`Hi${email ? ` ${email}` : ""},`,
|
|
1588
|
+
``,
|
|
1589
|
+
`You've been invited to join ${orgName} on ${appName}. Click the link below to accept:`,
|
|
1590
|
+
``,
|
|
1591
|
+
inviteUrl,
|
|
1592
|
+
``,
|
|
1593
|
+
`If you were not expecting this invitation, you can ignore this email.`
|
|
1594
|
+
].join("\n"),
|
|
1595
|
+
html: html(
|
|
1596
|
+
appName,
|
|
1597
|
+
`You've been invited to ${orgName}`,
|
|
1598
|
+
[
|
|
1599
|
+
p(`Hi${email ? ` <strong>${email}</strong>` : ""},`),
|
|
1600
|
+
p(`You've been invited to join <strong>${orgName}</strong> on ${appName}.`),
|
|
1601
|
+
button(inviteUrl, `Accept invitation`),
|
|
1602
|
+
p(`Or copy this link: <a href="${inviteUrl}" style="color:#C9A84C;">${inviteUrl}</a>`),
|
|
1603
|
+
p("If you were not expecting this invitation, you can safely ignore this email.")
|
|
1604
|
+
].join("")
|
|
1605
|
+
)
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
function welcomeTemplate(appName, appUrl, vars) {
|
|
1609
|
+
const email = vars.email ?? "";
|
|
1610
|
+
const name = vars.name ?? email;
|
|
1611
|
+
return {
|
|
1612
|
+
subject: `Welcome to ${appName}`,
|
|
1613
|
+
text: [
|
|
1614
|
+
`Welcome to ${appName}`,
|
|
1615
|
+
``,
|
|
1616
|
+
`Hi ${name},`,
|
|
1617
|
+
``,
|
|
1618
|
+
`Your account is ready. Head over to ${appUrl} to get started.`,
|
|
1619
|
+
``,
|
|
1620
|
+
`If you have any questions, reply to this email.`
|
|
1621
|
+
].join("\n"),
|
|
1622
|
+
html: html(
|
|
1623
|
+
appName,
|
|
1624
|
+
`Welcome to ${appName}`,
|
|
1625
|
+
[
|
|
1626
|
+
p(`Hi <strong>${name}</strong>,`),
|
|
1627
|
+
p(`Your account is ready. Welcome to ${appName}.`),
|
|
1628
|
+
button(appUrl, `Get started`),
|
|
1629
|
+
p("If you have any questions, just reply to this email.")
|
|
1630
|
+
].join("")
|
|
1631
|
+
)
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
function createEmailTemplates(config = {}) {
|
|
1635
|
+
const appName = config.appName ?? "KavachOS";
|
|
1636
|
+
const appUrl = config.appUrl ?? "http://localhost:3000";
|
|
1637
|
+
const overrides = config.templates ?? {};
|
|
1638
|
+
function render(name, vars) {
|
|
1639
|
+
const override = overrides[name];
|
|
1640
|
+
if (override) {
|
|
1641
|
+
return override(vars);
|
|
1642
|
+
}
|
|
1643
|
+
switch (name) {
|
|
1644
|
+
case "verification":
|
|
1645
|
+
return verificationTemplate(appName, appUrl, vars);
|
|
1646
|
+
case "passwordReset":
|
|
1647
|
+
return passwordResetTemplate(appName, appUrl, vars);
|
|
1648
|
+
case "magicLink":
|
|
1649
|
+
return magicLinkTemplate(appName, appUrl, vars);
|
|
1650
|
+
case "emailOtp":
|
|
1651
|
+
return emailOtpTemplate(appName, appUrl, vars);
|
|
1652
|
+
case "invitation":
|
|
1653
|
+
return invitationTemplate(appName, appUrl, vars);
|
|
1654
|
+
case "welcome":
|
|
1655
|
+
return welcomeTemplate(appName, appUrl, vars);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
return { render };
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// src/hooks/lifecycle.ts
|
|
1662
|
+
function classifyViolation(reason) {
|
|
1663
|
+
const r = reason?.toLowerCase() ?? "";
|
|
1664
|
+
if (r.includes("rate") || r.includes("rate_limited")) return "rate_limited";
|
|
1665
|
+
if (r.includes("ip") || r.includes("allowlist")) return "ip_blocked";
|
|
1666
|
+
if (r.includes("time") || r.includes("window")) return "time_restricted";
|
|
1667
|
+
if (r.includes("approval")) return "approval_required";
|
|
1668
|
+
return "permission_denied";
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// src/i18n/locales/en.ts
|
|
1672
|
+
var en = {
|
|
1673
|
+
// Auth errors
|
|
1674
|
+
"auth.invalidCredentials": "Invalid email or password.",
|
|
1675
|
+
"auth.emailNotVerified": "Please verify your email address before signing in.",
|
|
1676
|
+
"auth.accountLocked": "Your account has been locked. Contact support to unlock it.",
|
|
1677
|
+
"auth.rateLimited": "Too many requests. Try again in {{retryAfter}} seconds.",
|
|
1678
|
+
"auth.emailAlreadyExists": "An account with that email already exists.",
|
|
1679
|
+
"auth.weakPassword": "Password is too weak. Use at least 8 characters with a mix of letters, numbers, and symbols.",
|
|
1680
|
+
"auth.tokenExpired": "This link has expired. Request a new one.",
|
|
1681
|
+
"auth.tokenInvalid": "This link is invalid or has already been used.",
|
|
1682
|
+
"auth.unauthorized": "You are not authorized to perform this action.",
|
|
1683
|
+
// Agent errors
|
|
1684
|
+
"agent.notFound": "Agent not found.",
|
|
1685
|
+
"agent.revoked": "This agent's access has been revoked.",
|
|
1686
|
+
"agent.limitExceeded": "Agent limit reached for this account.",
|
|
1687
|
+
"agent.permissionDenied": "Agent does not have permission to perform this action.",
|
|
1688
|
+
// 2FA
|
|
1689
|
+
"twoFactor.invalidCode": "Invalid verification code. Check your authenticator app and try again.",
|
|
1690
|
+
"twoFactor.alreadyEnabled": "Two-factor authentication is already enabled on this account.",
|
|
1691
|
+
"twoFactor.notEnabled": "Two-factor authentication is not enabled on this account.",
|
|
1692
|
+
// Email subjects
|
|
1693
|
+
"email.verification.subject": "Verify your email address",
|
|
1694
|
+
"email.passwordReset.subject": "Reset your password",
|
|
1695
|
+
"email.magicLink.subject": "Your sign-in link",
|
|
1696
|
+
"email.otp.subject": "Your one-time code",
|
|
1697
|
+
"email.invitation.subject": "You have been invited to join {{orgName}}",
|
|
1698
|
+
"email.welcome.subject": "Welcome to {{appName}}",
|
|
1699
|
+
// General
|
|
1700
|
+
"general.serverError": "Something went wrong. Try again later.",
|
|
1701
|
+
"general.badRequest": "The request could not be processed.",
|
|
1702
|
+
"general.notFound": "The requested resource was not found."
|
|
1703
|
+
};
|
|
1704
|
+
|
|
1705
|
+
// src/i18n/i18n.ts
|
|
1706
|
+
function interpolate(template, vars) {
|
|
1707
|
+
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
1708
|
+
return Object.hasOwn(vars, key) ? vars[key] ?? match : `{{${key}}}`;
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
function resolveLocale(requested, registry, defaultLocale) {
|
|
1712
|
+
if (registry.has(requested)) return requested;
|
|
1713
|
+
const prefix = requested.split("-")[0];
|
|
1714
|
+
if (prefix && registry.has(prefix)) return prefix;
|
|
1715
|
+
if (registry.has(defaultLocale)) return defaultLocale;
|
|
1716
|
+
return "en";
|
|
1717
|
+
}
|
|
1718
|
+
function createI18n(config = {}) {
|
|
1719
|
+
const defaultLocale = config.defaultLocale ?? "en";
|
|
1720
|
+
const registry = /* @__PURE__ */ new Map();
|
|
1721
|
+
registry.set("en", { ...en });
|
|
1722
|
+
if (config.translations) {
|
|
1723
|
+
for (const [locale, keys] of Object.entries(config.translations)) {
|
|
1724
|
+
const existing = registry.get(locale) ?? {};
|
|
1725
|
+
registry.set(locale, { ...existing, ...keys });
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
function lookup(key, locale) {
|
|
1729
|
+
const resolved = resolveLocale(locale, registry, defaultLocale);
|
|
1730
|
+
const localeMap = registry.get(resolved);
|
|
1731
|
+
if (localeMap && key in localeMap) {
|
|
1732
|
+
return localeMap[key];
|
|
1733
|
+
}
|
|
1734
|
+
const englishMap = registry.get("en");
|
|
1735
|
+
if (englishMap && key in englishMap) {
|
|
1736
|
+
return englishMap[key];
|
|
1737
|
+
}
|
|
1738
|
+
return key;
|
|
1739
|
+
}
|
|
1740
|
+
function t(key, varsOrLocale, maybeLocale) {
|
|
1741
|
+
if (typeof varsOrLocale === "string" || varsOrLocale === void 0) {
|
|
1742
|
+
const locale2 = varsOrLocale ?? defaultLocale;
|
|
1743
|
+
return lookup(key, locale2);
|
|
1744
|
+
}
|
|
1745
|
+
const locale = maybeLocale ?? defaultLocale;
|
|
1746
|
+
const raw = lookup(key, locale);
|
|
1747
|
+
return interpolate(raw, varsOrLocale);
|
|
1748
|
+
}
|
|
1749
|
+
function addLocale(locale, translations) {
|
|
1750
|
+
const existing = registry.get(locale) ?? {};
|
|
1751
|
+
registry.set(locale, { ...existing, ...translations });
|
|
1752
|
+
}
|
|
1753
|
+
function getLocales() {
|
|
1754
|
+
return Array.from(registry.keys());
|
|
1755
|
+
}
|
|
1756
|
+
return { t, addLocale, getLocales };
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
// src/i18n/locales/de.ts
|
|
1760
|
+
var de = {
|
|
1761
|
+
// Auth errors
|
|
1762
|
+
"auth.invalidCredentials": "Ung\xFCltige E-Mail-Adresse oder falsches Passwort.",
|
|
1763
|
+
"auth.emailNotVerified": "Bitte best\xE4tige deine E-Mail-Adresse, bevor du dich anmeldest.",
|
|
1764
|
+
"auth.accountLocked": "Dein Konto wurde gesperrt. Wende dich an den Support, um es freizuschalten.",
|
|
1765
|
+
"auth.rateLimited": "Zu viele Anfragen. Versuche es in {{retryAfter}} Sekunden erneut.",
|
|
1766
|
+
"auth.emailAlreadyExists": "Ein Konto mit dieser E-Mail-Adresse existiert bereits.",
|
|
1767
|
+
"auth.weakPassword": "Das Passwort ist zu schwach. Verwende mindestens 8 Zeichen mit Buchstaben, Zahlen und Symbolen.",
|
|
1768
|
+
"auth.tokenExpired": "Dieser Link ist abgelaufen. Fordere einen neuen an.",
|
|
1769
|
+
"auth.tokenInvalid": "Dieser Link ist ung\xFCltig oder wurde bereits verwendet.",
|
|
1770
|
+
"auth.unauthorized": "Du bist nicht berechtigt, diese Aktion auszuf\xFChren.",
|
|
1771
|
+
// Agent errors
|
|
1772
|
+
"agent.notFound": "Agent nicht gefunden.",
|
|
1773
|
+
"agent.revoked": "Der Zugriff dieses Agenten wurde widerrufen.",
|
|
1774
|
+
"agent.limitExceeded": "Agentenlimit f\xFCr dieses Konto erreicht.",
|
|
1775
|
+
"agent.permissionDenied": "Der Agent hat keine Berechtigung f\xFCr diese Aktion.",
|
|
1776
|
+
// 2FA
|
|
1777
|
+
"twoFactor.invalidCode": "Ung\xFCltiger Best\xE4tigungscode. \xDCberpr\xFCfe deine Authentifizierungs-App und versuche es erneut.",
|
|
1778
|
+
"twoFactor.alreadyEnabled": "Die Zwei-Faktor-Authentifizierung ist f\xFCr dieses Konto bereits aktiviert.",
|
|
1779
|
+
"twoFactor.notEnabled": "Die Zwei-Faktor-Authentifizierung ist f\xFCr dieses Konto nicht aktiviert.",
|
|
1780
|
+
// Email subjects
|
|
1781
|
+
"email.verification.subject": "Best\xE4tige deine E-Mail-Adresse",
|
|
1782
|
+
"email.passwordReset.subject": "Setze dein Passwort zur\xFCck",
|
|
1783
|
+
"email.magicLink.subject": "Dein Anmelde-Link",
|
|
1784
|
+
"email.otp.subject": "Dein Einmalcode",
|
|
1785
|
+
"email.invitation.subject": "Du wurdest eingeladen, {{orgName}} beizutreten",
|
|
1786
|
+
"email.welcome.subject": "Willkommen bei {{appName}}",
|
|
1787
|
+
// General
|
|
1788
|
+
"general.serverError": "Etwas ist schiefgelaufen. Versuche es sp\xE4ter erneut.",
|
|
1789
|
+
"general.badRequest": "Die Anfrage konnte nicht verarbeitet werden.",
|
|
1790
|
+
"general.notFound": "Die angeforderte Ressource wurde nicht gefunden."
|
|
1791
|
+
};
|
|
1792
|
+
|
|
1793
|
+
// src/i18n/locales/es.ts
|
|
1794
|
+
var es = {
|
|
1795
|
+
// Auth errors
|
|
1796
|
+
"auth.invalidCredentials": "Correo electr\xF3nico o contrase\xF1a incorrectos.",
|
|
1797
|
+
"auth.emailNotVerified": "Verifica tu direcci\xF3n de correo electr\xF3nico antes de iniciar sesi\xF3n.",
|
|
1798
|
+
"auth.accountLocked": "Tu cuenta ha sido bloqueada. Contacta con soporte para desbloquearla.",
|
|
1799
|
+
"auth.rateLimited": "Demasiadas solicitudes. Int\xE9ntalo de nuevo en {{retryAfter}} segundos.",
|
|
1800
|
+
"auth.emailAlreadyExists": "Ya existe una cuenta con ese correo electr\xF3nico.",
|
|
1801
|
+
"auth.weakPassword": "La contrase\xF1a es demasiado d\xE9bil. Usa al menos 8 caracteres con letras, n\xFAmeros y s\xEDmbolos.",
|
|
1802
|
+
"auth.tokenExpired": "Este enlace ha caducado. Solicita uno nuevo.",
|
|
1803
|
+
"auth.tokenInvalid": "Este enlace no es v\xE1lido o ya ha sido utilizado.",
|
|
1804
|
+
"auth.unauthorized": "No tienes autorizaci\xF3n para realizar esta acci\xF3n.",
|
|
1805
|
+
// Agent errors
|
|
1806
|
+
"agent.notFound": "Agente no encontrado.",
|
|
1807
|
+
"agent.revoked": "El acceso de este agente ha sido revocado.",
|
|
1808
|
+
"agent.limitExceeded": "L\xEDmite de agentes alcanzado para esta cuenta.",
|
|
1809
|
+
"agent.permissionDenied": "El agente no tiene permiso para realizar esta acci\xF3n.",
|
|
1810
|
+
// 2FA
|
|
1811
|
+
"twoFactor.invalidCode": "C\xF3digo de verificaci\xF3n incorrecto. Comprueba tu aplicaci\xF3n autenticadora e int\xE9ntalo de nuevo.",
|
|
1812
|
+
"twoFactor.alreadyEnabled": "La autenticaci\xF3n de dos factores ya est\xE1 activada en esta cuenta.",
|
|
1813
|
+
"twoFactor.notEnabled": "La autenticaci\xF3n de dos factores no est\xE1 activada en esta cuenta.",
|
|
1814
|
+
// Email subjects
|
|
1815
|
+
"email.verification.subject": "Verifica tu direcci\xF3n de correo electr\xF3nico",
|
|
1816
|
+
"email.passwordReset.subject": "Restablece tu contrase\xF1a",
|
|
1817
|
+
"email.magicLink.subject": "Tu enlace de acceso",
|
|
1818
|
+
"email.otp.subject": "Tu c\xF3digo de un solo uso",
|
|
1819
|
+
"email.invitation.subject": "Has sido invitado a unirte a {{orgName}}",
|
|
1820
|
+
"email.welcome.subject": "Bienvenido a {{appName}}",
|
|
1821
|
+
// General
|
|
1822
|
+
"general.serverError": "Algo sali\xF3 mal. Int\xE9ntalo de nuevo m\xE1s tarde.",
|
|
1823
|
+
"general.badRequest": "La solicitud no pudo procesarse.",
|
|
1824
|
+
"general.notFound": "El recurso solicitado no fue encontrado."
|
|
1825
|
+
};
|
|
1826
|
+
|
|
1827
|
+
// src/i18n/locales/fr.ts
|
|
1828
|
+
var fr = {
|
|
1829
|
+
// Auth errors
|
|
1830
|
+
"auth.invalidCredentials": "Adresse e-mail ou mot de passe incorrect.",
|
|
1831
|
+
"auth.emailNotVerified": "Veuillez v\xE9rifier votre adresse e-mail avant de vous connecter.",
|
|
1832
|
+
"auth.accountLocked": "Votre compte a \xE9t\xE9 verrouill\xE9. Contactez le support pour le d\xE9verrouiller.",
|
|
1833
|
+
"auth.rateLimited": "Trop de tentatives. R\xE9essayez dans {{retryAfter}} secondes.",
|
|
1834
|
+
"auth.emailAlreadyExists": "Un compte avec cette adresse e-mail existe d\xE9j\xE0.",
|
|
1835
|
+
"auth.weakPassword": "Le mot de passe est trop faible. Utilisez au moins 8 caract\xE8res avec des lettres, des chiffres et des symboles.",
|
|
1836
|
+
"auth.tokenExpired": "Ce lien a expir\xE9. Demandez-en un nouveau.",
|
|
1837
|
+
"auth.tokenInvalid": "Ce lien est invalide ou a d\xE9j\xE0 \xE9t\xE9 utilis\xE9.",
|
|
1838
|
+
"auth.unauthorized": "Vous n'\xEAtes pas autoris\xE9 \xE0 effectuer cette action.",
|
|
1839
|
+
// Agent errors
|
|
1840
|
+
"agent.notFound": "Agent introuvable.",
|
|
1841
|
+
"agent.revoked": "L'acc\xE8s de cet agent a \xE9t\xE9 r\xE9voqu\xE9.",
|
|
1842
|
+
"agent.limitExceeded": "Limite d'agents atteinte pour ce compte.",
|
|
1843
|
+
"agent.permissionDenied": "L'agent n'est pas autoris\xE9 \xE0 effectuer cette action.",
|
|
1844
|
+
// 2FA
|
|
1845
|
+
"twoFactor.invalidCode": "Code de v\xE9rification invalide. V\xE9rifiez votre application d'authentification et r\xE9essayez.",
|
|
1846
|
+
"twoFactor.alreadyEnabled": "L'authentification \xE0 deux facteurs est d\xE9j\xE0 activ\xE9e sur ce compte.",
|
|
1847
|
+
"twoFactor.notEnabled": "L'authentification \xE0 deux facteurs n'est pas activ\xE9e sur ce compte.",
|
|
1848
|
+
// Email subjects
|
|
1849
|
+
"email.verification.subject": "V\xE9rifiez votre adresse e-mail",
|
|
1850
|
+
"email.passwordReset.subject": "R\xE9initialisez votre mot de passe",
|
|
1851
|
+
"email.magicLink.subject": "Votre lien de connexion",
|
|
1852
|
+
"email.otp.subject": "Votre code \xE0 usage unique",
|
|
1853
|
+
"email.invitation.subject": "Vous avez \xE9t\xE9 invit\xE9 \xE0 rejoindre {{orgName}}",
|
|
1854
|
+
"email.welcome.subject": "Bienvenue sur {{appName}}",
|
|
1855
|
+
// General
|
|
1856
|
+
"general.serverError": "Une erreur s'est produite. R\xE9essayez plus tard.",
|
|
1857
|
+
"general.badRequest": "La requ\xEAte n'a pas pu \xEAtre trait\xE9e.",
|
|
1858
|
+
"general.notFound": "La ressource demand\xE9e est introuvable."
|
|
1859
|
+
};
|
|
1860
|
+
|
|
1861
|
+
// src/i18n/locales/ja.ts
|
|
1862
|
+
var ja = {
|
|
1863
|
+
// Auth errors
|
|
1864
|
+
"auth.invalidCredentials": "\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u307E\u305F\u306F\u30D1\u30B9\u30EF\u30FC\u30C9\u304C\u6B63\u3057\u304F\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
1865
|
+
"auth.emailNotVerified": "\u30B5\u30A4\u30F3\u30A4\u30F3\u3059\u308B\u524D\u306B\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
1866
|
+
"auth.accountLocked": "\u30A2\u30AB\u30A6\u30F3\u30C8\u304C\u30ED\u30C3\u30AF\u3055\u308C\u3066\u3044\u307E\u3059\u3002\u30B5\u30DD\u30FC\u30C8\u306B\u9023\u7D61\u3057\u3066\u30ED\u30C3\u30AF\u3092\u89E3\u9664\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
1867
|
+
"auth.rateLimited": "\u30EA\u30AF\u30A8\u30B9\u30C8\u304C\u591A\u3059\u304E\u307E\u3059\u3002{{retryAfter}}\u79D2\u5F8C\u306B\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
1868
|
+
"auth.emailAlreadyExists": "\u305D\u306E\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u306E\u30A2\u30AB\u30A6\u30F3\u30C8\u306F\u3059\u3067\u306B\u5B58\u5728\u3057\u307E\u3059\u3002",
|
|
1869
|
+
"auth.weakPassword": "\u30D1\u30B9\u30EF\u30FC\u30C9\u304C\u5F31\u3059\u304E\u307E\u3059\u3002\u6587\u5B57\u3001\u6570\u5B57\u3001\u8A18\u53F7\u3092\u7D44\u307F\u5408\u308F\u305B\u305F8\u6587\u5B57\u4EE5\u4E0A\u306E\u30D1\u30B9\u30EF\u30FC\u30C9\u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
1870
|
+
"auth.tokenExpired": "\u3053\u306E\u30EA\u30F3\u30AF\u306E\u6709\u52B9\u671F\u9650\u304C\u5207\u308C\u3066\u3044\u307E\u3059\u3002\u65B0\u3057\u3044\u30EA\u30F3\u30AF\u3092\u30EA\u30AF\u30A8\u30B9\u30C8\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
1871
|
+
"auth.tokenInvalid": "\u3053\u306E\u30EA\u30F3\u30AF\u306F\u7121\u52B9\u304B\u3001\u3059\u3067\u306B\u4F7F\u7528\u3055\u308C\u3066\u3044\u307E\u3059\u3002",
|
|
1872
|
+
"auth.unauthorized": "\u3053\u306E\u64CD\u4F5C\u3092\u5B9F\u884C\u3059\u308B\u6A29\u9650\u304C\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
1873
|
+
// Agent errors
|
|
1874
|
+
"agent.notFound": "\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1875
|
+
"agent.revoked": "\u3053\u306E\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u306E\u30A2\u30AF\u30BB\u30B9\u304C\u53D6\u308A\u6D88\u3055\u308C\u307E\u3057\u305F\u3002",
|
|
1876
|
+
"agent.limitExceeded": "\u3053\u306E\u30A2\u30AB\u30A6\u30F3\u30C8\u306E\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u4E0A\u9650\u306B\u9054\u3057\u307E\u3057\u305F\u3002",
|
|
1877
|
+
"agent.permissionDenied": "\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u306B\u306F\u3053\u306E\u64CD\u4F5C\u3092\u5B9F\u884C\u3059\u308B\u6A29\u9650\u304C\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
1878
|
+
// 2FA
|
|
1879
|
+
"twoFactor.invalidCode": "\u78BA\u8A8D\u30B3\u30FC\u30C9\u304C\u6B63\u3057\u304F\u3042\u308A\u307E\u305B\u3093\u3002\u8A8D\u8A3C\u30A2\u30D7\u30EA\u3092\u78BA\u8A8D\u3057\u3066\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
1880
|
+
"twoFactor.alreadyEnabled": "\u3053\u306E\u30A2\u30AB\u30A6\u30F3\u30C8\u3067\u306F\u4E8C\u8981\u7D20\u8A8D\u8A3C\u304C\u3059\u3067\u306B\u6709\u52B9\u306B\u306A\u3063\u3066\u3044\u307E\u3059\u3002",
|
|
1881
|
+
"twoFactor.notEnabled": "\u3053\u306E\u30A2\u30AB\u30A6\u30F3\u30C8\u3067\u306F\u4E8C\u8981\u7D20\u8A8D\u8A3C\u304C\u6709\u52B9\u306B\u306A\u3063\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
1882
|
+
// Email subjects
|
|
1883
|
+
"email.verification.subject": "\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
1884
|
+
"email.passwordReset.subject": "\u30D1\u30B9\u30EF\u30FC\u30C9\u3092\u30EA\u30BB\u30C3\u30C8",
|
|
1885
|
+
"email.magicLink.subject": "\u30B5\u30A4\u30F3\u30A4\u30F3\u30EA\u30F3\u30AF",
|
|
1886
|
+
"email.otp.subject": "\u30EF\u30F3\u30BF\u30A4\u30E0\u30B3\u30FC\u30C9",
|
|
1887
|
+
"email.invitation.subject": "{{orgName}}\u3078\u306E\u62DB\u5F85",
|
|
1888
|
+
"email.welcome.subject": "{{appName}}\u3078\u3088\u3046\u3053\u305D",
|
|
1889
|
+
// General
|
|
1890
|
+
"general.serverError": "\u554F\u984C\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002\u5F8C\u3067\u3082\u3046\u4E00\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044\u3002",
|
|
1891
|
+
"general.badRequest": "\u30EA\u30AF\u30A8\u30B9\u30C8\u3092\u51E6\u7406\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002",
|
|
1892
|
+
"general.notFound": "\u30EA\u30AF\u30A8\u30B9\u30C8\u3055\u308C\u305F\u30EA\u30BD\u30FC\u30B9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002"
|
|
1893
|
+
};
|
|
1894
|
+
|
|
1895
|
+
// src/i18n/locales/zh.ts
|
|
1896
|
+
var zh = {
|
|
1897
|
+
// Auth errors
|
|
1898
|
+
"auth.invalidCredentials": "\u90AE\u7BB1\u6216\u5BC6\u7801\u4E0D\u6B63\u786E\u3002",
|
|
1899
|
+
"auth.emailNotVerified": "\u8BF7\u5728\u767B\u5F55\u524D\u9A8C\u8BC1\u60A8\u7684\u7535\u5B50\u90AE\u4EF6\u5730\u5740\u3002",
|
|
1900
|
+
"auth.accountLocked": "\u60A8\u7684\u8D26\u6237\u5DF2\u88AB\u9501\u5B9A\uFF0C\u8BF7\u8054\u7CFB\u652F\u6301\u56E2\u961F\u89E3\u9501\u3002",
|
|
1901
|
+
"auth.rateLimited": "\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u5728 {{retryAfter}} \u79D2\u540E\u91CD\u8BD5\u3002",
|
|
1902
|
+
"auth.emailAlreadyExists": "\u8BE5\u90AE\u7BB1\u5730\u5740\u5DF2\u6CE8\u518C\u8D26\u6237\u3002",
|
|
1903
|
+
"auth.weakPassword": "\u5BC6\u7801\u5F3A\u5EA6\u4E0D\u8DB3\uFF0C\u8BF7\u4F7F\u7528\u81F3\u5C11 8 \u4F4D\u5305\u542B\u5B57\u6BCD\u3001\u6570\u5B57\u548C\u7B26\u53F7\u7684\u5BC6\u7801\u3002",
|
|
1904
|
+
"auth.tokenExpired": "\u6B64\u94FE\u63A5\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u7533\u8BF7\u3002",
|
|
1905
|
+
"auth.tokenInvalid": "\u6B64\u94FE\u63A5\u65E0\u6548\u6216\u5DF2\u88AB\u4F7F\u7528\u3002",
|
|
1906
|
+
"auth.unauthorized": "\u60A8\u6CA1\u6709\u6743\u9650\u6267\u884C\u6B64\u64CD\u4F5C\u3002",
|
|
1907
|
+
// Agent errors
|
|
1908
|
+
"agent.notFound": "\u672A\u627E\u5230\u8BE5\u4EE3\u7406\u3002",
|
|
1909
|
+
"agent.revoked": "\u8BE5\u4EE3\u7406\u7684\u8BBF\u95EE\u6743\u9650\u5DF2\u88AB\u64A4\u9500\u3002",
|
|
1910
|
+
"agent.limitExceeded": "\u5DF2\u8FBE\u5230\u8BE5\u8D26\u6237\u7684\u4EE3\u7406\u6570\u91CF\u4E0A\u9650\u3002",
|
|
1911
|
+
"agent.permissionDenied": "\u4EE3\u7406\u6CA1\u6709\u6267\u884C\u6B64\u64CD\u4F5C\u7684\u6743\u9650\u3002",
|
|
1912
|
+
// 2FA
|
|
1913
|
+
"twoFactor.invalidCode": "\u9A8C\u8BC1\u7801\u65E0\u6548\uFF0C\u8BF7\u68C0\u67E5\u60A8\u7684\u9A8C\u8BC1\u5E94\u7528\u5E76\u91CD\u8BD5\u3002",
|
|
1914
|
+
"twoFactor.alreadyEnabled": "\u8BE5\u8D26\u6237\u5DF2\u542F\u7528\u53CC\u91CD\u9A8C\u8BC1\u3002",
|
|
1915
|
+
"twoFactor.notEnabled": "\u8BE5\u8D26\u6237\u672A\u542F\u7528\u53CC\u91CD\u9A8C\u8BC1\u3002",
|
|
1916
|
+
// Email subjects
|
|
1917
|
+
"email.verification.subject": "\u8BF7\u9A8C\u8BC1\u60A8\u7684\u7535\u5B50\u90AE\u4EF6\u5730\u5740",
|
|
1918
|
+
"email.passwordReset.subject": "\u91CD\u7F6E\u60A8\u7684\u5BC6\u7801",
|
|
1919
|
+
"email.magicLink.subject": "\u60A8\u7684\u767B\u5F55\u94FE\u63A5",
|
|
1920
|
+
"email.otp.subject": "\u60A8\u7684\u4E00\u6B21\u6027\u9A8C\u8BC1\u7801",
|
|
1921
|
+
"email.invitation.subject": "\u60A8\u5DF2\u88AB\u9080\u8BF7\u52A0\u5165 {{orgName}}",
|
|
1922
|
+
"email.welcome.subject": "\u6B22\u8FCE\u4F7F\u7528 {{appName}}",
|
|
1923
|
+
// General
|
|
1924
|
+
"general.serverError": "\u51FA\u73B0\u4E86\u4E00\u4E9B\u95EE\u9898\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002",
|
|
1925
|
+
"general.badRequest": "\u65E0\u6CD5\u5904\u7406\u8BE5\u8BF7\u6C42\u3002",
|
|
1926
|
+
"general.notFound": "\u672A\u627E\u5230\u8BF7\u6C42\u7684\u8D44\u6E90\u3002"
|
|
1927
|
+
};
|
|
1928
|
+
|
|
1929
|
+
// src/plugin/router.ts
|
|
1930
|
+
function matchPath(pattern, pathname) {
|
|
1931
|
+
const patternParts = pattern.split("/");
|
|
1932
|
+
const pathParts = pathname.split("/");
|
|
1933
|
+
if (patternParts.length !== pathParts.length) return null;
|
|
1934
|
+
const params = {};
|
|
1935
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
1936
|
+
const patternPart = patternParts[i];
|
|
1937
|
+
const pathPart = pathParts[i];
|
|
1938
|
+
if (patternPart === void 0 || pathPart === void 0) return null;
|
|
1939
|
+
if (patternPart.startsWith(":")) {
|
|
1940
|
+
const paramName = patternPart.slice(1);
|
|
1941
|
+
params[paramName] = decodeURIComponent(pathPart);
|
|
1942
|
+
} else if (patternPart !== pathPart) {
|
|
1943
|
+
return null;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
return params;
|
|
1947
|
+
}
|
|
1948
|
+
function createPluginRouter(endpoints) {
|
|
1949
|
+
return {
|
|
1950
|
+
async handle(request, basePath, endpointCtx) {
|
|
1951
|
+
const url = new URL(request.url);
|
|
1952
|
+
let pathname = url.pathname;
|
|
1953
|
+
const base = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
|
|
1954
|
+
if (base && pathname.startsWith(base)) {
|
|
1955
|
+
pathname = pathname.slice(base.length) || "/";
|
|
1956
|
+
}
|
|
1957
|
+
if (!pathname.startsWith("/")) {
|
|
1958
|
+
pathname = `/${pathname}`;
|
|
1959
|
+
}
|
|
1960
|
+
if (pathname.length > 1 && pathname.endsWith("/")) {
|
|
1961
|
+
pathname = pathname.slice(0, -1);
|
|
1962
|
+
}
|
|
1963
|
+
const method = request.method.toUpperCase();
|
|
1964
|
+
for (const endpoint of endpoints) {
|
|
1965
|
+
if (endpoint.method !== method) continue;
|
|
1966
|
+
const params = matchPath(endpoint.path, pathname);
|
|
1967
|
+
if (params === null) continue;
|
|
1968
|
+
const enrichedUrl = new URL(request.url);
|
|
1969
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1970
|
+
enrichedUrl.searchParams.set(`_param_${key}`, value);
|
|
1971
|
+
}
|
|
1972
|
+
const enrichedRequest = new Request(enrichedUrl.toString(), request);
|
|
1973
|
+
return endpoint.handler(enrichedRequest, endpointCtx);
|
|
1974
|
+
}
|
|
1975
|
+
return null;
|
|
1976
|
+
},
|
|
1977
|
+
getEndpoints() {
|
|
1978
|
+
return [...endpoints];
|
|
1979
|
+
}
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// src/plugin/runner.ts
|
|
1984
|
+
async function runMigrations(db, provider, statements) {
|
|
1985
|
+
if (statements.length === 0) return;
|
|
1986
|
+
if (provider === "sqlite") {
|
|
1987
|
+
const session = db.session;
|
|
1988
|
+
if (session?.client?.exec) {
|
|
1989
|
+
session.client.exec(`${statements.join(";\n")};`);
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
const anyDb2 = db;
|
|
1993
|
+
for (const sql of statements) {
|
|
1994
|
+
await anyDb2.run(sql);
|
|
1995
|
+
}
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
const anyDb = db;
|
|
1999
|
+
if (provider === "postgres") {
|
|
2000
|
+
const client = anyDb.$client ?? anyDb.session?.client;
|
|
2001
|
+
if (!client) {
|
|
2002
|
+
throw new Error(
|
|
2003
|
+
"KavachOS plugin migrations: cannot access underlying pg client from Drizzle instance."
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
2006
|
+
for (const sql of statements) {
|
|
2007
|
+
await client.query(sql);
|
|
2008
|
+
}
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
if (provider === "mysql") {
|
|
2012
|
+
const client = anyDb.$client ?? anyDb.session?.client;
|
|
2013
|
+
if (!client) {
|
|
2014
|
+
throw new Error(
|
|
2015
|
+
"KavachOS plugin migrations: cannot access underlying mysql2 client from Drizzle instance."
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
for (const sql of statements) {
|
|
2019
|
+
await client.execute(sql);
|
|
2020
|
+
}
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
throw new Error(`runMigrations: unsupported provider "${provider}"`);
|
|
2024
|
+
}
|
|
2025
|
+
async function initializePlugins(plugins, db, config) {
|
|
2026
|
+
const registry = {
|
|
2027
|
+
endpoints: [],
|
|
2028
|
+
migrations: [],
|
|
2029
|
+
hooks: {
|
|
2030
|
+
onRequest: [],
|
|
2031
|
+
onAuthenticate: [],
|
|
2032
|
+
onSessionCreate: [],
|
|
2033
|
+
onSessionRevoke: []
|
|
2034
|
+
},
|
|
2035
|
+
pluginContext: {}
|
|
2036
|
+
};
|
|
2037
|
+
for (const plugin of plugins) {
|
|
2038
|
+
const pluginMigrations = [];
|
|
2039
|
+
const ctx = {
|
|
2040
|
+
db,
|
|
2041
|
+
config,
|
|
2042
|
+
addEndpoint(endpoint) {
|
|
2043
|
+
registry.endpoints.push(endpoint);
|
|
2044
|
+
},
|
|
2045
|
+
addMigration(sql) {
|
|
2046
|
+
pluginMigrations.push(sql);
|
|
2047
|
+
registry.migrations.push(sql);
|
|
2048
|
+
}
|
|
2049
|
+
};
|
|
2050
|
+
if (plugin.init) {
|
|
2051
|
+
const result = await plugin.init(ctx);
|
|
2052
|
+
if (result?.context) {
|
|
2053
|
+
Object.assign(registry.pluginContext, result.context);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
if (plugin.hooks) {
|
|
2057
|
+
if (plugin.hooks.onRequest) {
|
|
2058
|
+
registry.hooks.onRequest.push(plugin.hooks.onRequest);
|
|
2059
|
+
}
|
|
2060
|
+
if (plugin.hooks.onAuthenticate) {
|
|
2061
|
+
registry.hooks.onAuthenticate.push(plugin.hooks.onAuthenticate);
|
|
2062
|
+
}
|
|
2063
|
+
if (plugin.hooks.onSessionCreate) {
|
|
2064
|
+
registry.hooks.onSessionCreate.push(plugin.hooks.onSessionCreate);
|
|
2065
|
+
}
|
|
2066
|
+
if (plugin.hooks.onSessionRevoke) {
|
|
2067
|
+
registry.hooks.onSessionRevoke.push(plugin.hooks.onSessionRevoke);
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
if (pluginMigrations.length > 0) {
|
|
2071
|
+
await runMigrations(db, config.database.provider, pluginMigrations);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
return registry;
|
|
2075
|
+
}
|
|
2076
|
+
function emptyUsage() {
|
|
2077
|
+
return {
|
|
2078
|
+
tokensCostToday: 0,
|
|
2079
|
+
tokensCostThisMonth: 0,
|
|
2080
|
+
callsToday: 0,
|
|
2081
|
+
callsThisMonth: 0,
|
|
2082
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
function rowToPolicy(row) {
|
|
2086
|
+
return {
|
|
2087
|
+
id: row.id,
|
|
2088
|
+
agentId: row.agentId ?? void 0,
|
|
2089
|
+
userId: row.userId ?? void 0,
|
|
2090
|
+
tenantId: row.tenantId ?? void 0,
|
|
2091
|
+
limits: row.limits ?? {},
|
|
2092
|
+
currentUsage: row.currentUsage ?? emptyUsage(),
|
|
2093
|
+
action: row.action,
|
|
2094
|
+
status: row.status,
|
|
2095
|
+
createdAt: row.createdAt
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
function isExceeded(limits, usage) {
|
|
2099
|
+
if (limits.maxCallsPerDay !== void 0 && usage.callsToday >= limits.maxCallsPerDay) return true;
|
|
2100
|
+
if (limits.maxCallsPerMonth !== void 0 && usage.callsThisMonth >= limits.maxCallsPerMonth)
|
|
2101
|
+
return true;
|
|
2102
|
+
if (limits.maxTokensCostPerDay !== void 0 && usage.tokensCostToday >= limits.maxTokensCostPerDay)
|
|
2103
|
+
return true;
|
|
2104
|
+
if (limits.maxTokensCostPerMonth !== void 0 && usage.tokensCostThisMonth >= limits.maxTokensCostPerMonth)
|
|
2105
|
+
return true;
|
|
2106
|
+
return false;
|
|
2107
|
+
}
|
|
2108
|
+
function createPolicyModule(db) {
|
|
2109
|
+
async function create(input) {
|
|
2110
|
+
const id = `pol_${randomUUID().replace(/-/g, "")}`;
|
|
2111
|
+
const now = /* @__PURE__ */ new Date();
|
|
2112
|
+
const usage = emptyUsage();
|
|
2113
|
+
await db.insert(budgetPolicies).values({
|
|
2114
|
+
id,
|
|
2115
|
+
agentId: input.agentId ?? null,
|
|
2116
|
+
userId: input.userId ?? null,
|
|
2117
|
+
tenantId: input.tenantId ?? null,
|
|
2118
|
+
limits: input.limits,
|
|
2119
|
+
currentUsage: usage,
|
|
2120
|
+
action: input.action,
|
|
2121
|
+
status: "active",
|
|
2122
|
+
createdAt: now
|
|
2123
|
+
});
|
|
2124
|
+
return {
|
|
2125
|
+
id,
|
|
2126
|
+
agentId: input.agentId,
|
|
2127
|
+
userId: input.userId,
|
|
2128
|
+
tenantId: input.tenantId,
|
|
2129
|
+
limits: input.limits,
|
|
2130
|
+
currentUsage: usage,
|
|
2131
|
+
action: input.action,
|
|
2132
|
+
status: "active",
|
|
2133
|
+
createdAt: now
|
|
2134
|
+
};
|
|
2135
|
+
}
|
|
2136
|
+
async function get(policyId) {
|
|
2137
|
+
const rows = await db.select().from(budgetPolicies).where(eq(budgetPolicies.id, policyId)).limit(1);
|
|
2138
|
+
const row = rows[0];
|
|
2139
|
+
if (!row) return null;
|
|
2140
|
+
return rowToPolicy(row);
|
|
2141
|
+
}
|
|
2142
|
+
async function list(filters) {
|
|
2143
|
+
let query = db.select().from(budgetPolicies).$dynamic();
|
|
2144
|
+
const conditions = [];
|
|
2145
|
+
if (filters?.agentId !== void 0) {
|
|
2146
|
+
conditions.push(
|
|
2147
|
+
or(eq(budgetPolicies.agentId, filters.agentId), isNull(budgetPolicies.agentId))
|
|
2148
|
+
);
|
|
2149
|
+
}
|
|
2150
|
+
if (filters?.userId !== void 0) {
|
|
2151
|
+
conditions.push(or(eq(budgetPolicies.userId, filters.userId), isNull(budgetPolicies.userId)));
|
|
2152
|
+
}
|
|
2153
|
+
if (filters?.tenantId !== void 0) {
|
|
2154
|
+
conditions.push(
|
|
2155
|
+
or(eq(budgetPolicies.tenantId, filters.tenantId), isNull(budgetPolicies.tenantId))
|
|
2156
|
+
);
|
|
2157
|
+
}
|
|
2158
|
+
if (conditions.length > 0) {
|
|
2159
|
+
query = query.where(and(...conditions));
|
|
2160
|
+
}
|
|
2161
|
+
const rows = await query;
|
|
2162
|
+
return rows.map(rowToPolicy);
|
|
2163
|
+
}
|
|
2164
|
+
async function update(policyId, updates) {
|
|
2165
|
+
const existing = await get(policyId);
|
|
2166
|
+
if (!existing) throw new Error(`Policy "${policyId}" not found.`);
|
|
2167
|
+
await db.update(budgetPolicies).set({
|
|
2168
|
+
limits: updates.limits ?? existing.limits,
|
|
2169
|
+
currentUsage: updates.currentUsage ?? existing.currentUsage,
|
|
2170
|
+
action: updates.action ?? existing.action,
|
|
2171
|
+
status: updates.status ?? existing.status
|
|
2172
|
+
}).where(eq(budgetPolicies.id, policyId));
|
|
2173
|
+
const updated = await get(policyId);
|
|
2174
|
+
if (!updated) throw new Error(`Policy "${policyId}" disappeared after update.`);
|
|
2175
|
+
return updated;
|
|
2176
|
+
}
|
|
2177
|
+
async function remove(policyId) {
|
|
2178
|
+
const existing = await get(policyId);
|
|
2179
|
+
if (!existing) throw new Error(`Policy "${policyId}" not found.`);
|
|
2180
|
+
await db.delete(budgetPolicies).where(eq(budgetPolicies.id, policyId));
|
|
2181
|
+
}
|
|
2182
|
+
async function checkBudget(agentId, tokensCost) {
|
|
2183
|
+
const rows = await db.select().from(budgetPolicies).where(
|
|
2184
|
+
and(
|
|
2185
|
+
ne(budgetPolicies.status, "disabled"),
|
|
2186
|
+
or(eq(budgetPolicies.agentId, agentId), isNull(budgetPolicies.agentId))
|
|
2187
|
+
)
|
|
2188
|
+
);
|
|
2189
|
+
for (const row of rows) {
|
|
2190
|
+
const policy = rowToPolicy(row);
|
|
2191
|
+
const usage = { ...policy.currentUsage };
|
|
2192
|
+
if (tokensCost !== void 0) {
|
|
2193
|
+
usage.tokensCostToday += tokensCost;
|
|
2194
|
+
usage.tokensCostThisMonth += tokensCost;
|
|
2195
|
+
}
|
|
2196
|
+
if (isExceeded(policy.limits, usage)) {
|
|
2197
|
+
return {
|
|
2198
|
+
allowed: policy.action === "warn",
|
|
2199
|
+
reason: `Budget policy "${policy.id}" exceeded (action: ${policy.action})`,
|
|
2200
|
+
policy
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
return { allowed: true };
|
|
2205
|
+
}
|
|
2206
|
+
async function recordUsage(agentId, tokensCost) {
|
|
2207
|
+
const rows = await db.select().from(budgetPolicies).where(
|
|
2208
|
+
and(
|
|
2209
|
+
ne(budgetPolicies.status, "disabled"),
|
|
2210
|
+
or(eq(budgetPolicies.agentId, agentId), isNull(budgetPolicies.agentId))
|
|
2211
|
+
)
|
|
2212
|
+
);
|
|
2213
|
+
for (const row of rows) {
|
|
2214
|
+
const policy = rowToPolicy(row);
|
|
2215
|
+
const usage = {
|
|
2216
|
+
tokensCostToday: policy.currentUsage.tokensCostToday + (tokensCost ?? 0),
|
|
2217
|
+
tokensCostThisMonth: policy.currentUsage.tokensCostThisMonth + (tokensCost ?? 0),
|
|
2218
|
+
callsToday: policy.currentUsage.callsToday + 1,
|
|
2219
|
+
callsThisMonth: policy.currentUsage.callsThisMonth + 1,
|
|
2220
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
2221
|
+
};
|
|
2222
|
+
const exceeded = isExceeded(policy.limits, usage);
|
|
2223
|
+
const newStatus = exceeded ? "triggered" : policy.status;
|
|
2224
|
+
await db.update(budgetPolicies).set({ currentUsage: usage, status: newStatus }).where(eq(budgetPolicies.id, policy.id));
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
async function resetDaily() {
|
|
2228
|
+
const rows = await db.select().from(budgetPolicies);
|
|
2229
|
+
let reset = 0;
|
|
2230
|
+
for (const row of rows) {
|
|
2231
|
+
const policy = rowToPolicy(row);
|
|
2232
|
+
const usage = {
|
|
2233
|
+
...policy.currentUsage,
|
|
2234
|
+
tokensCostToday: 0,
|
|
2235
|
+
callsToday: 0,
|
|
2236
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
2237
|
+
};
|
|
2238
|
+
const stillExceeded = isExceeded(policy.limits, usage);
|
|
2239
|
+
const newStatus = stillExceeded ? "triggered" : policy.status === "triggered" ? "active" : policy.status;
|
|
2240
|
+
await db.update(budgetPolicies).set({ currentUsage: usage, status: newStatus }).where(eq(budgetPolicies.id, policy.id));
|
|
2241
|
+
reset++;
|
|
2242
|
+
}
|
|
2243
|
+
return { reset };
|
|
2244
|
+
}
|
|
2245
|
+
async function resetMonthly() {
|
|
2246
|
+
const rows = await db.select().from(budgetPolicies);
|
|
2247
|
+
let reset = 0;
|
|
2248
|
+
for (const row of rows) {
|
|
2249
|
+
const policy = rowToPolicy(row);
|
|
2250
|
+
const usage = {
|
|
2251
|
+
...policy.currentUsage,
|
|
2252
|
+
tokensCostThisMonth: 0,
|
|
2253
|
+
callsThisMonth: 0,
|
|
2254
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
2255
|
+
};
|
|
2256
|
+
const stillExceeded = isExceeded(policy.limits, usage);
|
|
2257
|
+
const newStatus = stillExceeded ? "triggered" : policy.status === "triggered" ? "active" : policy.status;
|
|
2258
|
+
await db.update(budgetPolicies).set({ currentUsage: usage, status: newStatus }).where(eq(budgetPolicies.id, policy.id));
|
|
2259
|
+
reset++;
|
|
2260
|
+
}
|
|
2261
|
+
return { reset };
|
|
2262
|
+
}
|
|
2263
|
+
return { create, get, list, update, remove, checkBudget, recordUsage, resetDaily, resetMonthly };
|
|
2264
|
+
}
|
|
2265
|
+
function slugRegex() {
|
|
2266
|
+
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2267
|
+
}
|
|
2268
|
+
function rowToTenant(row) {
|
|
2269
|
+
return {
|
|
2270
|
+
id: row.id,
|
|
2271
|
+
name: row.name,
|
|
2272
|
+
slug: row.slug,
|
|
2273
|
+
settings: row.settings ?? {},
|
|
2274
|
+
status: row.status,
|
|
2275
|
+
createdAt: row.createdAt,
|
|
2276
|
+
updatedAt: row.updatedAt
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
function createTenantModule(db) {
|
|
2280
|
+
async function create(input) {
|
|
2281
|
+
if (!slugRegex().test(input.slug)) {
|
|
2282
|
+
throw new Error(
|
|
2283
|
+
`Invalid slug "${input.slug}". Use lowercase letters, numbers, and hyphens only.`
|
|
2284
|
+
);
|
|
2285
|
+
}
|
|
2286
|
+
const existing = await db.select().from(tenants).where(eq(tenants.slug, input.slug)).limit(1);
|
|
2287
|
+
if (existing.length > 0) {
|
|
2288
|
+
throw new Error(`Tenant with slug "${input.slug}" already exists.`);
|
|
2289
|
+
}
|
|
2290
|
+
const id = `tnt_${randomUUID().replace(/-/g, "")}`;
|
|
2291
|
+
const now = /* @__PURE__ */ new Date();
|
|
2292
|
+
const settings = input.settings ?? {};
|
|
2293
|
+
await db.insert(tenants).values({
|
|
2294
|
+
id,
|
|
2295
|
+
name: input.name,
|
|
2296
|
+
slug: input.slug,
|
|
2297
|
+
settings,
|
|
2298
|
+
status: "active",
|
|
2299
|
+
createdAt: now,
|
|
2300
|
+
updatedAt: now
|
|
2301
|
+
});
|
|
2302
|
+
return {
|
|
2303
|
+
id,
|
|
2304
|
+
name: input.name,
|
|
2305
|
+
slug: input.slug,
|
|
2306
|
+
settings,
|
|
2307
|
+
status: "active",
|
|
2308
|
+
createdAt: now,
|
|
2309
|
+
updatedAt: now
|
|
2310
|
+
};
|
|
2311
|
+
}
|
|
2312
|
+
async function get(tenantId) {
|
|
2313
|
+
const rows = await db.select().from(tenants).where(eq(tenants.id, tenantId)).limit(1);
|
|
2314
|
+
const row = rows[0];
|
|
2315
|
+
if (!row) return null;
|
|
2316
|
+
return rowToTenant(row);
|
|
2317
|
+
}
|
|
2318
|
+
async function getBySlug(slug) {
|
|
2319
|
+
const rows = await db.select().from(tenants).where(eq(tenants.slug, slug)).limit(1);
|
|
2320
|
+
const row = rows[0];
|
|
2321
|
+
if (!row) return null;
|
|
2322
|
+
return rowToTenant(row);
|
|
2323
|
+
}
|
|
2324
|
+
async function list() {
|
|
2325
|
+
const rows = await db.select().from(tenants);
|
|
2326
|
+
return rows.map(rowToTenant);
|
|
2327
|
+
}
|
|
2328
|
+
async function update(tenantId, updates) {
|
|
2329
|
+
const existing = await get(tenantId);
|
|
2330
|
+
if (!existing) throw new Error(`Tenant "${tenantId}" not found.`);
|
|
2331
|
+
if (updates.slug !== void 0 && updates.slug !== existing.slug) {
|
|
2332
|
+
if (!slugRegex().test(updates.slug)) {
|
|
2333
|
+
throw new Error(
|
|
2334
|
+
`Invalid slug "${updates.slug}". Use lowercase letters, numbers, and hyphens only.`
|
|
2335
|
+
);
|
|
2336
|
+
}
|
|
2337
|
+
const conflict = await db.select().from(tenants).where(eq(tenants.slug, updates.slug)).limit(1);
|
|
2338
|
+
if (conflict.length > 0) {
|
|
2339
|
+
throw new Error(`Tenant with slug "${updates.slug}" already exists.`);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
const now = /* @__PURE__ */ new Date();
|
|
2343
|
+
await db.update(tenants).set({
|
|
2344
|
+
name: updates.name ?? existing.name,
|
|
2345
|
+
slug: updates.slug ?? existing.slug,
|
|
2346
|
+
settings: updates.settings ? { ...existing.settings, ...updates.settings } : existing.settings,
|
|
2347
|
+
updatedAt: now
|
|
2348
|
+
}).where(eq(tenants.id, tenantId));
|
|
2349
|
+
const updated = await get(tenantId);
|
|
2350
|
+
if (!updated) throw new Error(`Tenant "${tenantId}" disappeared after update.`);
|
|
2351
|
+
return updated;
|
|
2352
|
+
}
|
|
2353
|
+
async function suspend(tenantId) {
|
|
2354
|
+
const existing = await get(tenantId);
|
|
2355
|
+
if (!existing) throw new Error(`Tenant "${tenantId}" not found.`);
|
|
2356
|
+
await db.update(tenants).set({ status: "suspended", updatedAt: /* @__PURE__ */ new Date() }).where(eq(tenants.id, tenantId));
|
|
2357
|
+
}
|
|
2358
|
+
async function activate(tenantId) {
|
|
2359
|
+
const existing = await get(tenantId);
|
|
2360
|
+
if (!existing) throw new Error(`Tenant "${tenantId}" not found.`);
|
|
2361
|
+
await db.update(tenants).set({ status: "active", updatedAt: /* @__PURE__ */ new Date() }).where(eq(tenants.id, tenantId));
|
|
2362
|
+
}
|
|
2363
|
+
return { create, get, getBySlug, list, update, suspend, activate };
|
|
2364
|
+
}
|
|
2365
|
+
var DEFAULT_THRESHOLDS = {
|
|
2366
|
+
untrusted: 20,
|
|
2367
|
+
limited: 40,
|
|
2368
|
+
standard: 60,
|
|
2369
|
+
trusted: 80,
|
|
2370
|
+
elevated: 95
|
|
2371
|
+
};
|
|
2372
|
+
function scoreToLevel(score, thresholds) {
|
|
2373
|
+
if (score >= thresholds.elevated) return "elevated";
|
|
2374
|
+
if (score >= thresholds.trusted) return "trusted";
|
|
2375
|
+
if (score >= thresholds.standard) return "standard";
|
|
2376
|
+
if (score >= thresholds.limited) return "limited";
|
|
2377
|
+
return "untrusted";
|
|
2378
|
+
}
|
|
2379
|
+
function clamp(value, min, max) {
|
|
2380
|
+
return Math.max(min, Math.min(max, value));
|
|
2381
|
+
}
|
|
2382
|
+
function rowToScore(row) {
|
|
2383
|
+
const factors = row.factors;
|
|
2384
|
+
return {
|
|
2385
|
+
agentId: row.agentId,
|
|
2386
|
+
score: row.score,
|
|
2387
|
+
level: row.level,
|
|
2388
|
+
factors,
|
|
2389
|
+
computedAt: row.computedAt.toISOString()
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
2392
|
+
function createTrustModule(config, db) {
|
|
2393
|
+
const thresholds = { ...DEFAULT_THRESHOLDS, ...config.thresholds };
|
|
2394
|
+
async function computeScore(agentId) {
|
|
2395
|
+
const now = /* @__PURE__ */ new Date();
|
|
2396
|
+
const agentRows = await db.select({ createdAt: agents.createdAt }).from(agents).where(eq(agents.id, agentId)).limit(1);
|
|
2397
|
+
const agentRow = agentRows[0];
|
|
2398
|
+
const ageInDays = agentRow ? (now.getTime() - agentRow.createdAt.getTime()) / (1e3 * 60 * 60 * 24) : 0;
|
|
2399
|
+
const allLogs = await db.select({
|
|
2400
|
+
result: auditLogs.result,
|
|
2401
|
+
reason: auditLogs.reason,
|
|
2402
|
+
timestamp: auditLogs.timestamp
|
|
2403
|
+
}).from(auditLogs).where(eq(auditLogs.agentId, agentId));
|
|
2404
|
+
const totalCalls = allLogs.length;
|
|
2405
|
+
const allowed = allLogs.filter((r) => r.result === "allowed").length;
|
|
2406
|
+
const denied = allLogs.filter((r) => r.result === "denied").length;
|
|
2407
|
+
const successRate = totalCalls > 0 ? allowed / totalCalls * 100 : 100;
|
|
2408
|
+
const denialRate = totalCalls > 0 ? denied / totalCalls * 100 : 0;
|
|
2409
|
+
const anomalyCount = allLogs.filter((r) => {
|
|
2410
|
+
if (r.result !== "denied") return false;
|
|
2411
|
+
const reason = r.reason ?? "";
|
|
2412
|
+
return reason.includes("INSUFFICIENT_PERMISSIONS") || reason.toLowerCase().includes("privilege") || reason.toLowerCase().includes("escalation");
|
|
2413
|
+
}).length;
|
|
2414
|
+
const violationLogs = allLogs.filter((r) => r.result === "denied").sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
2415
|
+
const lastViolation = violationLogs[0]?.timestamp.toISOString();
|
|
2416
|
+
let score = 50;
|
|
2417
|
+
score += Math.min(25, Math.floor(allowed / 100));
|
|
2418
|
+
score -= denied * 5;
|
|
2419
|
+
score -= anomalyCount * 10;
|
|
2420
|
+
if (ageInDays > 30) score += 10;
|
|
2421
|
+
else if (ageInDays > 7) score += 5;
|
|
2422
|
+
score = clamp(Math.round(score), 0, 100);
|
|
2423
|
+
const level = scoreToLevel(score, thresholds);
|
|
2424
|
+
const factors = {
|
|
2425
|
+
successRate: Math.round(successRate * 10) / 10,
|
|
2426
|
+
denialRate: Math.round(denialRate * 10) / 10,
|
|
2427
|
+
ageInDays: Math.round(ageInDays * 10) / 10,
|
|
2428
|
+
totalCalls,
|
|
2429
|
+
anomalyCount,
|
|
2430
|
+
lastViolation
|
|
2431
|
+
};
|
|
2432
|
+
const existingRows = await db.select({ agentId: trustScores.agentId }).from(trustScores).where(eq(trustScores.agentId, agentId)).limit(1);
|
|
2433
|
+
if (existingRows.length > 0) {
|
|
2434
|
+
await db.update(trustScores).set({ score, level, factors, computedAt: now }).where(eq(trustScores.agentId, agentId));
|
|
2435
|
+
} else {
|
|
2436
|
+
await db.insert(trustScores).values({
|
|
2437
|
+
agentId,
|
|
2438
|
+
score,
|
|
2439
|
+
level,
|
|
2440
|
+
factors,
|
|
2441
|
+
computedAt: now
|
|
2442
|
+
});
|
|
2443
|
+
}
|
|
2444
|
+
return {
|
|
2445
|
+
agentId,
|
|
2446
|
+
score,
|
|
2447
|
+
level,
|
|
2448
|
+
factors,
|
|
2449
|
+
computedAt: now.toISOString()
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
async function getScore(agentId) {
|
|
2453
|
+
const rows = await db.select().from(trustScores).where(eq(trustScores.agentId, agentId)).limit(1);
|
|
2454
|
+
const row = rows[0];
|
|
2455
|
+
if (!row) return null;
|
|
2456
|
+
return rowToScore(row);
|
|
2457
|
+
}
|
|
2458
|
+
async function computeAll() {
|
|
2459
|
+
const activeAgents = await db.select({ id: agents.id }).from(agents).where(eq(agents.status, "active"));
|
|
2460
|
+
const results = [];
|
|
2461
|
+
for (const agent of activeAgents) {
|
|
2462
|
+
const score = await computeScore(agent.id);
|
|
2463
|
+
results.push(score);
|
|
2464
|
+
}
|
|
2465
|
+
return results;
|
|
2466
|
+
}
|
|
2467
|
+
async function getScores(filters) {
|
|
2468
|
+
const rows = await db.select().from(trustScores);
|
|
2469
|
+
let scores = rows.map(rowToScore);
|
|
2470
|
+
if (filters?.level) {
|
|
2471
|
+
scores = scores.filter((s) => s.level === filters.level);
|
|
2472
|
+
}
|
|
2473
|
+
if (filters?.minScore !== void 0) {
|
|
2474
|
+
const min = filters.minScore;
|
|
2475
|
+
scores = scores.filter((s) => s.score >= min);
|
|
2476
|
+
}
|
|
2477
|
+
return scores;
|
|
2478
|
+
}
|
|
2479
|
+
return {
|
|
2480
|
+
computeScore,
|
|
2481
|
+
getScore,
|
|
2482
|
+
computeAll,
|
|
2483
|
+
getScores
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
// src/kavach.ts
|
|
2488
|
+
function classifyViolation2(reason) {
|
|
2489
|
+
const r = reason?.toLowerCase() ?? "";
|
|
2490
|
+
if (r.includes("rate") || r.includes("rate_limited")) return "rate_limited";
|
|
2491
|
+
if (r.includes("ip") || r.includes("allowlist")) return "ip_blocked";
|
|
2492
|
+
if (r.includes("time") || r.includes("window")) return "time_restricted";
|
|
2493
|
+
if (r.includes("approval")) return "approval_required";
|
|
2494
|
+
return "permission_denied";
|
|
2495
|
+
}
|
|
2496
|
+
async function createKavach(config) {
|
|
2497
|
+
const authAdapter = config.auth?.adapter ?? null;
|
|
458
2498
|
const db = await createDatabase(config.database);
|
|
459
2499
|
if (!config.database.skipMigrations) {
|
|
460
2500
|
await createTables(db, config.database.provider);
|
|
@@ -473,7 +2513,58 @@ async function createKavach(config) {
|
|
|
473
2513
|
const auditModule = createAuditModule({ db });
|
|
474
2514
|
const delegationModule = createDelegationModule({ db });
|
|
475
2515
|
const sessionManager = config.auth?.session ? createSessionManager(config.auth.session, db) : null;
|
|
2516
|
+
const privilegeAnalyzer = createPrivilegeAnalyzer(db);
|
|
2517
|
+
const hooks = config.hooks ?? {};
|
|
2518
|
+
const tenantModule = createTenantModule(db);
|
|
2519
|
+
const policyModule = createPolicyModule(db);
|
|
2520
|
+
const approvalModule = createApprovalModule(config.approval ?? {}, db);
|
|
2521
|
+
const trustModule = createTrustModule({}, db);
|
|
2522
|
+
const didModule = createDidModule(db, config.did);
|
|
2523
|
+
const magicLinkModule = config.magicLink && sessionManager ? createMagicLinkModule(config.magicLink, db, sessionManager) : null;
|
|
2524
|
+
const emailOtpModule = config.emailOtp && sessionManager ? createEmailOtpModule(config.emailOtp, db, sessionManager) : null;
|
|
2525
|
+
const totpModule = config.totp ? createTotpModule(config.totp, db) : null;
|
|
2526
|
+
const passkeyModule = config.passkey ? createPasskeyModule(config.passkey, db) : null;
|
|
2527
|
+
const orgModule = config.org ? createOrgModule(config.org, db) : null;
|
|
2528
|
+
const ssoModule = config.sso ? createSsoModule(config.sso, db) : null;
|
|
2529
|
+
const adminModule = config.admin ? createAdminModule(config.admin, db, sessionManager) : null;
|
|
2530
|
+
const apiKeyManagerModule = config.apiKeys ? createApiKeyManagerModule(config.apiKeys, db) : null;
|
|
2531
|
+
const usernameModule = config.username && sessionManager ? createUsernameAuthModule(config.username, db, sessionManager) : null;
|
|
2532
|
+
const phoneModule = config.phone && sessionManager ? createPhoneAuthModule(config.phone, db, sessionManager) : null;
|
|
2533
|
+
const captchaModule = config.captcha ? createCaptchaModule(config.captcha) : null;
|
|
2534
|
+
const webhookModule = config.webhooks && config.webhooks.length > 0 ? createWebhookModule(config.webhooks) : null;
|
|
2535
|
+
const pluginRegistry = await initializePlugins(config.plugins ?? [], db, config);
|
|
2536
|
+
const endpointCtx = {
|
|
2537
|
+
db,
|
|
2538
|
+
async getUser(request) {
|
|
2539
|
+
if (!authAdapter) return null;
|
|
2540
|
+
return authAdapter.resolveUser(request);
|
|
2541
|
+
},
|
|
2542
|
+
async getSession(token) {
|
|
2543
|
+
if (!sessionManager) return null;
|
|
2544
|
+
return sessionManager.validate(token);
|
|
2545
|
+
}
|
|
2546
|
+
};
|
|
2547
|
+
const pluginRouter = createPluginRouter(pluginRegistry.endpoints);
|
|
476
2548
|
async function authorize(agentId, request, context) {
|
|
2549
|
+
if (hooks.beforeAuthorize) {
|
|
2550
|
+
const verdict = await hooks.beforeAuthorize({
|
|
2551
|
+
agentId,
|
|
2552
|
+
action: request.action,
|
|
2553
|
+
resource: request.resource,
|
|
2554
|
+
arguments: request.arguments
|
|
2555
|
+
});
|
|
2556
|
+
if (verdict && !verdict.allow) {
|
|
2557
|
+
const reason = verdict.reason ?? "Blocked by beforeAuthorize hook";
|
|
2558
|
+
void hooks.onViolation?.({
|
|
2559
|
+
type: classifyViolation2(reason),
|
|
2560
|
+
agentId,
|
|
2561
|
+
action: request.action,
|
|
2562
|
+
resource: request.resource,
|
|
2563
|
+
reason
|
|
2564
|
+
});
|
|
2565
|
+
return { allowed: false, reason, auditId: "" };
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
477
2568
|
const agent = await agentModule.get(agentId);
|
|
478
2569
|
if (!agent) {
|
|
479
2570
|
return {
|
|
@@ -491,13 +2582,42 @@ async function createKavach(config) {
|
|
|
491
2582
|
}
|
|
492
2583
|
const enrichedRequest = context ? { ...request, context } : request;
|
|
493
2584
|
const ownResult = await permissionEngine.authorize(agent, enrichedRequest);
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
2585
|
+
let finalResult;
|
|
2586
|
+
if (ownResult.allowed) {
|
|
2587
|
+
finalResult = ownResult;
|
|
2588
|
+
} else {
|
|
2589
|
+
const delegatedPerms = await delegationModule.getEffectivePermissions(agentId);
|
|
2590
|
+
if (delegatedPerms.length === 0) {
|
|
2591
|
+
finalResult = ownResult;
|
|
2592
|
+
} else {
|
|
2593
|
+
const agentWithDelegated = { ...agent, permissions: delegatedPerms };
|
|
2594
|
+
const delegatedResult = await permissionEngine.authorize(
|
|
2595
|
+
agentWithDelegated,
|
|
2596
|
+
enrichedRequest
|
|
2597
|
+
);
|
|
2598
|
+
finalResult = delegatedResult.allowed ? delegatedResult : ownResult;
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
void hooks.afterAuthorize?.({
|
|
2602
|
+
agentId,
|
|
2603
|
+
action: request.action,
|
|
2604
|
+
resource: request.resource,
|
|
2605
|
+
result: {
|
|
2606
|
+
allowed: finalResult.allowed,
|
|
2607
|
+
reason: finalResult.reason,
|
|
2608
|
+
auditId: finalResult.auditId
|
|
2609
|
+
}
|
|
2610
|
+
});
|
|
2611
|
+
if (!finalResult.allowed) {
|
|
2612
|
+
void hooks.onViolation?.({
|
|
2613
|
+
type: classifyViolation2(finalResult.reason),
|
|
2614
|
+
agentId,
|
|
2615
|
+
action: request.action,
|
|
2616
|
+
resource: request.resource,
|
|
2617
|
+
reason: finalResult.reason ?? "Authorization denied"
|
|
2618
|
+
});
|
|
2619
|
+
}
|
|
2620
|
+
return finalResult;
|
|
501
2621
|
}
|
|
502
2622
|
async function authorizeByToken(token, request, context) {
|
|
503
2623
|
const agent = await agentModule.validateToken(token);
|
|
@@ -519,6 +2639,31 @@ async function createKavach(config) {
|
|
|
519
2639
|
}
|
|
520
2640
|
return delegationModule.delegate(input, parentAgent.permissions);
|
|
521
2641
|
}
|
|
2642
|
+
const agentProxy = {
|
|
2643
|
+
async create(...args) {
|
|
2644
|
+
const [input] = args;
|
|
2645
|
+
if (hooks.beforeAgentCreate) {
|
|
2646
|
+
const verdict = await hooks.beforeAgentCreate(input);
|
|
2647
|
+
if (verdict && !verdict.allow) {
|
|
2648
|
+
throw new Error(verdict.reason ?? "Agent creation blocked by beforeAgentCreate hook");
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
const agent = await agentModule.create(input);
|
|
2652
|
+
void hooks.afterAgentCreate?.(agent);
|
|
2653
|
+
return agent;
|
|
2654
|
+
},
|
|
2655
|
+
async revoke(agentId) {
|
|
2656
|
+
await agentModule.revoke(agentId);
|
|
2657
|
+
void hooks.onAgentRevoke?.(agentId);
|
|
2658
|
+
},
|
|
2659
|
+
async rotate(...args) {
|
|
2660
|
+
return agentModule.rotate(...args);
|
|
2661
|
+
},
|
|
2662
|
+
get: agentModule.get,
|
|
2663
|
+
list: agentModule.list,
|
|
2664
|
+
update: agentModule.update,
|
|
2665
|
+
validateToken: agentModule.validateToken
|
|
2666
|
+
};
|
|
522
2667
|
const mcpRegistry = {
|
|
523
2668
|
/**
|
|
524
2669
|
* Register a new MCP tool server.
|
|
@@ -581,15 +2726,7 @@ async function createKavach(config) {
|
|
|
581
2726
|
}
|
|
582
2727
|
};
|
|
583
2728
|
return {
|
|
584
|
-
agent:
|
|
585
|
-
create: agentModule.create,
|
|
586
|
-
get: agentModule.get,
|
|
587
|
-
list: agentModule.list,
|
|
588
|
-
update: agentModule.update,
|
|
589
|
-
revoke: agentModule.revoke,
|
|
590
|
-
rotate: agentModule.rotate,
|
|
591
|
-
validateToken: agentModule.validateToken
|
|
592
|
-
},
|
|
2729
|
+
agent: agentProxy,
|
|
593
2730
|
authorize,
|
|
594
2731
|
authorizeByToken,
|
|
595
2732
|
delegate,
|
|
@@ -610,6 +2747,17 @@ async function createKavach(config) {
|
|
|
610
2747
|
* database table — no separate in-memory store needed.
|
|
611
2748
|
*/
|
|
612
2749
|
mcp: mcpRegistry,
|
|
2750
|
+
/**
|
|
2751
|
+
* Least-privilege analyzer.
|
|
2752
|
+
*
|
|
2753
|
+
* Compare agent permissions against actual audit log usage to surface
|
|
2754
|
+
* wildcards, unused grants, and over-permissioned identities.
|
|
2755
|
+
*/
|
|
2756
|
+
analyzer: {
|
|
2757
|
+
analyzeAgent: privilegeAnalyzer.analyzeAgent,
|
|
2758
|
+
analyzeAll: privilegeAnalyzer.analyzeAll,
|
|
2759
|
+
getSummary: privilegeAnalyzer.getSummary
|
|
2760
|
+
},
|
|
613
2761
|
/**
|
|
614
2762
|
* Human auth integration.
|
|
615
2763
|
*
|
|
@@ -645,7 +2793,249 @@ async function createKavach(config) {
|
|
|
645
2793
|
return authAdapter.resolveUser(request);
|
|
646
2794
|
},
|
|
647
2795
|
/** Direct database access for advanced usage */
|
|
648
|
-
db
|
|
2796
|
+
db,
|
|
2797
|
+
/**
|
|
2798
|
+
* Multi-tenant isolation.
|
|
2799
|
+
*
|
|
2800
|
+
* Create and manage tenants (organizations) that share a single
|
|
2801
|
+
* KavachOS instance with full data isolation. Agents can be scoped
|
|
2802
|
+
* to a tenant via `tenantId`.
|
|
2803
|
+
*/
|
|
2804
|
+
tenant: tenantModule,
|
|
2805
|
+
/**
|
|
2806
|
+
* Agent execution budget policies.
|
|
2807
|
+
*
|
|
2808
|
+
* Set spending caps (token cost, call counts) per agent, user, or
|
|
2809
|
+
* tenant. Exceeded policies trigger a configurable action: warn,
|
|
2810
|
+
* throttle, block, or revoke.
|
|
2811
|
+
*/
|
|
2812
|
+
policies: policyModule,
|
|
2813
|
+
/**
|
|
2814
|
+
* CIBA-style async human approval flows.
|
|
2815
|
+
*
|
|
2816
|
+
* Create pending approval requests, notify humans via webhook or
|
|
2817
|
+
* custom handler, and resolve them with approve / deny.
|
|
2818
|
+
*/
|
|
2819
|
+
approval: approvalModule,
|
|
2820
|
+
/**
|
|
2821
|
+
* Graduated autonomy trust scoring.
|
|
2822
|
+
*
|
|
2823
|
+
* Compute and persist 0-100 trust scores derived from audit history,
|
|
2824
|
+
* mapped to five levels: untrusted, limited, standard, trusted, elevated.
|
|
2825
|
+
*/
|
|
2826
|
+
trust: trustModule,
|
|
2827
|
+
/**
|
|
2828
|
+
* W3C Decentralized Identifiers (DID) for agents.
|
|
2829
|
+
*
|
|
2830
|
+
* Generate did:key or did:web identities, sign payloads, and verify
|
|
2831
|
+
* signatures. Private keys are never stored — they are returned to
|
|
2832
|
+
* the caller on generation and must be stored securely.
|
|
2833
|
+
*
|
|
2834
|
+
* @example
|
|
2835
|
+
* ```typescript
|
|
2836
|
+
* const { agentDid, privateKeyJwk } = await kavach.did.generateKey(agentId);
|
|
2837
|
+
* const signed = await kavach.did.sign(agentId, { action: 'read' }, privateKeyJwk);
|
|
2838
|
+
* const result = await kavach.did.verify(signed.jws, agentDid.did);
|
|
2839
|
+
* ```
|
|
2840
|
+
*/
|
|
2841
|
+
did: didModule,
|
|
2842
|
+
/**
|
|
2843
|
+
* Magic link (passwordless email) authentication.
|
|
2844
|
+
*
|
|
2845
|
+
* Null when `magicLink` config was not provided or `auth.session` is not
|
|
2846
|
+
* configured (sessions are required to issue tokens on verification).
|
|
2847
|
+
*
|
|
2848
|
+
* @example
|
|
2849
|
+
* ```typescript
|
|
2850
|
+
* // In your route handler
|
|
2851
|
+
* const response = await kavach.magicLink?.handleRequest(request);
|
|
2852
|
+
* if (response) return response;
|
|
2853
|
+
* ```
|
|
2854
|
+
*/
|
|
2855
|
+
magicLink: magicLinkModule,
|
|
2856
|
+
/**
|
|
2857
|
+
* Email OTP (one-time password) authentication.
|
|
2858
|
+
*
|
|
2859
|
+
* Null when `emailOtp` config was not provided or `auth.session` is not
|
|
2860
|
+
* configured.
|
|
2861
|
+
*
|
|
2862
|
+
* @example
|
|
2863
|
+
* ```typescript
|
|
2864
|
+
* const response = await kavach.emailOtp?.handleRequest(request);
|
|
2865
|
+
* if (response) return response;
|
|
2866
|
+
* ```
|
|
2867
|
+
*/
|
|
2868
|
+
emailOtp: emailOtpModule,
|
|
2869
|
+
/**
|
|
2870
|
+
* TOTP two-factor authentication.
|
|
2871
|
+
*
|
|
2872
|
+
* Null when `totp` config was not provided.
|
|
2873
|
+
*
|
|
2874
|
+
* @example
|
|
2875
|
+
* ```typescript
|
|
2876
|
+
* // On setup (show QR code to user)
|
|
2877
|
+
* const { secret, uri, backupCodes } = await kavach.totp.setup(userId);
|
|
2878
|
+
*
|
|
2879
|
+
* // After user scans QR and enters code
|
|
2880
|
+
* const { enabled } = await kavach.totp.enable(userId, totpCode);
|
|
2881
|
+
*
|
|
2882
|
+
* // On login (after password check)
|
|
2883
|
+
* const { valid } = await kavach.totp.verify(userId, totpCode);
|
|
2884
|
+
* ```
|
|
2885
|
+
*/
|
|
2886
|
+
totp: totpModule,
|
|
2887
|
+
/**
|
|
2888
|
+
* Passkey / WebAuthn authentication.
|
|
2889
|
+
*
|
|
2890
|
+
* Null when `passkey` config was not provided.
|
|
2891
|
+
*
|
|
2892
|
+
* @example
|
|
2893
|
+
* ```typescript
|
|
2894
|
+
* // Registration — step 1: get options, send to browser
|
|
2895
|
+
* const options = await kavach.passkey.getRegistrationOptions(userId, userName);
|
|
2896
|
+
*
|
|
2897
|
+
* // Registration — step 2: verify browser response
|
|
2898
|
+
* const { credential } = await kavach.passkey.verifyRegistration(userId, response);
|
|
2899
|
+
*
|
|
2900
|
+
* // Authentication — step 1: get options
|
|
2901
|
+
* const options = await kavach.passkey.getAuthenticationOptions(userId);
|
|
2902
|
+
*
|
|
2903
|
+
* // Authentication — step 2: verify browser response
|
|
2904
|
+
* const result = await kavach.passkey.verifyAuthentication(response);
|
|
2905
|
+
* if (result) console.log('Authenticated user:', result.userId);
|
|
2906
|
+
* ```
|
|
2907
|
+
*/
|
|
2908
|
+
passkey: passkeyModule,
|
|
2909
|
+
/**
|
|
2910
|
+
* Organizations + RBAC.
|
|
2911
|
+
*
|
|
2912
|
+
* Null when `org` config was not provided.
|
|
2913
|
+
*
|
|
2914
|
+
* @example
|
|
2915
|
+
* ```typescript
|
|
2916
|
+
* const org = await kavach.org?.create({ name: 'Acme', slug: 'acme', ownerId: userId });
|
|
2917
|
+
* const allowed = await kavach.org?.hasPermission(org.id, userId, 'agents:create');
|
|
2918
|
+
* ```
|
|
2919
|
+
*/
|
|
2920
|
+
org: orgModule,
|
|
2921
|
+
/**
|
|
2922
|
+
* SSO (SAML 2.0 + OIDC) enterprise authentication.
|
|
2923
|
+
*
|
|
2924
|
+
* Null when `sso` config was not provided.
|
|
2925
|
+
*
|
|
2926
|
+
* @example
|
|
2927
|
+
* ```typescript
|
|
2928
|
+
* const conn = await kavach.sso?.createConnection({ orgId, providerId: 'okta', type: 'saml', domain: 'acme.com' });
|
|
2929
|
+
* const url = await kavach.sso?.getSamlAuthUrl(conn.id);
|
|
2930
|
+
* ```
|
|
2931
|
+
*/
|
|
2932
|
+
sso: ssoModule,
|
|
2933
|
+
/**
|
|
2934
|
+
* Admin module.
|
|
2935
|
+
*
|
|
2936
|
+
* Null when `admin` config was not provided.
|
|
2937
|
+
*
|
|
2938
|
+
* @example
|
|
2939
|
+
* ```typescript
|
|
2940
|
+
* await kavach.admin?.banUser(userId, 'Spam');
|
|
2941
|
+
* const { session } = await kavach.admin?.impersonate(adminId, userId);
|
|
2942
|
+
* ```
|
|
2943
|
+
*/
|
|
2944
|
+
admin: adminModule,
|
|
2945
|
+
/**
|
|
2946
|
+
* API key management.
|
|
2947
|
+
*
|
|
2948
|
+
* Null when `apiKeys` config was not provided.
|
|
2949
|
+
*
|
|
2950
|
+
* @example
|
|
2951
|
+
* ```typescript
|
|
2952
|
+
* const { key, apiKey } = await kavach.apiKeys?.create({ userId, name: 'CI', permissions: ['agents:read'] });
|
|
2953
|
+
* const result = await kavach.apiKeys?.validate(key);
|
|
2954
|
+
* ```
|
|
2955
|
+
*/
|
|
2956
|
+
apiKeys: apiKeyManagerModule,
|
|
2957
|
+
/**
|
|
2958
|
+
* Username + password authentication.
|
|
2959
|
+
*
|
|
2960
|
+
* Null when `username` config was not provided or `auth.session` is not
|
|
2961
|
+
* configured (sessions are required to issue tokens on sign-in/up).
|
|
2962
|
+
*
|
|
2963
|
+
* @example
|
|
2964
|
+
* ```typescript
|
|
2965
|
+
* const response = await kavach.username?.handleRequest(request);
|
|
2966
|
+
* if (response) return response;
|
|
2967
|
+
* ```
|
|
2968
|
+
*/
|
|
2969
|
+
username: usernameModule,
|
|
2970
|
+
/**
|
|
2971
|
+
* Phone number (SMS OTP) authentication.
|
|
2972
|
+
*
|
|
2973
|
+
* Null when `phone` config was not provided or `auth.session` is not
|
|
2974
|
+
* configured.
|
|
2975
|
+
*
|
|
2976
|
+
* @example
|
|
2977
|
+
* ```typescript
|
|
2978
|
+
* const response = await kavach.phone?.handleRequest(request);
|
|
2979
|
+
* if (response) return response;
|
|
2980
|
+
* ```
|
|
2981
|
+
*/
|
|
2982
|
+
phone: phoneModule,
|
|
2983
|
+
/**
|
|
2984
|
+
* Captcha integration (reCAPTCHA, hCaptcha, Cloudflare Turnstile).
|
|
2985
|
+
*
|
|
2986
|
+
* Null when `captcha` config was not provided.
|
|
2987
|
+
*
|
|
2988
|
+
* @example
|
|
2989
|
+
* ```typescript
|
|
2990
|
+
* const result = await kavach.captcha?.verify(token, ip);
|
|
2991
|
+
* if (!result?.success) return new Response('Captcha failed', { status: 403 });
|
|
2992
|
+
* ```
|
|
2993
|
+
*/
|
|
2994
|
+
captcha: captchaModule,
|
|
2995
|
+
/**
|
|
2996
|
+
* Webhook system.
|
|
2997
|
+
*
|
|
2998
|
+
* Null when `webhooks` config was not provided or the array is empty.
|
|
2999
|
+
*
|
|
3000
|
+
* @example
|
|
3001
|
+
* ```typescript
|
|
3002
|
+
* kavach.webhooks?.emit('user.created', { userId: user.id });
|
|
3003
|
+
* ```
|
|
3004
|
+
*/
|
|
3005
|
+
webhooks: webhookModule,
|
|
3006
|
+
/**
|
|
3007
|
+
* Plugin system.
|
|
3008
|
+
*
|
|
3009
|
+
* Route incoming HTTP requests through plugin-registered endpoints,
|
|
3010
|
+
* retrieve all endpoints for adapter mounting, or access plugin-provided
|
|
3011
|
+
* context values.
|
|
3012
|
+
*
|
|
3013
|
+
* @example
|
|
3014
|
+
* ```typescript
|
|
3015
|
+
* // In a framework adapter
|
|
3016
|
+
* app.all('/kavach/*', async (req) => {
|
|
3017
|
+
* const response = await kavach.plugins.handleRequest(req);
|
|
3018
|
+
* if (response) return response;
|
|
3019
|
+
* return new Response('Not Found', { status: 404 });
|
|
3020
|
+
* });
|
|
3021
|
+
* ```
|
|
3022
|
+
*/
|
|
3023
|
+
plugins: {
|
|
3024
|
+
/** Route a request through plugin endpoints. Returns null if no plugin handles it. */
|
|
3025
|
+
handleRequest(request, basePath = "") {
|
|
3026
|
+
return pluginRouter.handle(request, basePath, endpointCtx);
|
|
3027
|
+
},
|
|
3028
|
+
/** Get all endpoints registered by plugins (for framework adapter mounting). */
|
|
3029
|
+
getEndpoints() {
|
|
3030
|
+
return pluginRouter.getEndpoints();
|
|
3031
|
+
},
|
|
3032
|
+
/** Get the merged plugin context (values returned from plugin init). */
|
|
3033
|
+
getContext() {
|
|
3034
|
+
return { ...pluginRegistry.pluginContext };
|
|
3035
|
+
},
|
|
3036
|
+
/** Access the raw plugin registry (hooks, migrations, etc.). */
|
|
3037
|
+
registry: pluginRegistry
|
|
3038
|
+
}
|
|
649
3039
|
};
|
|
650
3040
|
}
|
|
651
3041
|
|
|
@@ -1053,6 +3443,491 @@ function generateOpenAPISpec(options) {
|
|
|
1053
3443
|
};
|
|
1054
3444
|
}
|
|
1055
3445
|
|
|
1056
|
-
|
|
3446
|
+
// src/session/cookie.ts
|
|
3447
|
+
var IS_PRODUCTION = typeof process !== "undefined" && process.env.NODE_ENV === "production";
|
|
3448
|
+
var DEFAULT_OPTIONS = {
|
|
3449
|
+
httpOnly: true,
|
|
3450
|
+
secure: IS_PRODUCTION,
|
|
3451
|
+
sameSite: "lax",
|
|
3452
|
+
path: "/"
|
|
3453
|
+
};
|
|
3454
|
+
function serializeCookie(name, value, options) {
|
|
3455
|
+
validateCookieName(name);
|
|
3456
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
3457
|
+
const parts = [`${name}=${encodeURIComponent(value)}`];
|
|
3458
|
+
if (opts.httpOnly) parts.push("HttpOnly");
|
|
3459
|
+
if (opts.secure) parts.push("Secure");
|
|
3460
|
+
const sameSite = opts.sameSite ?? "lax";
|
|
3461
|
+
parts.push(`SameSite=${capitalize(sameSite)}`);
|
|
3462
|
+
const path = opts.path ?? "/";
|
|
3463
|
+
parts.push(`Path=${path}`);
|
|
3464
|
+
if (options?.domain) parts.push(`Domain=${options.domain}`);
|
|
3465
|
+
if (options?.maxAge !== void 0) {
|
|
3466
|
+
parts.push(`Max-Age=${options.maxAge}`);
|
|
3467
|
+
const expiryDate = new Date(Date.now() + options.maxAge * 1e3);
|
|
3468
|
+
parts.push(`Expires=${expiryDate.toUTCString()}`);
|
|
3469
|
+
} else if (options?.expires) {
|
|
3470
|
+
parts.push(`Expires=${options.expires.toUTCString()}`);
|
|
3471
|
+
}
|
|
3472
|
+
if (options?.partitioned) parts.push("Partitioned");
|
|
3473
|
+
return parts.join("; ");
|
|
3474
|
+
}
|
|
3475
|
+
function serializeCookieDeletion(name, options) {
|
|
3476
|
+
return serializeCookie(name, "", {
|
|
3477
|
+
...options,
|
|
3478
|
+
maxAge: 0,
|
|
3479
|
+
expires: /* @__PURE__ */ new Date(0)
|
|
3480
|
+
});
|
|
3481
|
+
}
|
|
3482
|
+
function parseCookies(header) {
|
|
3483
|
+
const result = {};
|
|
3484
|
+
if (!header || !header.trim()) return result;
|
|
3485
|
+
for (const pair of header.split(";")) {
|
|
3486
|
+
const eqIndex = pair.indexOf("=");
|
|
3487
|
+
if (eqIndex === -1) continue;
|
|
3488
|
+
const name = pair.slice(0, eqIndex).trim();
|
|
3489
|
+
const raw = pair.slice(eqIndex + 1).trim();
|
|
3490
|
+
if (!name) continue;
|
|
3491
|
+
try {
|
|
3492
|
+
result[name] = decodeURIComponent(raw);
|
|
3493
|
+
} catch {
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
return result;
|
|
3497
|
+
}
|
|
3498
|
+
function getCookie(header, name) {
|
|
3499
|
+
return parseCookies(header)[name];
|
|
3500
|
+
}
|
|
3501
|
+
function parseCookiesFromRequest(request) {
|
|
3502
|
+
return parseCookies(request.headers.get("cookie") ?? "");
|
|
3503
|
+
}
|
|
3504
|
+
function capitalize(s) {
|
|
3505
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
3506
|
+
}
|
|
3507
|
+
var COOKIE_NAME_SEPARATORS = /[\s()<>@,;:\\"/[\]?={}]/;
|
|
3508
|
+
function validateCookieName(name) {
|
|
3509
|
+
if (!name) {
|
|
3510
|
+
throw new Error(`Invalid cookie name: "${name}"`);
|
|
3511
|
+
}
|
|
3512
|
+
for (let i = 0; i < name.length; i++) {
|
|
3513
|
+
const code2 = name.charCodeAt(i);
|
|
3514
|
+
if (code2 <= 31 || code2 === 127) {
|
|
3515
|
+
throw new Error(`Invalid cookie name: "${name}"`);
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
if (COOKIE_NAME_SEPARATORS.test(name)) {
|
|
3519
|
+
throw new Error(`Invalid cookie name: "${name}"`);
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
// src/session/csrf.ts
|
|
3524
|
+
var TOKEN_BYTE_LENGTH = 32;
|
|
3525
|
+
function generateCsrfToken() {
|
|
3526
|
+
const bytes = new Uint8Array(TOKEN_BYTE_LENGTH);
|
|
3527
|
+
crypto.getRandomValues(bytes);
|
|
3528
|
+
return uint8ArrayToBase64Url(bytes);
|
|
3529
|
+
}
|
|
3530
|
+
function validateCsrfToken(requestToken, cookieToken) {
|
|
3531
|
+
if (!requestToken || !cookieToken) {
|
|
3532
|
+
return { valid: false, reason: "Missing CSRF token" };
|
|
3533
|
+
}
|
|
3534
|
+
if (!timingSafeEqual(requestToken, cookieToken)) {
|
|
3535
|
+
return { valid: false, reason: "CSRF token mismatch" };
|
|
3536
|
+
}
|
|
3537
|
+
return { valid: true };
|
|
3538
|
+
}
|
|
3539
|
+
function validateOrigin(request, trustedOrigins, allowMissingOrigin = false) {
|
|
3540
|
+
const normalised = trustedOrigins.map(normaliseOrigin);
|
|
3541
|
+
const originHeader = request.headers.get("origin");
|
|
3542
|
+
if (originHeader) {
|
|
3543
|
+
if (originHeader === "null") {
|
|
3544
|
+
return { valid: false, reason: "Opaque origin rejected" };
|
|
3545
|
+
}
|
|
3546
|
+
const requestOrigin = normaliseOrigin(originHeader);
|
|
3547
|
+
if (normalised.includes(requestOrigin)) {
|
|
3548
|
+
return { valid: true };
|
|
3549
|
+
}
|
|
3550
|
+
return {
|
|
3551
|
+
valid: false,
|
|
3552
|
+
reason: `Origin "${originHeader}" is not in the trusted list`
|
|
3553
|
+
};
|
|
3554
|
+
}
|
|
3555
|
+
const refererHeader = request.headers.get("referer");
|
|
3556
|
+
if (refererHeader) {
|
|
3557
|
+
try {
|
|
3558
|
+
const refererOrigin = normaliseOrigin(new URL(refererHeader).origin);
|
|
3559
|
+
if (normalised.includes(refererOrigin)) {
|
|
3560
|
+
return { valid: true };
|
|
3561
|
+
}
|
|
3562
|
+
return {
|
|
3563
|
+
valid: false,
|
|
3564
|
+
reason: `Referer origin "${refererOrigin}" is not in the trusted list`
|
|
3565
|
+
};
|
|
3566
|
+
} catch {
|
|
3567
|
+
return { valid: false, reason: "Malformed Referer header" };
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
if (allowMissingOrigin) {
|
|
3571
|
+
return { valid: true };
|
|
3572
|
+
}
|
|
3573
|
+
return { valid: false, reason: "No Origin or Referer header present" };
|
|
3574
|
+
}
|
|
3575
|
+
function normaliseOrigin(origin) {
|
|
3576
|
+
return origin.replace(/\/$/, "").toLowerCase();
|
|
3577
|
+
}
|
|
3578
|
+
function timingSafeEqual(a, b) {
|
|
3579
|
+
const aBytes = new TextEncoder().encode(a);
|
|
3580
|
+
const bBytes = new TextEncoder().encode(b);
|
|
3581
|
+
if (aBytes.length !== bBytes.length) {
|
|
3582
|
+
let _diff = 0;
|
|
3583
|
+
const max = Math.max(aBytes.length, bBytes.length);
|
|
3584
|
+
for (let i = 0; i < max; i++) {
|
|
3585
|
+
_diff |= (aBytes[i] ?? 0) ^ (bBytes[i] ?? 0);
|
|
3586
|
+
}
|
|
3587
|
+
return false;
|
|
3588
|
+
}
|
|
3589
|
+
let diff = 0;
|
|
3590
|
+
for (let i = 0; i < aBytes.length; i++) {
|
|
3591
|
+
diff |= (aBytes[i] ?? 0) ^ (bBytes[i] ?? 0);
|
|
3592
|
+
}
|
|
3593
|
+
return diff === 0;
|
|
3594
|
+
}
|
|
3595
|
+
function uint8ArrayToBase64Url(bytes) {
|
|
3596
|
+
if (typeof Buffer !== "undefined") {
|
|
3597
|
+
return Buffer.from(bytes).toString("base64url");
|
|
3598
|
+
}
|
|
3599
|
+
let binary = "";
|
|
3600
|
+
for (const byte of bytes) {
|
|
3601
|
+
binary += String.fromCharCode(byte);
|
|
3602
|
+
}
|
|
3603
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
3604
|
+
}
|
|
3605
|
+
var DEFAULT_SESSION_NAME = "kavach_session";
|
|
3606
|
+
var DEFAULT_MAX_AGE_SECONDS = 60 * 60 * 24 * 7;
|
|
3607
|
+
function createCookieSessionManager(config, db) {
|
|
3608
|
+
const sessionName = config.sessionName ?? DEFAULT_SESSION_NAME;
|
|
3609
|
+
const maxAgeSecs = config.maxAge ?? DEFAULT_MAX_AGE_SECONDS;
|
|
3610
|
+
const autoRefresh = config.autoRefresh ?? true;
|
|
3611
|
+
const raw = createSessionManager(config, db);
|
|
3612
|
+
const baseCookieOpts = {
|
|
3613
|
+
httpOnly: true,
|
|
3614
|
+
sameSite: "lax",
|
|
3615
|
+
path: "/",
|
|
3616
|
+
...config.cookieOptions,
|
|
3617
|
+
maxAge: maxAgeSecs
|
|
3618
|
+
};
|
|
3619
|
+
function buildSetCookie(token) {
|
|
3620
|
+
return serializeCookie(sessionName, token, baseCookieOpts);
|
|
3621
|
+
}
|
|
3622
|
+
function buildDeleteCookie() {
|
|
3623
|
+
const { maxAge: _omit, ...rest } = baseCookieOpts;
|
|
3624
|
+
return serializeCookieDeletion(sessionName, rest);
|
|
3625
|
+
}
|
|
3626
|
+
async function createSession(userId, metadata) {
|
|
3627
|
+
const { session, token } = await raw.create(userId, metadata);
|
|
3628
|
+
return { session, setCookieHeader: buildSetCookie(token) };
|
|
3629
|
+
}
|
|
3630
|
+
async function validateSession(cookieHeader) {
|
|
3631
|
+
const token = getCookie(cookieHeader, sessionName);
|
|
3632
|
+
if (!token) {
|
|
3633
|
+
return { session: null, refreshCookieHeader: null };
|
|
3634
|
+
}
|
|
3635
|
+
const session = await raw.validate(token);
|
|
3636
|
+
if (!session) {
|
|
3637
|
+
return { session: null, refreshCookieHeader: null };
|
|
3638
|
+
}
|
|
3639
|
+
if (autoRefresh) {
|
|
3640
|
+
const refreshed = await refreshSession(session.id);
|
|
3641
|
+
if (refreshed) {
|
|
3642
|
+
return { session: refreshed.session, refreshCookieHeader: refreshed.setCookieHeader };
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
return { session, refreshCookieHeader: null };
|
|
3646
|
+
}
|
|
3647
|
+
async function refreshSession(sessionId) {
|
|
3648
|
+
const rows = await db.select().from(sessions).where(and(eq(sessions.id, sessionId)));
|
|
3649
|
+
const row = rows[0];
|
|
3650
|
+
if (!row) return null;
|
|
3651
|
+
if (row.expiresAt <= /* @__PURE__ */ new Date()) return null;
|
|
3652
|
+
await db.delete(sessions).where(eq(sessions.id, sessionId));
|
|
3653
|
+
const { session: newSession, token: newToken } = await raw.create(
|
|
3654
|
+
row.userId,
|
|
3655
|
+
row.metadata ?? void 0
|
|
3656
|
+
);
|
|
3657
|
+
return { session: newSession, setCookieHeader: buildSetCookie(newToken) };
|
|
3658
|
+
}
|
|
3659
|
+
async function revokeSession(sessionId) {
|
|
3660
|
+
await raw.revoke(sessionId);
|
|
3661
|
+
return { deleteCookieHeader: buildDeleteCookie() };
|
|
3662
|
+
}
|
|
3663
|
+
async function revokeAllSessions(userId) {
|
|
3664
|
+
await raw.revokeAll(userId);
|
|
3665
|
+
return { deleteCookieHeader: buildDeleteCookie() };
|
|
3666
|
+
}
|
|
3667
|
+
async function listSessions(userId) {
|
|
3668
|
+
return raw.list(userId);
|
|
3669
|
+
}
|
|
3670
|
+
function buildLogoutCookie() {
|
|
3671
|
+
return buildDeleteCookie();
|
|
3672
|
+
}
|
|
3673
|
+
return {
|
|
3674
|
+
createSession,
|
|
3675
|
+
validateSession,
|
|
3676
|
+
refreshSession,
|
|
3677
|
+
revokeSession,
|
|
3678
|
+
revokeAllSessions,
|
|
3679
|
+
listSessions,
|
|
3680
|
+
buildLogoutCookie,
|
|
3681
|
+
raw
|
|
3682
|
+
};
|
|
3683
|
+
}
|
|
3684
|
+
var MultiSessionLimitError = class extends Error {
|
|
3685
|
+
code = "SESSION_LIMIT_REACHED";
|
|
3686
|
+
constructor(userId, max) {
|
|
3687
|
+
super(`User ${userId} has reached the maximum of ${max} concurrent sessions`);
|
|
3688
|
+
}
|
|
3689
|
+
};
|
|
3690
|
+
function parseUserAgent(ua) {
|
|
3691
|
+
if (!ua) return void 0;
|
|
3692
|
+
let os;
|
|
3693
|
+
if (/iphone|ipad|ipod/i.test(ua)) {
|
|
3694
|
+
os = "iOS";
|
|
3695
|
+
} else if (/android/i.test(ua)) {
|
|
3696
|
+
os = "Android";
|
|
3697
|
+
} else if (/macintosh|mac os x/i.test(ua)) {
|
|
3698
|
+
os = "macOS";
|
|
3699
|
+
} else if (/windows/i.test(ua)) {
|
|
3700
|
+
os = "Windows";
|
|
3701
|
+
} else if (/linux/i.test(ua)) {
|
|
3702
|
+
os = "Linux";
|
|
3703
|
+
} else {
|
|
3704
|
+
os = "Unknown OS";
|
|
3705
|
+
}
|
|
3706
|
+
let browser;
|
|
3707
|
+
if (/edg\//i.test(ua)) {
|
|
3708
|
+
browser = "Edge";
|
|
3709
|
+
} else if (/opr\//i.test(ua) || /opera/i.test(ua)) {
|
|
3710
|
+
browser = "Opera";
|
|
3711
|
+
} else if (/firefox\//i.test(ua)) {
|
|
3712
|
+
browser = "Firefox";
|
|
3713
|
+
} else if (/chrome\//i.test(ua) && !/chromium/i.test(ua)) {
|
|
3714
|
+
browser = "Chrome";
|
|
3715
|
+
} else if (/safari\//i.test(ua) && !/chrome/i.test(ua)) {
|
|
3716
|
+
browser = "Safari";
|
|
3717
|
+
} else if (/curl\//i.test(ua)) {
|
|
3718
|
+
browser = "curl";
|
|
3719
|
+
} else if (/python-requests/i.test(ua)) {
|
|
3720
|
+
browser = "Python";
|
|
3721
|
+
} else {
|
|
3722
|
+
browser = "Unknown";
|
|
3723
|
+
}
|
|
3724
|
+
return `${browser} on ${os}`;
|
|
3725
|
+
}
|
|
3726
|
+
function rowToSessionInfo(row) {
|
|
3727
|
+
const metadata = row.metadata ?? void 0;
|
|
3728
|
+
const device = metadata && typeof metadata.device === "string" ? metadata.device : void 0;
|
|
3729
|
+
const ip = metadata && typeof metadata.ip === "string" ? metadata.ip : void 0;
|
|
3730
|
+
let cleanMetadata;
|
|
3731
|
+
if (metadata) {
|
|
3732
|
+
const { device: _d, ip: _i, ...rest } = metadata;
|
|
3733
|
+
cleanMetadata = Object.keys(rest).length > 0 ? rest : void 0;
|
|
3734
|
+
}
|
|
3735
|
+
return {
|
|
3736
|
+
id: row.id,
|
|
3737
|
+
createdAt: row.createdAt,
|
|
3738
|
+
expiresAt: row.expiresAt,
|
|
3739
|
+
...cleanMetadata !== void 0 && { metadata: cleanMetadata },
|
|
3740
|
+
...device !== void 0 && { device },
|
|
3741
|
+
...ip !== void 0 && { ip }
|
|
3742
|
+
};
|
|
3743
|
+
}
|
|
3744
|
+
function createMultiSessionModule(config, db, sessionManager) {
|
|
3745
|
+
const maxSessions = config.maxSessions ?? 10;
|
|
3746
|
+
const overflowStrategy = config.overflowStrategy ?? "evict-oldest";
|
|
3747
|
+
async function listSessions(userId) {
|
|
3748
|
+
const now = /* @__PURE__ */ new Date();
|
|
3749
|
+
const rows = await db.select().from(sessions).where(eq(sessions.userId, userId));
|
|
3750
|
+
return rows.filter((r) => r.expiresAt > now).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()).map(rowToSessionInfo);
|
|
3751
|
+
}
|
|
3752
|
+
async function revokeSession(sessionId) {
|
|
3753
|
+
await sessionManager.revoke(sessionId);
|
|
3754
|
+
}
|
|
3755
|
+
async function revokeOtherSessions(userId, currentSessionId) {
|
|
3756
|
+
const now = /* @__PURE__ */ new Date();
|
|
3757
|
+
const activeRows = await db.select({ id: sessions.id, expiresAt: sessions.expiresAt }).from(sessions).where(and(eq(sessions.userId, userId), ne(sessions.id, currentSessionId)));
|
|
3758
|
+
const activeIds = activeRows.filter((r) => r.expiresAt > now).map((r) => r.id);
|
|
3759
|
+
for (const id of activeIds) {
|
|
3760
|
+
await sessionManager.revoke(id);
|
|
3761
|
+
}
|
|
3762
|
+
return activeIds.length;
|
|
3763
|
+
}
|
|
3764
|
+
async function getSessionCount(userId) {
|
|
3765
|
+
const now = /* @__PURE__ */ new Date();
|
|
3766
|
+
const rows = await db.select({ id: sessions.id, expiresAt: sessions.expiresAt }).from(sessions).where(eq(sessions.userId, userId));
|
|
3767
|
+
return rows.filter((r) => r.expiresAt > now).length;
|
|
3768
|
+
}
|
|
3769
|
+
async function enforceSessionLimit(userId) {
|
|
3770
|
+
const now = /* @__PURE__ */ new Date();
|
|
3771
|
+
const rows = await db.select({ id: sessions.id, expiresAt: sessions.expiresAt, createdAt: sessions.createdAt }).from(sessions).where(eq(sessions.userId, userId));
|
|
3772
|
+
const activeSessions = rows.filter((r) => r.expiresAt > now).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
3773
|
+
if (activeSessions.length < maxSessions) return;
|
|
3774
|
+
if (overflowStrategy === "reject") {
|
|
3775
|
+
throw new MultiSessionLimitError(userId, maxSessions);
|
|
3776
|
+
}
|
|
3777
|
+
const toEvict = activeSessions.slice(0, activeSessions.length - maxSessions + 1);
|
|
3778
|
+
for (const s of toEvict) {
|
|
3779
|
+
await sessionManager.revoke(s.id);
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
return { listSessions, revokeSession, revokeOtherSessions, getSessionCount, enforceSessionLimit };
|
|
3783
|
+
}
|
|
3784
|
+
function buildSessionMetadata(request, extra) {
|
|
3785
|
+
const ua = request.headers.get("user-agent");
|
|
3786
|
+
const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("x-real-ip") ?? void 0;
|
|
3787
|
+
const device = parseUserAgent(ua);
|
|
3788
|
+
return {
|
|
3789
|
+
...device !== void 0 && { device },
|
|
3790
|
+
...ip !== void 0 && { ip },
|
|
3791
|
+
...extra
|
|
3792
|
+
};
|
|
3793
|
+
}
|
|
3794
|
+
|
|
3795
|
+
// src/webhooks/webhook.ts
|
|
3796
|
+
function generateId() {
|
|
3797
|
+
const bytes = new Uint8Array(16);
|
|
3798
|
+
crypto.getRandomValues(bytes);
|
|
3799
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
3800
|
+
}
|
|
3801
|
+
async function signPayload2(secret, body) {
|
|
3802
|
+
const encoder = new TextEncoder();
|
|
3803
|
+
const keyData = encoder.encode(secret);
|
|
3804
|
+
const messageData = encoder.encode(body);
|
|
3805
|
+
const key = await crypto.subtle.importKey(
|
|
3806
|
+
"raw",
|
|
3807
|
+
keyData,
|
|
3808
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
3809
|
+
false,
|
|
3810
|
+
["sign"]
|
|
3811
|
+
);
|
|
3812
|
+
const signature = await crypto.subtle.sign("HMAC", key, messageData);
|
|
3813
|
+
const hex = Array.from(new Uint8Array(signature), (b) => b.toString(16).padStart(2, "0")).join(
|
|
3814
|
+
""
|
|
3815
|
+
);
|
|
3816
|
+
return `sha256=${hex}`;
|
|
3817
|
+
}
|
|
3818
|
+
async function deliverWebhook(url, event, payload, deliveryId, timestamp, signature, timeoutMs) {
|
|
3819
|
+
try {
|
|
3820
|
+
const response = await fetch(url, {
|
|
3821
|
+
method: "POST",
|
|
3822
|
+
headers: {
|
|
3823
|
+
"content-type": "application/json",
|
|
3824
|
+
"x-kavach-event": event,
|
|
3825
|
+
"x-kavach-delivery": deliveryId,
|
|
3826
|
+
"x-kavach-timestamp": timestamp,
|
|
3827
|
+
"x-kavach-signature": signature
|
|
3828
|
+
},
|
|
3829
|
+
body: JSON.stringify(payload),
|
|
3830
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
3831
|
+
});
|
|
3832
|
+
return { success: response.ok, statusCode: response.status };
|
|
3833
|
+
} catch (err) {
|
|
3834
|
+
return {
|
|
3835
|
+
success: false,
|
|
3836
|
+
error: err instanceof Error ? err.message : "Unknown error"
|
|
3837
|
+
};
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
async function dispatchWithRetry(url, event, payload, config) {
|
|
3841
|
+
const deliveryId = generateId();
|
|
3842
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3843
|
+
const body = JSON.stringify(payload);
|
|
3844
|
+
const signature = await signPayload2(config.secret, body);
|
|
3845
|
+
const delays = [1e3, 2e3, 4e3];
|
|
3846
|
+
for (let attempt = 0; attempt < config.maxRetries; attempt++) {
|
|
3847
|
+
if (attempt > 0) {
|
|
3848
|
+
await new Promise((resolve) => setTimeout(resolve, delays[attempt - 1] ?? 4e3));
|
|
3849
|
+
}
|
|
3850
|
+
const result = await deliverWebhook(
|
|
3851
|
+
url,
|
|
3852
|
+
event,
|
|
3853
|
+
payload,
|
|
3854
|
+
deliveryId,
|
|
3855
|
+
timestamp,
|
|
3856
|
+
signature,
|
|
3857
|
+
config.timeoutMs
|
|
3858
|
+
);
|
|
3859
|
+
if (result.success) {
|
|
3860
|
+
return;
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
function createWebhookModule2(config) {
|
|
3865
|
+
const resolvedConfig = {
|
|
3866
|
+
secret: config.secret,
|
|
3867
|
+
maxRetries: config.maxRetries ?? 3,
|
|
3868
|
+
timeoutMs: config.timeoutMs ?? 1e4
|
|
3869
|
+
};
|
|
3870
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
3871
|
+
async function subscribe(url, events) {
|
|
3872
|
+
const sub = {
|
|
3873
|
+
id: generateId(),
|
|
3874
|
+
url,
|
|
3875
|
+
events,
|
|
3876
|
+
active: true,
|
|
3877
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
3878
|
+
};
|
|
3879
|
+
subscriptions.set(sub.id, sub);
|
|
3880
|
+
return sub;
|
|
3881
|
+
}
|
|
3882
|
+
async function unsubscribe(subscriptionId) {
|
|
3883
|
+
subscriptions.delete(subscriptionId);
|
|
3884
|
+
}
|
|
3885
|
+
async function list() {
|
|
3886
|
+
return Array.from(subscriptions.values());
|
|
3887
|
+
}
|
|
3888
|
+
function dispatch(event, payload) {
|
|
3889
|
+
const matching = Array.from(subscriptions.values()).filter(
|
|
3890
|
+
(sub) => sub.active && sub.events.includes(event)
|
|
3891
|
+
);
|
|
3892
|
+
for (const sub of matching) {
|
|
3893
|
+
void dispatchWithRetry(sub.url, event, payload, resolvedConfig);
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3896
|
+
async function test(subscriptionId) {
|
|
3897
|
+
const sub = subscriptions.get(subscriptionId);
|
|
3898
|
+
if (!sub) {
|
|
3899
|
+
return { success: false, error: "Subscription not found" };
|
|
3900
|
+
}
|
|
3901
|
+
const deliveryId = generateId();
|
|
3902
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3903
|
+
const pingPayload = { event: "ping", subscriptionId, timestamp };
|
|
3904
|
+
const body = JSON.stringify(pingPayload);
|
|
3905
|
+
const signature = await signPayload2(resolvedConfig.secret, body);
|
|
3906
|
+
return deliverWebhook(
|
|
3907
|
+
sub.url,
|
|
3908
|
+
"auth.login",
|
|
3909
|
+
// placeholder event type for test delivery
|
|
3910
|
+
pingPayload,
|
|
3911
|
+
deliveryId,
|
|
3912
|
+
timestamp,
|
|
3913
|
+
signature,
|
|
3914
|
+
resolvedConfig.timeoutMs
|
|
3915
|
+
);
|
|
3916
|
+
}
|
|
3917
|
+
return { subscribe, unsubscribe, list, dispatch, test };
|
|
3918
|
+
}
|
|
3919
|
+
async function verifyWebhookSignature(secret, rawBody, signature) {
|
|
3920
|
+
const expected = await signPayload2(secret, rawBody);
|
|
3921
|
+
if (expected.length !== signature.length) return false;
|
|
3922
|
+
const a = new TextEncoder().encode(expected);
|
|
3923
|
+
const b = new TextEncoder().encode(signature);
|
|
3924
|
+
let diff = 0;
|
|
3925
|
+
for (let i = 0; i < a.length; i++) {
|
|
3926
|
+
diff |= (a[i] ?? 0) ^ (b[i] ?? 0);
|
|
3927
|
+
}
|
|
3928
|
+
return diff === 0;
|
|
3929
|
+
}
|
|
3930
|
+
|
|
3931
|
+
export { MultiSessionLimitError, buildDidDocument, buildSessionMetadata, classifyViolation, createApprovalModule, createCookieSessionManager, createDatabase, createDatabaseSync, createDelegationModule, createDidModule, createEmailTemplates, createI18n, createKavach, createMultiSessionModule, createPluginRouter, createPolicyModule, createPresentation, createPrivilegeAnalyzer, createTables, createTenantModule, createTrustModule, createWebhookModule2 as createWebhookModule, de, en, es, fr, generateCsrfToken, generateDidKey, generateDidWeb, generateOpenAPISpec, getCookie, getDidWebUrl, initializePlugins, ja, parseCookies, parseCookiesFromRequest, resolveDidKey, resolveDidWeb, serializeCookie, serializeCookieDeletion, signPayload, validateCsrfToken, validateOrigin, verifyPayload, verifyPresentation, verifyWebhookSignature, zh };
|
|
1057
3932
|
//# sourceMappingURL=index.js.map
|
|
1058
3933
|
//# sourceMappingURL=index.js.map
|