@datacules/agent-identity-fastify 0.11.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 +109 -0
- package/dist/cjs/index.js +49 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/esm/index.js +43 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/types/index.d.ts +16 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +30 -3
- package/src/fastify.test.ts +0 -342
- package/src/index.ts +0 -81
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,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.agentIdentityPlugin = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Fastify plugin for @datacules/agent-identity.
|
|
9
|
+
*
|
|
10
|
+
* Decorates each request with resolvedCredential before the route handler runs.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* import Fastify from 'fastify';
|
|
14
|
+
* import { agentIdentityPlugin } from '@datacules/agent-identity-fastify';
|
|
15
|
+
*
|
|
16
|
+
* const app = Fastify();
|
|
17
|
+
* await app.register(agentIdentityPlugin, { credentials, rules, logger });
|
|
18
|
+
*
|
|
19
|
+
* app.post('/ai/complete', async (request, reply) => {
|
|
20
|
+
* const cred = request.resolvedCredential; // typed
|
|
21
|
+
* });
|
|
22
|
+
*/
|
|
23
|
+
const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
|
|
24
|
+
const agent_identity_1 = require("@datacules/agent-identity");
|
|
25
|
+
const plugin = async (fastify, options) => {
|
|
26
|
+
const { credentials, rules, logger, contextKey = 'agentContext', passThrough = true, } = options;
|
|
27
|
+
const router = (0, agent_identity_1.createRouter)(credentials, rules, logger);
|
|
28
|
+
fastify.decorateRequest('resolvedCredential', null);
|
|
29
|
+
fastify.addHook('preHandler', async (request, reply) => {
|
|
30
|
+
const body = request.body;
|
|
31
|
+
const ctx = body?.[contextKey];
|
|
32
|
+
if (!ctx) {
|
|
33
|
+
if (!passThrough) {
|
|
34
|
+
return reply.status(400).send({ error: `Missing required field: ${contextKey}` });
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const resolved = router.resolve(ctx);
|
|
39
|
+
if (!resolved) {
|
|
40
|
+
return reply.status(403).send({ error: 'No credential resolved for this context' });
|
|
41
|
+
}
|
|
42
|
+
request.resolvedCredential = resolved;
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
exports.agentIdentityPlugin = (0, fastify_plugin_1.default)(plugin, {
|
|
46
|
+
name: '@datacules/agent-identity-fastify',
|
|
47
|
+
fastify: '>=4.0.0',
|
|
48
|
+
});
|
|
49
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;;;AAAA;;;;;;;;;;;;;;;GAeG;AACH,oEAAgC;AAChC,8DAAyD;AAwBzD,MAAM,MAAM,GAAmD,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE;IACxF,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,eAAe,CAAC,oBAAoB,EAAE,IAAI,CAAC,CAAC;IAEpD,OAAO,CAAC,OAAO,CACb,YAAY,EACZ,KAAK,EAAE,OAAuB,EAAE,KAAmB,EAAE,EAAE;QACrD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAsC,CAAC;QAC5D,MAAM,GAAG,GAAG,IAAI,EAAE,CAAC,UAAU,CAAoC,CAAC;QAElE,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjB,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,2BAA2B,UAAU,EAAE,EAAE,CAAC,CAAC;YACpF,CAAC;YACD,OAAO;QACT,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yCAAyC,EAAE,CAAC,CAAC;QACtF,CAAC;QAED,OAAO,CAAC,kBAAkB,GAAG,QAAQ,CAAC;IACxC,CAAC,CACF,CAAC;AACJ,CAAC,CAAC;AAEW,QAAA,mBAAmB,GAAG,IAAA,wBAAE,EAAC,MAAM,EAAE;IAC5C,IAAI,EAAE,mCAAmC;IACzC,OAAO,EAAE,SAAS;CACnB,CAAC,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fastify plugin for @datacules/agent-identity.
|
|
3
|
+
*
|
|
4
|
+
* Decorates each request with resolvedCredential before the route handler runs.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* import Fastify from 'fastify';
|
|
8
|
+
* import { agentIdentityPlugin } from '@datacules/agent-identity-fastify';
|
|
9
|
+
*
|
|
10
|
+
* const app = Fastify();
|
|
11
|
+
* await app.register(agentIdentityPlugin, { credentials, rules, logger });
|
|
12
|
+
*
|
|
13
|
+
* app.post('/ai/complete', async (request, reply) => {
|
|
14
|
+
* const cred = request.resolvedCredential; // typed
|
|
15
|
+
* });
|
|
16
|
+
*/
|
|
17
|
+
import fp from 'fastify-plugin';
|
|
18
|
+
import { createRouter } from '@datacules/agent-identity';
|
|
19
|
+
const plugin = async (fastify, options) => {
|
|
20
|
+
const { credentials, rules, logger, contextKey = 'agentContext', passThrough = true, } = options;
|
|
21
|
+
const router = createRouter(credentials, rules, logger);
|
|
22
|
+
fastify.decorateRequest('resolvedCredential', null);
|
|
23
|
+
fastify.addHook('preHandler', async (request, reply) => {
|
|
24
|
+
const body = request.body;
|
|
25
|
+
const ctx = body?.[contextKey];
|
|
26
|
+
if (!ctx) {
|
|
27
|
+
if (!passThrough) {
|
|
28
|
+
return reply.status(400).send({ error: `Missing required field: ${contextKey}` });
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const resolved = router.resolve(ctx);
|
|
33
|
+
if (!resolved) {
|
|
34
|
+
return reply.status(403).send({ error: 'No credential resolved for this context' });
|
|
35
|
+
}
|
|
36
|
+
request.resolvedCredential = resolved;
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
export const agentIdentityPlugin = fp(plugin, {
|
|
40
|
+
name: '@datacules/agent-identity-fastify',
|
|
41
|
+
fastify: '>=4.0.0',
|
|
42
|
+
});
|
|
43
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAChC,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAwBzD,MAAM,MAAM,GAAmD,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE;IACxF,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,eAAe,CAAC,oBAAoB,EAAE,IAAI,CAAC,CAAC;IAEpD,OAAO,CAAC,OAAO,CACb,YAAY,EACZ,KAAK,EAAE,OAAuB,EAAE,KAAmB,EAAE,EAAE;QACrD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAsC,CAAC;QAC5D,MAAM,GAAG,GAAG,IAAI,EAAE,CAAC,UAAU,CAAoC,CAAC;QAElE,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjB,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,2BAA2B,UAAU,EAAE,EAAE,CAAC,CAAC;YACpF,CAAC;YACD,OAAO;QACT,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yCAAyC,EAAE,CAAC,CAAC;QACtF,CAAC;QAED,OAAO,CAAC,kBAAkB,GAAG,QAAQ,CAAC;IACxC,CAAC,CACF,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAC,MAAM,EAAE;IAC5C,IAAI,EAAE,mCAAmC;IACzC,OAAO,EAAE,SAAS;CACnB,CAAC,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AuditLogger, Credential, ResolvedCredential, RoutingRule } from '@datacules/agent-identity';
|
|
2
|
+
import type { FastifyPluginAsync } from 'fastify';
|
|
3
|
+
declare module 'fastify' {
|
|
4
|
+
interface FastifyRequest {
|
|
5
|
+
resolvedCredential: ResolvedCredential | null;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export interface AgentIdentityPluginOptions {
|
|
9
|
+
credentials: Credential[];
|
|
10
|
+
rules: RoutingRule[];
|
|
11
|
+
logger?: AuditLogger;
|
|
12
|
+
contextKey?: string;
|
|
13
|
+
passThrough?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare const agentIdentityPlugin: FastifyPluginAsync<AgentIdentityPluginOptions>;
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAEV,WAAW,EACX,UAAU,EACV,kBAAkB,EAClB,WAAW,EACZ,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,kBAAkB,EAAgC,MAAM,SAAS,CAAC;AAEhF,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,cAAc;QACtB,kBAAkB,EAAE,kBAAkB,GAAG,IAAI,CAAC;KAC/C;CACF;AAED,MAAM,WAAW,0BAA0B;IACzC,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAsCD,eAAO,MAAM,mBAAmB,gDAG9B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,17 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datacules/agent-identity-fastify",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Fastify plugin 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/fastify"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"agent-identity",
|
|
15
|
+
"fastify",
|
|
16
|
+
"plugin",
|
|
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.
|
|
41
|
+
"@datacules/agent-identity": "^0.11.1",
|
|
15
42
|
"fastify": ">=4.0.0",
|
|
16
43
|
"fastify-plugin": ">=4.0.0"
|
|
17
44
|
},
|
package/src/fastify.test.ts
DELETED
|
@@ -1,342 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* fastify.test.ts
|
|
3
|
-
*
|
|
4
|
-
* Vitest test suite for agentIdentityPlugin from
|
|
5
|
-
* @datacules/agent-identity-fastify.
|
|
6
|
-
*
|
|
7
|
-
* Fastify uses `import type` for FastifyPluginAsync, FastifyRequest,
|
|
8
|
-
* FastifyReply — type imports are erased at runtime.
|
|
9
|
-
*
|
|
10
|
-
* The preHandler hook under test is extracted by calling the plugin with
|
|
11
|
-
* a minimal mock Fastify instance that captures decorateRequest and
|
|
12
|
-
* addHook calls. This avoids requiring the fastify or fastify-plugin
|
|
13
|
-
* runtime packages.
|
|
14
|
-
*
|
|
15
|
-
* 12 test cases:
|
|
16
|
-
* passThrough behavior (4): absent context passThrough true/false, 400 naming
|
|
17
|
-
* credential resolution (5): attach, resolvedFor, 403 on no match, 403 expired
|
|
18
|
-
* custom contextKey (2): reads correct field, 400 names custom key
|
|
19
|
-
* plugin registration (1): fastify-plugin wrapping verified
|
|
20
|
-
*/
|
|
21
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
22
|
-
import { agentIdentityPlugin } from './index';
|
|
23
|
-
import type { Credential, RoutingRule } from '@datacules/agent-identity';
|
|
24
|
-
|
|
25
|
-
// ─── Fixtures ─────────────────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
const FIXED_CREDENTIAL: Credential = {
|
|
28
|
-
id: 'cred-openai-fixed',
|
|
29
|
-
kind: 'fixed',
|
|
30
|
-
name: 'OpenAI Prod Key',
|
|
31
|
-
scope: 'read write',
|
|
32
|
-
status: 'active',
|
|
33
|
-
provider: 'openai',
|
|
34
|
-
ref: 'openai-prod-key',
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const USER_DELEGATED_CREDENTIAL: Credential = {
|
|
38
|
-
id: 'cred-anthropic-user',
|
|
39
|
-
kind: 'user-delegated',
|
|
40
|
-
name: 'Anthropic User Token',
|
|
41
|
-
scope: 'read',
|
|
42
|
-
status: 'active',
|
|
43
|
-
provider: 'anthropic',
|
|
44
|
-
ref: 'anthropic-user-token',
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const EXPIRED_CREDENTIAL: Credential = {
|
|
48
|
-
id: 'cred-expired',
|
|
49
|
-
kind: 'fixed',
|
|
50
|
-
name: 'Expired Key',
|
|
51
|
-
scope: 'read write',
|
|
52
|
-
status: 'active',
|
|
53
|
-
provider: 'openai',
|
|
54
|
-
ref: 'expired-key',
|
|
55
|
-
expiresAt: new Date(Date.now() - 1_000).toISOString(),
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const RULES: RoutingRule[] = [
|
|
59
|
-
{
|
|
60
|
-
id: 'rule-openai-shared',
|
|
61
|
-
credentialRef: 'openai-prod-key',
|
|
62
|
-
priority: 10,
|
|
63
|
-
matchProvider: 'openai',
|
|
64
|
-
matchResourceKind: 'shared',
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
id: 'rule-anthropic-personal',
|
|
68
|
-
credentialRef: 'anthropic-user-token',
|
|
69
|
-
priority: 20,
|
|
70
|
-
matchProvider: 'anthropic',
|
|
71
|
-
matchResourceKind: 'personal',
|
|
72
|
-
},
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
const EXPIRED_RULES: RoutingRule[] = [
|
|
76
|
-
{
|
|
77
|
-
id: 'rule-expired',
|
|
78
|
-
credentialRef: 'expired-key',
|
|
79
|
-
priority: 5,
|
|
80
|
-
matchProvider: 'openai',
|
|
81
|
-
matchResourceKind: 'shared',
|
|
82
|
-
},
|
|
83
|
-
];
|
|
84
|
-
|
|
85
|
-
const BASE_CONTEXT = {
|
|
86
|
-
userId: 'user-123',
|
|
87
|
-
resourceId: 'res-abc',
|
|
88
|
-
resourceKind: 'shared' as const,
|
|
89
|
-
provider: 'openai' as const,
|
|
90
|
-
model: 'gpt-4',
|
|
91
|
-
action: 'complete',
|
|
92
|
-
traceId: 'trace-001',
|
|
93
|
-
requestedAt: new Date().toISOString(),
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
// ─── Mock Fastify instance helpers ────────────────────────────────────────────
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Builds a minimal mock Fastify instance and calls the plugin's inner
|
|
100
|
-
* function (the one wrapped by fastify-plugin) to capture the addHook
|
|
101
|
-
* preHandler call. Returns the captured preHandler function so tests
|
|
102
|
-
* can invoke it directly without running a real Fastify server.
|
|
103
|
-
*
|
|
104
|
-
* fastify-plugin sets [Symbol.for('skip-override')] = true and exposes
|
|
105
|
-
* the original plugin via .default or the function itself — we access
|
|
106
|
-
* the unwrapped function by checking for [Symbol.for('fastify.display-name')]
|
|
107
|
-
* or falling back to calling the exported plugin directly.
|
|
108
|
-
*/
|
|
109
|
-
async function extractPreHandler(
|
|
110
|
-
options: Parameters<typeof agentIdentityPlugin>[1]
|
|
111
|
-
): Promise<(request: Record<string, unknown>, reply: ReturnType<typeof makeReply>) => Promise<void>> {
|
|
112
|
-
let capturedHook: ((req: unknown, reply: unknown) => Promise<void>) | null = null;
|
|
113
|
-
|
|
114
|
-
const mockFastify = {
|
|
115
|
-
decorateRequest: vi.fn(),
|
|
116
|
-
addHook: vi.fn((_hookName: string, fn: (req: unknown, reply: unknown) => Promise<void>) => {
|
|
117
|
-
capturedHook = fn;
|
|
118
|
-
}),
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
// agentIdentityPlugin is the fp()-wrapped plugin. fp() sets skip-override
|
|
122
|
-
// and the wrapped function is stored on .default or exposed through the
|
|
123
|
-
// Symbol-keyed property. We can call it directly — fp() returns a function
|
|
124
|
-
// that accepts (fastify, options) just like the inner plugin, but also
|
|
125
|
-
// skips Fastify's encapsulation. When called as a plain function it still
|
|
126
|
-
// invokes the inner plugin body.
|
|
127
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
128
|
-
await (agentIdentityPlugin as any)(mockFastify, options);
|
|
129
|
-
|
|
130
|
-
if (!capturedHook) {
|
|
131
|
-
throw new Error('addHook(preHandler) was not called by agentIdentityPlugin');
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return capturedHook as (req: Record<string, unknown>, reply: ReturnType<typeof makeReply>) => Promise<void>;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function makeRequest(body?: Record<string, unknown>) {
|
|
138
|
-
return { body, resolvedCredential: null } as Record<string, unknown>;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function makeReply() {
|
|
142
|
-
const send = vi.fn();
|
|
143
|
-
const status = vi.fn(() => ({ send }));
|
|
144
|
-
return { status, send };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
148
|
-
|
|
149
|
-
describe('agentIdentityPlugin', () => {
|
|
150
|
-
|
|
151
|
-
// ─── Plugin registration ───────────────────────────────────────────────────
|
|
152
|
-
|
|
153
|
-
describe('plugin registration', () => {
|
|
154
|
-
it('is a fastify-plugin (has skip-override symbol set by fp())', () => {
|
|
155
|
-
// fastify-plugin sets this symbol to true so Fastify does not create
|
|
156
|
-
// a child scope — verifies fp() wrapping is in place
|
|
157
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
158
|
-
expect((agentIdentityPlugin as any)[Symbol.for('skip-override')]).toBe(true);
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
// ─── passThrough behavior ──────────────────────────────────────────────────
|
|
163
|
-
|
|
164
|
-
describe('passThrough behavior', () => {
|
|
165
|
-
it('does not call reply when agentContext is absent and passThrough=true (default)', async () => {
|
|
166
|
-
const hook = await extractPreHandler({
|
|
167
|
-
credentials: [FIXED_CREDENTIAL],
|
|
168
|
-
rules: RULES,
|
|
169
|
-
});
|
|
170
|
-
const req = makeRequest({ otherField: 'value' });
|
|
171
|
-
const reply = makeReply();
|
|
172
|
-
|
|
173
|
-
await hook(req, reply);
|
|
174
|
-
|
|
175
|
-
expect(reply.status).not.toHaveBeenCalled();
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('does not call reply when req.body is undefined and passThrough=true', async () => {
|
|
179
|
-
const hook = await extractPreHandler({
|
|
180
|
-
credentials: [FIXED_CREDENTIAL],
|
|
181
|
-
rules: RULES,
|
|
182
|
-
});
|
|
183
|
-
const req = makeRequest(undefined);
|
|
184
|
-
const reply = makeReply();
|
|
185
|
-
|
|
186
|
-
await hook(req, reply);
|
|
187
|
-
|
|
188
|
-
expect(reply.status).not.toHaveBeenCalled();
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('sends 400 when agentContext is absent and passThrough=false', async () => {
|
|
192
|
-
const hook = await extractPreHandler({
|
|
193
|
-
credentials: [FIXED_CREDENTIAL],
|
|
194
|
-
rules: RULES,
|
|
195
|
-
passThrough: false,
|
|
196
|
-
});
|
|
197
|
-
const req = makeRequest({});
|
|
198
|
-
const reply = makeReply();
|
|
199
|
-
|
|
200
|
-
await hook(req, reply);
|
|
201
|
-
|
|
202
|
-
expect(reply.status).toHaveBeenCalledWith(400);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it('400 error message names the missing contextKey', async () => {
|
|
206
|
-
const hook = await extractPreHandler({
|
|
207
|
-
credentials: [FIXED_CREDENTIAL],
|
|
208
|
-
rules: RULES,
|
|
209
|
-
passThrough: false,
|
|
210
|
-
});
|
|
211
|
-
const req = makeRequest({});
|
|
212
|
-
const reply = makeReply();
|
|
213
|
-
|
|
214
|
-
await hook(req, reply);
|
|
215
|
-
|
|
216
|
-
expect(reply.send).toHaveBeenCalledWith(
|
|
217
|
-
expect.objectContaining({ error: expect.stringContaining('agentContext') })
|
|
218
|
-
);
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
// ─── Credential resolution ─────────────────────────────────────────────────
|
|
223
|
-
|
|
224
|
-
describe('credential resolution', () => {
|
|
225
|
-
it('attaches resolvedCredential to request on successful resolution', async () => {
|
|
226
|
-
const hook = await extractPreHandler({
|
|
227
|
-
credentials: [FIXED_CREDENTIAL],
|
|
228
|
-
rules: RULES,
|
|
229
|
-
});
|
|
230
|
-
const req = makeRequest({ agentContext: BASE_CONTEXT });
|
|
231
|
-
const reply = makeReply();
|
|
232
|
-
|
|
233
|
-
await hook(req, reply);
|
|
234
|
-
|
|
235
|
-
expect((req as Record<string, unknown>).resolvedCredential).toBeDefined();
|
|
236
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
237
|
-
expect(((req as any).resolvedCredential as any)?.credentialId).toBe('cred-openai-fixed');
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it('sets resolvedFor to "service" for fixed credentials', async () => {
|
|
241
|
-
const hook = await extractPreHandler({
|
|
242
|
-
credentials: [FIXED_CREDENTIAL],
|
|
243
|
-
rules: RULES,
|
|
244
|
-
});
|
|
245
|
-
const req = makeRequest({ agentContext: BASE_CONTEXT });
|
|
246
|
-
const reply = makeReply();
|
|
247
|
-
|
|
248
|
-
await hook(req, reply);
|
|
249
|
-
|
|
250
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
251
|
-
const resolved = (req as any).resolvedCredential as any;
|
|
252
|
-
expect(resolved?.kind).toBe('fixed');
|
|
253
|
-
expect(resolved?.resolvedFor).toBe('service');
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it('sets resolvedFor to ctx.userId for user-delegated credentials', async () => {
|
|
257
|
-
const hook = await extractPreHandler({
|
|
258
|
-
credentials: [FIXED_CREDENTIAL, USER_DELEGATED_CREDENTIAL],
|
|
259
|
-
rules: RULES,
|
|
260
|
-
});
|
|
261
|
-
const anthropicCtx = {
|
|
262
|
-
...BASE_CONTEXT,
|
|
263
|
-
provider: 'anthropic' as const,
|
|
264
|
-
resourceKind: 'personal' as const,
|
|
265
|
-
};
|
|
266
|
-
const req = makeRequest({ agentContext: anthropicCtx });
|
|
267
|
-
const reply = makeReply();
|
|
268
|
-
|
|
269
|
-
await hook(req, reply);
|
|
270
|
-
|
|
271
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
272
|
-
const resolved = (req as any).resolvedCredential as any;
|
|
273
|
-
expect(resolved?.kind).toBe('user-delegated');
|
|
274
|
-
expect(resolved?.resolvedFor).toBe('user-123');
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it('sends 403 when no routing rule matches the context', async () => {
|
|
278
|
-
const hook = await extractPreHandler({
|
|
279
|
-
credentials: [FIXED_CREDENTIAL],
|
|
280
|
-
rules: RULES,
|
|
281
|
-
});
|
|
282
|
-
// gemini matches no configured rule
|
|
283
|
-
const ctx = { ...BASE_CONTEXT, provider: 'gemini' as const };
|
|
284
|
-
const req = makeRequest({ agentContext: ctx });
|
|
285
|
-
const reply = makeReply();
|
|
286
|
-
|
|
287
|
-
await hook(req, reply);
|
|
288
|
-
|
|
289
|
-
expect(reply.status).toHaveBeenCalledWith(403);
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it('sends 403 when the matched credential is expired', async () => {
|
|
293
|
-
const hook = await extractPreHandler({
|
|
294
|
-
credentials: [EXPIRED_CREDENTIAL],
|
|
295
|
-
rules: EXPIRED_RULES,
|
|
296
|
-
});
|
|
297
|
-
const req = makeRequest({ agentContext: BASE_CONTEXT });
|
|
298
|
-
const reply = makeReply();
|
|
299
|
-
|
|
300
|
-
await hook(req, reply);
|
|
301
|
-
|
|
302
|
-
expect(reply.status).toHaveBeenCalledWith(403);
|
|
303
|
-
});
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
// ─── Custom contextKey ─────────────────────────────────────────────────────
|
|
307
|
-
|
|
308
|
-
describe('custom contextKey', () => {
|
|
309
|
-
it('reads the agent context from the custom contextKey field', async () => {
|
|
310
|
-
const hook = await extractPreHandler({
|
|
311
|
-
credentials: [FIXED_CREDENTIAL],
|
|
312
|
-
rules: RULES,
|
|
313
|
-
contextKey: 'identity',
|
|
314
|
-
});
|
|
315
|
-
const req = makeRequest({ identity: BASE_CONTEXT });
|
|
316
|
-
const reply = makeReply();
|
|
317
|
-
|
|
318
|
-
await hook(req, reply);
|
|
319
|
-
|
|
320
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
321
|
-
expect((req as any).resolvedCredential).toBeDefined();
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it('400 error message names the custom contextKey when passThrough=false', async () => {
|
|
325
|
-
const hook = await extractPreHandler({
|
|
326
|
-
credentials: [FIXED_CREDENTIAL],
|
|
327
|
-
rules: RULES,
|
|
328
|
-
contextKey: 'identity',
|
|
329
|
-
passThrough: false,
|
|
330
|
-
});
|
|
331
|
-
const req = makeRequest({});
|
|
332
|
-
const reply = makeReply();
|
|
333
|
-
|
|
334
|
-
await hook(req, reply);
|
|
335
|
-
|
|
336
|
-
expect(reply.status).toHaveBeenCalledWith(400);
|
|
337
|
-
expect(reply.send).toHaveBeenCalledWith(
|
|
338
|
-
expect.objectContaining({ error: expect.stringContaining('identity') })
|
|
339
|
-
);
|
|
340
|
-
});
|
|
341
|
-
});
|
|
342
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fastify plugin for @datacules/agent-identity.
|
|
3
|
-
*
|
|
4
|
-
* Decorates each request with resolvedCredential before the route handler runs.
|
|
5
|
-
*
|
|
6
|
-
* Usage:
|
|
7
|
-
* import Fastify from 'fastify';
|
|
8
|
-
* import { agentIdentityPlugin } from '@datacules/agent-identity-fastify';
|
|
9
|
-
*
|
|
10
|
-
* const app = Fastify();
|
|
11
|
-
* await app.register(agentIdentityPlugin, { credentials, rules, logger });
|
|
12
|
-
*
|
|
13
|
-
* app.post('/ai/complete', async (request, reply) => {
|
|
14
|
-
* const cred = request.resolvedCredential; // typed
|
|
15
|
-
* });
|
|
16
|
-
*/
|
|
17
|
-
import fp from 'fastify-plugin';
|
|
18
|
-
import { createRouter } from '@datacules/agent-identity';
|
|
19
|
-
import type {
|
|
20
|
-
AgentRequestContext,
|
|
21
|
-
AuditLogger,
|
|
22
|
-
Credential,
|
|
23
|
-
ResolvedCredential,
|
|
24
|
-
RoutingRule,
|
|
25
|
-
} from '@datacules/agent-identity';
|
|
26
|
-
import type { FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify';
|
|
27
|
-
|
|
28
|
-
declare module 'fastify' {
|
|
29
|
-
interface FastifyRequest {
|
|
30
|
-
resolvedCredential: ResolvedCredential | null;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface AgentIdentityPluginOptions {
|
|
35
|
-
credentials: Credential[];
|
|
36
|
-
rules: RoutingRule[];
|
|
37
|
-
logger?: AuditLogger;
|
|
38
|
-
contextKey?: string;
|
|
39
|
-
passThrough?: boolean;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const plugin: FastifyPluginAsync<AgentIdentityPluginOptions> = async (fastify, options) => {
|
|
43
|
-
const {
|
|
44
|
-
credentials,
|
|
45
|
-
rules,
|
|
46
|
-
logger,
|
|
47
|
-
contextKey = 'agentContext',
|
|
48
|
-
passThrough = true,
|
|
49
|
-
} = options;
|
|
50
|
-
|
|
51
|
-
const router = createRouter(credentials, rules, logger);
|
|
52
|
-
|
|
53
|
-
fastify.decorateRequest('resolvedCredential', null);
|
|
54
|
-
|
|
55
|
-
fastify.addHook(
|
|
56
|
-
'preHandler',
|
|
57
|
-
async (request: FastifyRequest, reply: FastifyReply) => {
|
|
58
|
-
const body = request.body as Record<string, unknown> | null;
|
|
59
|
-
const ctx = body?.[contextKey] as AgentRequestContext | undefined;
|
|
60
|
-
|
|
61
|
-
if (!ctx) {
|
|
62
|
-
if (!passThrough) {
|
|
63
|
-
return reply.status(400).send({ error: `Missing required field: ${contextKey}` });
|
|
64
|
-
}
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const resolved = router.resolve(ctx);
|
|
69
|
-
if (!resolved) {
|
|
70
|
-
return reply.status(403).send({ error: 'No credential resolved for this context' });
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
request.resolvedCredential = resolved;
|
|
74
|
-
}
|
|
75
|
-
);
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
export const agentIdentityPlugin = fp(plugin, {
|
|
79
|
-
name: '@datacules/agent-identity-fastify',
|
|
80
|
-
fastify: '>=4.0.0',
|
|
81
|
-
});
|