@tumbaland/backend-core 1.38.0 → 1.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apiKeys/ApiKey.d.ts +11 -0
- package/dist/apiKeys/ApiKey.d.ts.map +1 -1
- package/dist/apiKeys/ApiKey.js +14 -1
- package/dist/apiKeys/ApiKey.js.map +1 -1
- package/dist/apiKeys/middleware.d.ts +12 -0
- package/dist/apiKeys/middleware.d.ts.map +1 -1
- package/dist/apiKeys/middleware.js +16 -4
- package/dist/apiKeys/middleware.js.map +1 -1
- package/dist/apiKeys/service.d.ts.map +1 -1
- package/dist/apiKeys/service.js +1 -0
- package/dist/apiKeys/service.js.map +1 -1
- package/dist/apiKeys/types.d.ts +2 -0
- package/dist/apiKeys/types.d.ts.map +1 -1
- package/dist/audit/AuditEvent.d.ts +82 -0
- package/dist/audit/AuditEvent.d.ts.map +1 -0
- package/dist/audit/AuditEvent.js +77 -0
- package/dist/audit/AuditEvent.js.map +1 -0
- package/dist/audit/actor.d.ts +41 -0
- package/dist/audit/actor.d.ts.map +1 -0
- package/dist/audit/actor.js +39 -0
- package/dist/audit/actor.js.map +1 -0
- package/dist/audit/context.d.ts +40 -0
- package/dist/audit/context.d.ts.map +1 -0
- package/dist/audit/context.js +60 -0
- package/dist/audit/context.js.map +1 -0
- package/dist/audit/index.d.ts +12 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +22 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/audit/plugin.d.ts +23 -0
- package/dist/audit/plugin.d.ts.map +1 -0
- package/dist/audit/plugin.js +226 -0
- package/dist/audit/plugin.js.map +1 -0
- package/dist/audit/reads.d.ts +47 -0
- package/dist/audit/reads.d.ts.map +1 -0
- package/dist/audit/reads.js +94 -0
- package/dist/audit/reads.js.map +1 -0
- package/dist/audit/service.d.ts +51 -0
- package/dist/audit/service.d.ts.map +1 -0
- package/dist/audit/service.js +66 -0
- package/dist/audit/service.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/oauth/models.d.ts.map +1 -1
- package/dist/oauth/models.js +10 -0
- package/dist/oauth/models.js.map +1 -1
- package/dist/oauth/tokens.d.ts +15 -0
- package/dist/oauth/tokens.d.ts.map +1 -1
- package/dist/oauth/tokens.js +5 -1
- package/dist/oauth/tokens.js.map +1 -1
- package/package.json +1 -1
- package/src/apiKeys/ApiKey.test.ts +25 -1
- package/src/apiKeys/ApiKey.ts +15 -0
- package/src/apiKeys/middleware.test.ts +26 -3
- package/src/apiKeys/middleware.ts +32 -5
- package/src/apiKeys/service.test.ts +2 -0
- package/src/apiKeys/service.ts +1 -0
- package/src/apiKeys/types.ts +2 -0
- package/src/audit/AuditEvent.ts +123 -0
- package/src/audit/actor.test.ts +95 -0
- package/src/audit/actor.ts +68 -0
- package/src/audit/context.test.ts +91 -0
- package/src/audit/context.ts +83 -0
- package/src/audit/index.ts +11 -0
- package/src/audit/plugin.test.ts +258 -0
- package/src/audit/plugin.ts +254 -0
- package/src/audit/reads.test.ts +164 -0
- package/src/audit/reads.ts +88 -0
- package/src/audit/service.test.ts +115 -0
- package/src/audit/service.ts +95 -0
- package/src/index.ts +1 -0
- package/src/middleware/authMiddleware.test.ts +3 -1
- package/src/oauth/models.ts +11 -0
- package/src/oauth/tokens.test.ts +2 -0
- package/src/oauth/tokens.ts +20 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { AuditEvent, type IAuditEvent, type ToolCall } from './AuditEvent';
|
|
2
|
+
import type { ActorKind } from './actor';
|
|
3
|
+
|
|
4
|
+
export interface AuditQuery {
|
|
5
|
+
userId: string;
|
|
6
|
+
/** narrow to one actor — a key's id or an OAuth client id */
|
|
7
|
+
credentialId?: string;
|
|
8
|
+
actorKind?: ActorKind;
|
|
9
|
+
action?: IAuditEvent['action'];
|
|
10
|
+
resource?: string;
|
|
11
|
+
/** narrow to one area — 'relationship-service', 'album-service' */
|
|
12
|
+
service?: string;
|
|
13
|
+
from?: Date;
|
|
14
|
+
to?: Date;
|
|
15
|
+
limit?: number;
|
|
16
|
+
page?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AuditEntry {
|
|
20
|
+
id: string;
|
|
21
|
+
at: string;
|
|
22
|
+
service: string;
|
|
23
|
+
action: IAuditEvent['action'];
|
|
24
|
+
resource: string;
|
|
25
|
+
resourceId?: string;
|
|
26
|
+
actorKind: ActorKind;
|
|
27
|
+
actorLabel: string;
|
|
28
|
+
actorCredentialId?: string;
|
|
29
|
+
tenant: string;
|
|
30
|
+
changes?: Record<string, { from: unknown; to: unknown }>;
|
|
31
|
+
snapshot?: Record<string, unknown>;
|
|
32
|
+
count?: number;
|
|
33
|
+
/** present on 'call': which tool ran, how long it took, and what it cost */
|
|
34
|
+
tool?: ToolCall;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const toEntry = (event: IAuditEvent): AuditEntry => ({
|
|
38
|
+
id: String(event._id),
|
|
39
|
+
at: event.at.toISOString(),
|
|
40
|
+
service: event.service,
|
|
41
|
+
action: event.action,
|
|
42
|
+
resource: event.resource,
|
|
43
|
+
resourceId: event.resourceId,
|
|
44
|
+
actorKind: event.actorKind,
|
|
45
|
+
actorLabel: event.actorLabel,
|
|
46
|
+
actorCredentialId: event.actorCredentialId,
|
|
47
|
+
tenant: event.tenant,
|
|
48
|
+
changes: event.changes,
|
|
49
|
+
snapshot: event.snapshot,
|
|
50
|
+
count: event.count,
|
|
51
|
+
tool: event.tool
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* One account's trail, newest first.
|
|
56
|
+
*
|
|
57
|
+
* Always scoped to `userId` by the query rather than filtered afterwards, so
|
|
58
|
+
* there is no path where a wider read is one forgotten condition away. The trail
|
|
59
|
+
* carries old values of journal entries, which makes it as sensitive as the
|
|
60
|
+
* journal itself.
|
|
61
|
+
*/
|
|
62
|
+
export const listAuditEvents = async (
|
|
63
|
+
query: AuditQuery
|
|
64
|
+
): Promise<{ events: AuditEntry[]; total: number; page: number; totalPages: number }> => {
|
|
65
|
+
const limit = Math.min(query.limit ?? 50, 200);
|
|
66
|
+
const page = Math.max(query.page ?? 1, 1);
|
|
67
|
+
|
|
68
|
+
const filter: Record<string, unknown> = { userId: query.userId };
|
|
69
|
+
if (query.credentialId) filter.actorCredentialId = query.credentialId;
|
|
70
|
+
if (query.actorKind) filter.actorKind = query.actorKind;
|
|
71
|
+
if (query.action) filter.action = query.action;
|
|
72
|
+
if (query.resource) filter.resource = query.resource;
|
|
73
|
+
if (query.service) filter.service = query.service;
|
|
74
|
+
if (query.from || query.to) {
|
|
75
|
+
const at: Record<string, Date> = {};
|
|
76
|
+
if (query.from) at.$gte = query.from;
|
|
77
|
+
if (query.to) at.$lte = query.to;
|
|
78
|
+
filter.at = at;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const [events, total] = await Promise.all([
|
|
82
|
+
AuditEvent.find(filter)
|
|
83
|
+
.sort({ at: -1 })
|
|
84
|
+
.skip((page - 1) * limit)
|
|
85
|
+
.limit(limit),
|
|
86
|
+
AuditEvent.countDocuments(filter)
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
events: events.map(toEntry),
|
|
91
|
+
total,
|
|
92
|
+
page,
|
|
93
|
+
totalPages: Math.ceil(total / limit)
|
|
94
|
+
};
|
|
95
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -195,7 +195,9 @@ describe('OAuth access tokens are not sessions', () => {
|
|
|
195
195
|
scopes: ['relationship:read'],
|
|
196
196
|
tenants: [PERSONAL_TENANT],
|
|
197
197
|
defaultTenant: PERSONAL_TENANT,
|
|
198
|
-
tenantNames: {}
|
|
198
|
+
tenantNames: {},
|
|
199
|
+
clientId: 'client-1',
|
|
200
|
+
clientName: 'Claude'
|
|
199
201
|
}).accessToken;
|
|
200
202
|
|
|
201
203
|
it('refuses one on a session-only route', () => {
|
package/src/oauth/models.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import mongoose, { Document, Schema } from 'mongoose';
|
|
2
|
+
import { auditPlugin } from '../audit/plugin';
|
|
2
3
|
import { PERSONAL_TENANT, type Tenant } from '../apiKeys/types';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -148,6 +149,16 @@ const RefreshTokenSchema = new Schema<IRefreshToken>(
|
|
|
148
149
|
|
|
149
150
|
RefreshTokenSchema.index({ userId: 1, createdAt: -1 });
|
|
150
151
|
|
|
152
|
+
/**
|
|
153
|
+
* A grant appearing and being revoked is the shape of "who connected Claude,
|
|
154
|
+
* and when did it stop" — the question the connections page answers for the
|
|
155
|
+
* present and this answers for the past.
|
|
156
|
+
*
|
|
157
|
+
* `tokenHash` is redacted: it is the only thing standing between a copy of this
|
|
158
|
+
* collection and a working refresh token.
|
|
159
|
+
*/
|
|
160
|
+
RefreshTokenSchema.plugin(auditPlugin, { resource: 'connection', redact: ['tokenHash'] });
|
|
161
|
+
|
|
151
162
|
export const RefreshToken = mongoose.models.RefreshToken
|
|
152
163
|
? (mongoose.models.RefreshToken as mongoose.Model<IRefreshToken>)
|
|
153
164
|
: mongoose.model<IRefreshToken>('RefreshToken', RefreshTokenSchema);
|
package/src/oauth/tokens.test.ts
CHANGED
|
@@ -18,6 +18,8 @@ const mint = (over: Partial<Parameters<typeof mintAccessToken>[0]> = {}) =>
|
|
|
18
18
|
tenants: [PERSONAL_TENANT],
|
|
19
19
|
defaultTenant: PERSONAL_TENANT,
|
|
20
20
|
tenantNames: { [PERSONAL_TENANT]: 'My own data' },
|
|
21
|
+
clientId: 'client-1',
|
|
22
|
+
clientName: 'Claude',
|
|
21
23
|
...over
|
|
22
24
|
});
|
|
23
25
|
|
package/src/oauth/tokens.ts
CHANGED
|
@@ -49,6 +49,17 @@ export interface AccessTokenClaims {
|
|
|
49
49
|
tenantNames: Record<Tenant, string>;
|
|
50
50
|
/** the single tenant an older token was pinned to; read by `readTenants` */
|
|
51
51
|
groupId?: string | null;
|
|
52
|
+
/**
|
|
53
|
+
* Which app this token was issued to.
|
|
54
|
+
*
|
|
55
|
+
* `jti` identifies the token and rotates every hour, so it cannot stand in for
|
|
56
|
+
* the connection — an audit trail built on it would show a different actor
|
|
57
|
+
* each time the assistant refreshed. The client id is stable for the life of
|
|
58
|
+
* the grant, and the name is snapshotted alongside it for the same reason the
|
|
59
|
+
* tenant names are: nothing downstream can look it up.
|
|
60
|
+
*/
|
|
61
|
+
clientId: string;
|
|
62
|
+
clientName: string;
|
|
52
63
|
email: string;
|
|
53
64
|
name: string;
|
|
54
65
|
typ: typeof ACCESS_TOKEN_TYPE;
|
|
@@ -68,6 +79,8 @@ export interface MintAccessTokenInput {
|
|
|
68
79
|
tenants: Tenant[];
|
|
69
80
|
defaultTenant: Tenant;
|
|
70
81
|
tenantNames: Record<Tenant, string>;
|
|
82
|
+
clientId: string;
|
|
83
|
+
clientName: string;
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
export interface MintedAccessToken {
|
|
@@ -88,6 +101,8 @@ export const mintAccessToken = (input: MintAccessTokenInput): MintedAccessToken
|
|
|
88
101
|
tenants: input.tenants,
|
|
89
102
|
defaultTenant: input.defaultTenant,
|
|
90
103
|
tenantNames: input.tenantNames,
|
|
104
|
+
clientId: input.clientId,
|
|
105
|
+
clientName: input.clientName,
|
|
91
106
|
email: input.email,
|
|
92
107
|
name: input.name,
|
|
93
108
|
typ: ACCESS_TOKEN_TYPE,
|
|
@@ -109,6 +124,8 @@ export interface AccessTokenVerification {
|
|
|
109
124
|
scopes?: ApiKeyScope[];
|
|
110
125
|
tenants?: ApiKeyTenant;
|
|
111
126
|
tenantNames?: Record<Tenant, string>;
|
|
127
|
+
clientId?: string;
|
|
128
|
+
clientName?: string;
|
|
112
129
|
}
|
|
113
130
|
|
|
114
131
|
/**
|
|
@@ -144,7 +161,9 @@ export const verifyAccessToken = (
|
|
|
144
161
|
name: claims.name ?? '',
|
|
145
162
|
scopes: (claims.scope ?? '').split(' ').filter(isApiKeyScope),
|
|
146
163
|
tenants: readTenants(claims),
|
|
147
|
-
tenantNames: claims.tenantNames ?? {}
|
|
164
|
+
tenantNames: claims.tenantNames ?? {},
|
|
165
|
+
clientId: claims.clientId,
|
|
166
|
+
clientName: claims.clientName
|
|
148
167
|
};
|
|
149
168
|
};
|
|
150
169
|
|