@datacules/agent-identity-express 0.10.0 → 0.11.1

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/LICENSE ADDED
@@ -0,0 +1,109 @@
1
+ Datacules Agent Identity License — Version 1.0
2
+ Copyright (c) 2026 Datacules LLC. All rights reserved.
3
+
4
+ ─────────────────────────────────────────────────────────────────────────────
5
+ PREAMBLE
6
+ ─────────────────────────────────────────────────────────────────────────────
7
+
8
+ This software — Agent Identity & Auth Patterns — is developed and owned by
9
+ Datacules LLC. It is made available to the public as open-source software
10
+ under the permissive terms below.
11
+
12
+ Datacules LLC retains ownership and authorship of this software while
13
+ granting broad, royalty-free rights for anyone to use, copy, modify, and
14
+ distribute it — in commercial or non-commercial contexts — without requiring
15
+ that derivative works also become open source.
16
+
17
+ ─────────────────────────────────────────────────────────────────────────────
18
+ TERMS AND CONDITIONS
19
+ ─────────────────────────────────────────────────────────────────────────────
20
+
21
+ 1. PERMISSION TO USE
22
+
23
+ Permission is hereby granted, free of charge, to any person or
24
+ organization obtaining a copy of this software and associated
25
+ documentation files (the "Software"), to use, copy, modify, merge,
26
+ publish, distribute, sublicense, and/or sell copies of the Software,
27
+ and to permit persons to whom the Software is furnished to do so,
28
+ subject to the conditions below.
29
+
30
+ 2. ATTRIBUTION
31
+
32
+ a. Redistributions of source code must retain this copyright notice,
33
+ this list of conditions, and the disclaimer below.
34
+
35
+ b. Redistributions in binary form or as a product must reproduce this
36
+ copyright notice, this list of conditions, and the disclaimer in the
37
+ documentation and/or other materials provided with the distribution.
38
+
39
+ c. Neither the name "Datacules LLC" nor the names of its contributors
40
+ may be used to endorse or promote products derived from this Software
41
+ without prior written permission from Datacules LLC.
42
+
43
+ 3. COMMERCIAL USE
44
+
45
+ Use of this Software in commercial products, SaaS platforms, internal
46
+ enterprise tools, or any revenue-generating context is explicitly
47
+ permitted without royalty, fee, or additional licensing agreement,
48
+ provided that the conditions in Section 2 (Attribution) are met.
49
+
50
+ 4. NO COPYLEFT / NO VIRAL REQUIREMENT
51
+
52
+ This license does NOT require that derivative works, modifications,
53
+ or software that uses or embeds this Software be made open source.
54
+ You may incorporate this Software into proprietary or closed-source
55
+ products under your own license terms.
56
+
57
+ 5. MODIFICATIONS
58
+
59
+ Modified versions of the Software may be distributed under the same
60
+ terms as this license or under any other permissive open-source
61
+ license (e.g. MIT, Apache 2.0, BSD), provided that:
62
+
63
+ a. The original copyright notice of Datacules LLC is preserved.
64
+ b. Modifications are clearly documented and distinguished from the
65
+ original work.
66
+
67
+ 6. COMPATIBILITY
68
+
69
+ This license is compatible with other permissive open-source licenses
70
+ such as MIT, BSD 2-Clause, BSD 3-Clause, and Apache License 2.0. It
71
+ is also GPL-compatible — this Software may coexist with GPL-licensed
72
+ code, though this Software itself is not distributed under the GPL.
73
+
74
+ ─────────────────────────────────────────────────────────────────────────────
75
+ DISCLAIMER
76
+ ─────────────────────────────────────────────────────────────────────────────
77
+
78
+ THIS SOFTWARE IS PROVIDED BY DATACULES LLC AND CONTRIBUTORS "AS IS" AND
79
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
80
+ IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
81
+ AND NON-INFRINGEMENT ARE DISCLAIMED.
82
+
83
+ IN NO EVENT SHALL DATACULES LLC OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
84
+ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
85
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
86
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
87
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
88
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
89
+ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
90
+
91
+ ─────────────────────────────────────────────────────────────────────────────
92
+ SUMMARY (non-binding)
93
+ ─────────────────────────────────────────────────────────────────────────────
94
+
95
+ ✔ Use freely — commercial, proprietary, or open-source projects
96
+ ✔ Modify and distribute with or without changes
97
+ ✔ Sell products built on this Software
98
+ ✔ No royalties or fees
99
+ ✔ No requirement to open-source your own code
100
+ ✔ Attribution to Datacules LLC required in source and binary distributions
101
+ ✗ Do not use "Datacules LLC" to endorse derived products without permission
102
+
103
+ ─────────────────────────────────────────────────────────────────────────────
104
+ CONTACT
105
+ ─────────────────────────────────────────────────────────────────────────────
106
+
107
+ Datacules LLC
108
+ For licensing enquiries: legal@datacules.com
109
+ Product: https://github.com/hvrcharon1/agent-identity
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.agentIdentityMiddleware = agentIdentityMiddleware;
4
+ /**
5
+ * Express middleware for @datacules/agent-identity.
6
+ *
7
+ * Resolves credentials before any downstream route handler runs.
8
+ * The resolved credential is attached to req.resolvedCredential.
9
+ *
10
+ * Usage:
11
+ * import express from 'express';
12
+ * import { agentIdentityMiddleware } from '@datacules/agent-identity-express';
13
+ *
14
+ * const app = express();
15
+ * app.use(express.json());
16
+ * app.use(agentIdentityMiddleware({ credentials, rules, logger }));
17
+ *
18
+ * app.post('/ai/complete', (req, res) => {
19
+ * const cred = req.resolvedCredential; // already resolved
20
+ * });
21
+ */
22
+ const agent_identity_1 = require("@datacules/agent-identity");
23
+ function agentIdentityMiddleware(options) {
24
+ const { credentials, rules, logger, contextKey = 'agentContext', passThrough = true, } = options;
25
+ const router = (0, agent_identity_1.createRouter)(credentials, rules, logger);
26
+ return (req, res, next) => {
27
+ const ctx = req.body?.[contextKey];
28
+ if (!ctx) {
29
+ if (passThrough)
30
+ return next();
31
+ res.status(400).json({ error: `Missing required field: ${contextKey}` });
32
+ return;
33
+ }
34
+ const resolved = router.resolve(ctx);
35
+ if (!resolved) {
36
+ res.status(403).json({ error: 'No credential resolved for this context' });
37
+ return;
38
+ }
39
+ req.resolvedCredential = resolved;
40
+ next();
41
+ };
42
+ }
43
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;AAuDA,0DA6BC;AApFD;;;;;;;;;;;;;;;;;GAiBG;AACH,8DAAyD;AAqCzD,SAAgB,uBAAuB,CAAC,OAAuC;IAC7E,MAAM,EACJ,WAAW,EACX,KAAK,EACL,MAAM,EACN,UAAU,GAAG,cAAc,EAC3B,WAAW,GAAG,IAAI,GACnB,GAAG,OAAO,CAAC;IAEZ,MAAM,MAAM,GAAG,IAAA,6BAAY,EAAC,WAAW,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;IAExD,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAQ,EAAE;QAC/D,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,UAAU,CAAoC,CAAC;QAEtE,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,IAAI,WAAW;gBAAE,OAAO,IAAI,EAAE,CAAC;YAC/B,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,2BAA2B,UAAU,EAAE,EAAE,CAAC,CAAC;YACzE,OAAO;QACT,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yCAAyC,EAAE,CAAC,CAAC;YAC3E,OAAO;QACT,CAAC;QAED,GAAG,CAAC,kBAAkB,GAAG,QAAQ,CAAC;QAClC,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Express middleware for @datacules/agent-identity.
3
+ *
4
+ * Resolves credentials before any downstream route handler runs.
5
+ * The resolved credential is attached to req.resolvedCredential.
6
+ *
7
+ * Usage:
8
+ * import express from 'express';
9
+ * import { agentIdentityMiddleware } from '@datacules/agent-identity-express';
10
+ *
11
+ * const app = express();
12
+ * app.use(express.json());
13
+ * app.use(agentIdentityMiddleware({ credentials, rules, logger }));
14
+ *
15
+ * app.post('/ai/complete', (req, res) => {
16
+ * const cred = req.resolvedCredential; // already resolved
17
+ * });
18
+ */
19
+ import { createRouter } from '@datacules/agent-identity';
20
+ export function agentIdentityMiddleware(options) {
21
+ const { credentials, rules, logger, contextKey = 'agentContext', passThrough = true, } = options;
22
+ const router = createRouter(credentials, rules, logger);
23
+ return (req, res, next) => {
24
+ const ctx = req.body?.[contextKey];
25
+ if (!ctx) {
26
+ if (passThrough)
27
+ return next();
28
+ res.status(400).json({ error: `Missing required field: ${contextKey}` });
29
+ return;
30
+ }
31
+ const resolved = router.resolve(ctx);
32
+ if (!resolved) {
33
+ res.status(403).json({ error: 'No credential resolved for this context' });
34
+ return;
35
+ }
36
+ req.resolvedCredential = resolved;
37
+ next();
38
+ };
39
+ }
40
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAqCzD,MAAM,UAAU,uBAAuB,CAAC,OAAuC;IAC7E,MAAM,EACJ,WAAW,EACX,KAAK,EACL,MAAM,EACN,UAAU,GAAG,cAAc,EAC3B,WAAW,GAAG,IAAI,GACnB,GAAG,OAAO,CAAC;IAEZ,MAAM,MAAM,GAAG,YAAY,CAAC,WAAW,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;IAExD,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAQ,EAAE;QAC/D,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,UAAU,CAAoC,CAAC;QAEtE,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,IAAI,WAAW;gBAAE,OAAO,IAAI,EAAE,CAAC;YAC/B,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,2BAA2B,UAAU,EAAE,EAAE,CAAC,CAAC;YACzE,OAAO;QACT,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yCAAyC,EAAE,CAAC,CAAC;YAC3E,OAAO;QACT,CAAC;QAED,GAAG,CAAC,kBAAkB,GAAG,QAAQ,CAAC;QAClC,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,28 @@
1
+ import type { AuditLogger, Credential, ResolvedCredential, RoutingRule } from '@datacules/agent-identity';
2
+ import type { Request, Response, NextFunction } from 'express';
3
+ declare global {
4
+ namespace Express {
5
+ interface Request {
6
+ resolvedCredential?: ResolvedCredential;
7
+ }
8
+ }
9
+ }
10
+ export interface AgentIdentityMiddlewareOptions {
11
+ credentials: Credential[];
12
+ rules: RoutingRule[];
13
+ logger?: AuditLogger;
14
+ /**
15
+ * Key in req.body that holds the AgentRequestContext.
16
+ * Default: 'agentContext'
17
+ */
18
+ contextKey?: string;
19
+ /**
20
+ * If true, the middleware passes through when no agentContext is found
21
+ * rather than returning 400. Use when the middleware is global and only
22
+ * some routes are agent-identity-aware.
23
+ * Default: true
24
+ */
25
+ passThrough?: boolean;
26
+ }
27
+ export declare function agentIdentityMiddleware(options: AgentIdentityMiddlewareOptions): (req: Request, res: Response, next: NextFunction) => void;
28
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAEV,WAAW,EACX,UAAU,EACV,kBAAkB,EAClB,WAAW,EACZ,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG/D,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;SACzC;KACF;CACF;AAED,MAAM,WAAW,8BAA8B;IAC7C,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,8BAA8B,IAWrE,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,KAAG,IAAI,CAkB/D"}
package/package.json CHANGED
@@ -1,17 +1,44 @@
1
1
  {
2
2
  "name": "@datacules/agent-identity-express",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "private": false,
5
5
  "description": "Express middleware for @datacules/agent-identity",
6
+ "author": "Datacules LLC",
7
+ "license": "SEE LICENSE IN LICENSE",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/hvrcharon1/agent-identity.git",
11
+ "directory": "packages/integrations/express"
12
+ },
13
+ "keywords": [
14
+ "agent-identity",
15
+ "express",
16
+ "middleware",
17
+ "credential-routing",
18
+ "ai-agents",
19
+ "datacules"
20
+ ],
6
21
  "main": "./dist/cjs/index.js",
7
22
  "module": "./dist/esm/index.js",
8
23
  "types": "./dist/types/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "import": "./dist/esm/index.js",
27
+ "require": "./dist/cjs/index.js",
28
+ "types": "./dist/types/index.d.ts"
29
+ }
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "LICENSE",
34
+ "README.md"
35
+ ],
9
36
  "scripts": {
10
- "build": "tsc -p tsconfig.build.json",
37
+ "build": "tsc -p tsconfig.build.json && tsc -p tsconfig.cjs.json",
11
38
  "type-check": "tsc --noEmit"
12
39
  },
13
40
  "peerDependencies": {
14
- "@datacules/agent-identity": "^0.8.0",
41
+ "@datacules/agent-identity": "^0.11.1",
15
42
  "express": ">=4.0.0"
16
43
  },
17
44
  "devDependencies": {
@@ -1,340 +0,0 @@
1
- /**
2
- * express.test.ts
3
- *
4
- * Vitest test suite for agentIdentityMiddleware from
5
- * @datacules/agent-identity-express.
6
- *
7
- * Express uses `import type` for Request/Response/NextFunction — those imports
8
- * are erased at runtime, so no express runtime dependency is needed here.
9
- * req, res, and next are created as plain typed mock objects.
10
- *
11
- * 13 test cases:
12
- * passThrough behavior (4): absent context + passThrough=true/false variants
13
- * credential resolution (7): attach, next, resolvedFor, 403, expired, logger
14
- * custom contextKey (2): reads correct field, 400 names custom key
15
- */
16
- import { describe, it, expect, vi } from 'vitest';
17
- import { agentIdentityMiddleware } from './index';
18
- import type { Credential, RoutingRule } from '@datacules/agent-identity';
19
-
20
- // ─── Fixtures ────────────────────────────────────────────────────────────────
21
-
22
- const FIXED_CREDENTIAL: Credential = {
23
- id: 'cred-openai-fixed',
24
- kind: 'fixed',
25
- name: 'OpenAI Prod Key',
26
- scope: 'read write',
27
- status: 'active',
28
- provider: 'openai',
29
- ref: 'openai-prod-key',
30
- };
31
-
32
- const USER_DELEGATED_CREDENTIAL: Credential = {
33
- id: 'cred-anthropic-user',
34
- kind: 'user-delegated',
35
- name: 'Anthropic User Token',
36
- scope: 'read',
37
- status: 'active',
38
- provider: 'anthropic',
39
- ref: 'anthropic-user-token',
40
- };
41
-
42
- const EXPIRED_CREDENTIAL: Credential = {
43
- id: 'cred-expired',
44
- kind: 'fixed',
45
- name: 'Expired Key',
46
- scope: 'read write',
47
- status: 'active',
48
- provider: 'openai',
49
- ref: 'expired-key',
50
- expiresAt: new Date(Date.now() - 1_000).toISOString(), // 1 second in the past
51
- };
52
-
53
- const RULES: RoutingRule[] = [
54
- {
55
- id: 'rule-openai-shared',
56
- credentialRef: 'openai-prod-key',
57
- priority: 10,
58
- matchProvider: 'openai',
59
- matchResourceKind: 'shared',
60
- },
61
- {
62
- id: 'rule-anthropic-personal',
63
- credentialRef: 'anthropic-user-token',
64
- priority: 20,
65
- matchProvider: 'anthropic',
66
- matchResourceKind: 'personal',
67
- },
68
- ];
69
-
70
- const EXPIRED_RULES: RoutingRule[] = [
71
- {
72
- id: 'rule-expired',
73
- credentialRef: 'expired-key',
74
- priority: 5,
75
- matchProvider: 'openai',
76
- matchResourceKind: 'shared',
77
- },
78
- ];
79
-
80
- const BASE_CONTEXT = {
81
- userId: 'user-123',
82
- resourceId: 'res-abc',
83
- resourceKind: 'shared' as const,
84
- provider: 'openai' as const,
85
- model: 'gpt-4',
86
- action: 'complete',
87
- traceId: 'trace-001',
88
- requestedAt: new Date().toISOString(),
89
- };
90
-
91
- // ─── Mock helpers ─────────────────────────────────────────────────────────────
92
-
93
- // req.body can be undefined (before express.json() middleware runs)
94
- function makeReq(body?: Record<string, unknown>) {
95
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
96
- return { body } as any;
97
- }
98
-
99
- // res.status(N).json(obj) — status() returns a plain object whose json property
100
- // is the same vi.fn() exposed as res.json, so assertions on res.json capture
101
- // all json() calls made through either the chained or direct path.
102
- function makeRes() {
103
- const json = vi.fn();
104
- const status = vi.fn(() => ({ json }));
105
- return { status, json };
106
- }
107
-
108
- function makeNext() {
109
- return vi.fn();
110
- }
111
-
112
- // ─── Tests ────────────────────────────────────────────────────────────────────
113
-
114
- describe('agentIdentityMiddleware', () => {
115
-
116
- // ─── passThrough behavior ──────────────────────────────────────────────────
117
-
118
- describe('passThrough behavior', () => {
119
- it('calls next() when agentContext is absent and passThrough=true (default)', () => {
120
- const mw = agentIdentityMiddleware({
121
- credentials: [FIXED_CREDENTIAL],
122
- rules: RULES,
123
- });
124
- const req = makeReq({ otherField: 'value' });
125
- const res = makeRes();
126
- const next = makeNext();
127
-
128
- mw(req, res as any, next); // eslint-disable-line @typescript-eslint/no-explicit-any
129
-
130
- expect(next).toHaveBeenCalledOnce();
131
- expect(res.status).not.toHaveBeenCalled();
132
- });
133
-
134
- it('calls next() when req.body is undefined and passThrough=true', () => {
135
- const mw = agentIdentityMiddleware({
136
- credentials: [FIXED_CREDENTIAL],
137
- rules: RULES,
138
- });
139
- const req = makeReq(undefined);
140
- const res = makeRes();
141
- const next = makeNext();
142
-
143
- mw(req, res as any, next); // eslint-disable-line @typescript-eslint/no-explicit-any
144
-
145
- expect(next).toHaveBeenCalledOnce();
146
- expect(res.status).not.toHaveBeenCalled();
147
- });
148
-
149
- it('sends 400 when agentContext is absent and passThrough=false', () => {
150
- const mw = agentIdentityMiddleware({
151
- credentials: [FIXED_CREDENTIAL],
152
- rules: RULES,
153
- passThrough: false,
154
- });
155
- const req = makeReq({});
156
- const res = makeRes();
157
- const next = makeNext();
158
-
159
- mw(req, res as any, next); // eslint-disable-line @typescript-eslint/no-explicit-any
160
-
161
- expect(res.status).toHaveBeenCalledWith(400);
162
- expect(next).not.toHaveBeenCalled();
163
- });
164
-
165
- it('400 error message names the missing contextKey', () => {
166
- const mw = agentIdentityMiddleware({
167
- credentials: [FIXED_CREDENTIAL],
168
- rules: RULES,
169
- passThrough: false,
170
- });
171
- const req = makeReq({});
172
- const res = makeRes();
173
- const next = makeNext();
174
-
175
- mw(req, res as any, next); // eslint-disable-line @typescript-eslint/no-explicit-any
176
-
177
- // res.status() returns { json: same-vi-fn }, so res.json captures the call
178
- expect(res.json).toHaveBeenCalledWith(
179
- expect.objectContaining({ error: expect.stringContaining('agentContext') })
180
- );
181
- });
182
- });
183
-
184
- // ─── Credential resolution ─────────────────────────────────────────────────
185
-
186
- describe('credential resolution', () => {
187
- it('attaches resolvedCredential to req on successful resolution', () => {
188
- const mw = agentIdentityMiddleware({
189
- credentials: [FIXED_CREDENTIAL],
190
- rules: RULES,
191
- });
192
- const req = makeReq({ agentContext: BASE_CONTEXT });
193
- const res = makeRes();
194
- const next = makeNext();
195
-
196
- mw(req, res as any, next); // eslint-disable-line @typescript-eslint/no-explicit-any
197
-
198
- expect(req.resolvedCredential).toBeDefined();
199
- expect(req.resolvedCredential?.credentialId).toBe('cred-openai-fixed');
200
- });
201
-
202
- it('calls next() and sends no response on successful resolution', () => {
203
- const mw = agentIdentityMiddleware({
204
- credentials: [FIXED_CREDENTIAL],
205
- rules: RULES,
206
- });
207
- const req = makeReq({ agentContext: BASE_CONTEXT });
208
- const res = makeRes();
209
- const next = makeNext();
210
-
211
- mw(req, res as any, next); // eslint-disable-line @typescript-eslint/no-explicit-any
212
-
213
- expect(next).toHaveBeenCalledOnce();
214
- expect(res.status).not.toHaveBeenCalled();
215
- });
216
-
217
- it('sets resolvedFor to "service" for fixed credentials', () => {
218
- const mw = agentIdentityMiddleware({
219
- credentials: [FIXED_CREDENTIAL],
220
- rules: RULES,
221
- });
222
- const req = makeReq({ agentContext: BASE_CONTEXT });
223
- const res = makeRes();
224
- const next = makeNext();
225
-
226
- mw(req, res as any, next); // eslint-disable-line @typescript-eslint/no-explicit-any
227
-
228
- expect(req.resolvedCredential?.kind).toBe('fixed');
229
- expect(req.resolvedCredential?.resolvedFor).toBe('service');
230
- });
231
-
232
- it('sets resolvedFor to ctx.userId for user-delegated credentials', () => {
233
- const mw = agentIdentityMiddleware({
234
- credentials: [FIXED_CREDENTIAL, USER_DELEGATED_CREDENTIAL],
235
- rules: RULES,
236
- });
237
- const anthropicCtx = {
238
- ...BASE_CONTEXT,
239
- provider: 'anthropic' as const,
240
- resourceKind: 'personal' as const,
241
- };
242
- const req = makeReq({ agentContext: anthropicCtx });
243
- const res = makeRes();
244
- const next = makeNext();
245
-
246
- mw(req, res as any, next); // eslint-disable-line @typescript-eslint/no-explicit-any
247
-
248
- expect(req.resolvedCredential?.kind).toBe('user-delegated');
249
- expect(req.resolvedCredential?.resolvedFor).toBe('user-123');
250
- });
251
-
252
- it('sends 403 when no routing rule matches the context', () => {
253
- const mw = agentIdentityMiddleware({
254
- credentials: [FIXED_CREDENTIAL],
255
- rules: RULES,
256
- });
257
- // gemini matches no configured rule
258
- const ctx = { ...BASE_CONTEXT, provider: 'gemini' as const };
259
- const req = makeReq({ agentContext: ctx });
260
- const res = makeRes();
261
- const next = makeNext();
262
-
263
- mw(req, res as any, next); // eslint-disable-line @typescript-eslint/no-explicit-any
264
-
265
- expect(res.status).toHaveBeenCalledWith(403);
266
- expect(next).not.toHaveBeenCalled();
267
- });
268
-
269
- it('sends 403 when the matched credential is expired', () => {
270
- const mw = agentIdentityMiddleware({
271
- credentials: [EXPIRED_CREDENTIAL],
272
- rules: EXPIRED_RULES,
273
- });
274
- const req = makeReq({ agentContext: BASE_CONTEXT });
275
- const res = makeRes();
276
- const next = makeNext();
277
-
278
- mw(req, res as any, next); // eslint-disable-line @typescript-eslint/no-explicit-any
279
-
280
- expect(res.status).toHaveBeenCalledWith(403);
281
- expect(next).not.toHaveBeenCalled();
282
- });
283
-
284
- it('invokes the audit logger when a credential resolves successfully', () => {
285
- const logger = { log: vi.fn() };
286
- const mw = agentIdentityMiddleware({
287
- credentials: [FIXED_CREDENTIAL],
288
- rules: RULES,
289
- logger,
290
- });
291
- const req = makeReq({ agentContext: BASE_CONTEXT });
292
- const res = makeRes();
293
- const next = makeNext();
294
-
295
- mw(req, res as any, next); // eslint-disable-line @typescript-eslint/no-explicit-any
296
-
297
- // logger.log() is called synchronously inside Promise.resolve(logger.log(entry))
298
- expect(logger.log).toHaveBeenCalledOnce();
299
- });
300
- });
301
-
302
- // ─── Custom contextKey ─────────────────────────────────────────────────────
303
-
304
- describe('custom contextKey', () => {
305
- it('reads the agent context from the custom contextKey field', () => {
306
- const mw = agentIdentityMiddleware({
307
- credentials: [FIXED_CREDENTIAL],
308
- rules: RULES,
309
- contextKey: 'identity',
310
- });
311
- const req = makeReq({ identity: BASE_CONTEXT });
312
- const res = makeRes();
313
- const next = makeNext();
314
-
315
- mw(req, res as any, next); // eslint-disable-line @typescript-eslint/no-explicit-any
316
-
317
- expect(req.resolvedCredential).toBeDefined();
318
- expect(next).toHaveBeenCalledOnce();
319
- });
320
-
321
- it('400 error message names the custom contextKey when passThrough=false', () => {
322
- const mw = agentIdentityMiddleware({
323
- credentials: [FIXED_CREDENTIAL],
324
- rules: RULES,
325
- contextKey: 'identity',
326
- passThrough: false,
327
- });
328
- const req = makeReq({});
329
- const res = makeRes();
330
- const next = makeNext();
331
-
332
- mw(req, res as any, next); // eslint-disable-line @typescript-eslint/no-explicit-any
333
-
334
- expect(res.status).toHaveBeenCalledWith(400);
335
- expect(res.json).toHaveBeenCalledWith(
336
- expect.objectContaining({ error: expect.stringContaining('identity') })
337
- );
338
- });
339
- });
340
- });
package/src/index.ts DELETED
@@ -1,85 +0,0 @@
1
- /**
2
- * Express middleware for @datacules/agent-identity.
3
- *
4
- * Resolves credentials before any downstream route handler runs.
5
- * The resolved credential is attached to req.resolvedCredential.
6
- *
7
- * Usage:
8
- * import express from 'express';
9
- * import { agentIdentityMiddleware } from '@datacules/agent-identity-express';
10
- *
11
- * const app = express();
12
- * app.use(express.json());
13
- * app.use(agentIdentityMiddleware({ credentials, rules, logger }));
14
- *
15
- * app.post('/ai/complete', (req, res) => {
16
- * const cred = req.resolvedCredential; // already resolved
17
- * });
18
- */
19
- import { createRouter } from '@datacules/agent-identity';
20
- import type {
21
- AgentRequestContext,
22
- AuditLogger,
23
- Credential,
24
- ResolvedCredential,
25
- RoutingRule,
26
- } from '@datacules/agent-identity';
27
- import type { Request, Response, NextFunction } from 'express';
28
-
29
- // Extend Express Request type
30
- declare global {
31
- namespace Express {
32
- interface Request {
33
- resolvedCredential?: ResolvedCredential;
34
- }
35
- }
36
- }
37
-
38
- export interface AgentIdentityMiddlewareOptions {
39
- credentials: Credential[];
40
- rules: RoutingRule[];
41
- logger?: AuditLogger;
42
- /**
43
- * Key in req.body that holds the AgentRequestContext.
44
- * Default: 'agentContext'
45
- */
46
- contextKey?: string;
47
- /**
48
- * If true, the middleware passes through when no agentContext is found
49
- * rather than returning 400. Use when the middleware is global and only
50
- * some routes are agent-identity-aware.
51
- * Default: true
52
- */
53
- passThrough?: boolean;
54
- }
55
-
56
- export function agentIdentityMiddleware(options: AgentIdentityMiddlewareOptions) {
57
- const {
58
- credentials,
59
- rules,
60
- logger,
61
- contextKey = 'agentContext',
62
- passThrough = true,
63
- } = options;
64
-
65
- const router = createRouter(credentials, rules, logger);
66
-
67
- return (req: Request, res: Response, next: NextFunction): void => {
68
- const ctx = req.body?.[contextKey] as AgentRequestContext | undefined;
69
-
70
- if (!ctx) {
71
- if (passThrough) return next();
72
- res.status(400).json({ error: `Missing required field: ${contextKey}` });
73
- return;
74
- }
75
-
76
- const resolved = router.resolve(ctx);
77
- if (!resolved) {
78
- res.status(403).json({ error: 'No credential resolved for this context' });
79
- return;
80
- }
81
-
82
- req.resolvedCredential = resolved;
83
- next();
84
- };
85
- }