@slashfi/agents-sdk 0.16.0 → 0.18.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/agent-definitions/auth.d.ts.map +1 -1
- package/dist/agent-definitions/auth.js +44 -11
- package/dist/agent-definitions/auth.js.map +1 -1
- package/dist/agent-definitions/integrations.d.ts.map +1 -1
- package/dist/agent-definitions/integrations.js +106 -45
- package/dist/agent-definitions/integrations.js.map +1 -1
- package/dist/agent-definitions/remote-registry.d.ts.map +1 -1
- package/dist/agent-definitions/remote-registry.js +174 -45
- package/dist/agent-definitions/remote-registry.js.map +1 -1
- package/dist/agent-definitions/secrets.d.ts.map +1 -1
- package/dist/agent-definitions/secrets.js +1 -4
- package/dist/agent-definitions/secrets.js.map +1 -1
- package/dist/agent-definitions/users.d.ts.map +1 -1
- package/dist/agent-definitions/users.js +14 -3
- package/dist/agent-definitions/users.js.map +1 -1
- package/dist/define-config.d.ts +125 -0
- package/dist/define-config.d.ts.map +1 -0
- package/dist/define-config.js +75 -0
- package/dist/define-config.js.map +1 -0
- package/dist/define.d.ts +11 -2
- package/dist/define.d.ts.map +1 -1
- package/dist/define.js +57 -26
- package/dist/define.js.map +1 -1
- package/dist/events.d.ts +133 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +57 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +16 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -3
- package/dist/index.js.map +1 -1
- package/dist/integration-interface.d.ts +3 -3
- package/dist/integration-interface.d.ts.map +1 -1
- package/dist/integration-interface.js +29 -21
- package/dist/integration-interface.js.map +1 -1
- package/dist/integrations-store.d.ts +2 -2
- package/dist/integrations-store.d.ts.map +1 -1
- package/dist/integrations-store.js +3 -3
- package/dist/integrations-store.js.map +1 -1
- package/dist/jwt.d.ts.map +1 -1
- package/dist/jwt.js +7 -5
- package/dist/jwt.js.map +1 -1
- package/dist/key-manager.d.ts.map +1 -1
- package/dist/key-manager.js +5 -3
- package/dist/key-manager.js.map +1 -1
- package/dist/oidc-signin.d.ts +32 -0
- package/dist/oidc-signin.d.ts.map +1 -0
- package/dist/oidc-signin.js +138 -0
- package/dist/oidc-signin.js.map +1 -0
- package/dist/registry-consumer.d.ts +104 -0
- package/dist/registry-consumer.d.ts.map +1 -0
- package/dist/registry-consumer.js +230 -0
- package/dist/registry-consumer.js.map +1 -0
- package/dist/registry.d.ts +5 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +76 -4
- package/dist/registry.js.map +1 -1
- package/dist/secret-collection.d.ts.map +1 -1
- package/dist/secret-collection.js.map +1 -1
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +222 -27
- package/dist/server.js.map +1 -1
- package/dist/test-utils/mock-oidc-server.d.ts +36 -0
- package/dist/test-utils/mock-oidc-server.d.ts.map +1 -0
- package/dist/test-utils/mock-oidc-server.js +96 -0
- package/dist/test-utils/mock-oidc-server.js.map +1 -0
- package/dist/types.d.ts +106 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/agent-definitions/auth.ts +106 -38
- package/src/agent-definitions/integrations.ts +201 -73
- package/src/agent-definitions/remote-registry.ts +262 -65
- package/src/agent-definitions/secrets.ts +22 -8
- package/src/agent-definitions/users.ts +16 -4
- package/src/cli.ts +293 -0
- package/src/codegen.test.ts +527 -0
- package/src/codegen.ts +1348 -0
- package/src/consumer.test.ts +536 -0
- package/src/define-config.ts +205 -0
- package/src/define.ts +134 -46
- package/src/events.ts +237 -0
- package/src/index.ts +107 -8
- package/src/integration-interface.ts +52 -28
- package/src/integrations-store.ts +9 -5
- package/src/jwt.ts +48 -19
- package/src/key-manager.test.ts +22 -13
- package/src/key-manager.ts +8 -10
- package/src/oidc-signin.ts +223 -0
- package/src/registry-consumer.ts +413 -0
- package/src/registry.ts +115 -9
- package/src/secret-collection.ts +2 -1
- package/src/server.test.ts +304 -238
- package/src/server.ts +371 -69
- package/src/test-utils/mock-oidc-server.ts +123 -0
- package/src/types.ts +172 -18
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock OIDC Provider for testing.
|
|
3
|
+
*
|
|
4
|
+
* A minimal OpenID Connect provider that runs locally.
|
|
5
|
+
* Supports the authorization code flow with no user interaction.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const server = await startMockOIDC({ port: 0 });
|
|
9
|
+
* // server.url = 'http://localhost:XXXXX'
|
|
10
|
+
* // server.issuer = server.url
|
|
11
|
+
* // Configure your app to use server.url as the OIDC provider
|
|
12
|
+
* await server.stop();
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const TEST_USER = {
|
|
16
|
+
sub: "test-user-001",
|
|
17
|
+
email: "test@example.com",
|
|
18
|
+
name: "Test User",
|
|
19
|
+
picture: "https://example.com/avatar.png",
|
|
20
|
+
"https://slack.com/team_id": "T_TEST",
|
|
21
|
+
"https://slack.com/team_name": "Test Workspace",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const TEST_CLIENT_ID = "test-client-id";
|
|
25
|
+
const TEST_CLIENT_SECRET = "test-client-secret";
|
|
26
|
+
const VALID_CODE = "test-auth-code-12345";
|
|
27
|
+
const ACCESS_TOKEN = `test-access-token-${Date.now()}`;
|
|
28
|
+
|
|
29
|
+
export interface MockOIDCServer {
|
|
30
|
+
url: string;
|
|
31
|
+
port: number;
|
|
32
|
+
issuer: string;
|
|
33
|
+
clientId: string;
|
|
34
|
+
clientSecret: string;
|
|
35
|
+
testUser: typeof TEST_USER;
|
|
36
|
+
accessToken: string;
|
|
37
|
+
stop: () => Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function startMockOIDC(
|
|
41
|
+
opts: { port?: number } = {},
|
|
42
|
+
): Promise<MockOIDCServer> {
|
|
43
|
+
const server: ReturnType<typeof Bun.serve> = Bun.serve({
|
|
44
|
+
port: opts.port ?? 0,
|
|
45
|
+
fetch(req): Response | Promise<Response> {
|
|
46
|
+
const url = new URL(req.url);
|
|
47
|
+
|
|
48
|
+
// Discovery
|
|
49
|
+
if (url.pathname === "/.well-known/openid-configuration") {
|
|
50
|
+
const base: string = `http://localhost:${server.port}`;
|
|
51
|
+
return Response.json({
|
|
52
|
+
issuer: base,
|
|
53
|
+
authorization_endpoint: `${base}/authorize`,
|
|
54
|
+
token_endpoint: `${base}/token`,
|
|
55
|
+
userinfo_endpoint: `${base}/userinfo`,
|
|
56
|
+
jwks_uri: `${base}/jwks`,
|
|
57
|
+
response_types_supported: ["code"],
|
|
58
|
+
subject_types_supported: ["public"],
|
|
59
|
+
id_token_signing_alg_values_supported: ["RS256"],
|
|
60
|
+
scopes_supported: ["openid", "email", "profile"],
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Authorize - immediately redirect back with code
|
|
65
|
+
if (url.pathname === "/authorize") {
|
|
66
|
+
const redirectUri = url.searchParams.get("redirect_uri")!;
|
|
67
|
+
const state = url.searchParams.get("state") ?? "";
|
|
68
|
+
const sep = redirectUri.includes("?") ? "&" : "?";
|
|
69
|
+
return Response.redirect(
|
|
70
|
+
`${redirectUri}${sep}code=${VALID_CODE}&state=${state}`,
|
|
71
|
+
302,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Token exchange
|
|
76
|
+
if (url.pathname === "/token" && req.method === "POST") {
|
|
77
|
+
return Response.json({
|
|
78
|
+
access_token: ACCESS_TOKEN,
|
|
79
|
+
token_type: "bearer",
|
|
80
|
+
expires_in: 3600,
|
|
81
|
+
scope: "openid email profile",
|
|
82
|
+
id_token: "fake.id.token",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// UserInfo
|
|
87
|
+
if (url.pathname === "/userinfo") {
|
|
88
|
+
const auth = req.headers.get("authorization");
|
|
89
|
+
if (auth !== `Bearer ${ACCESS_TOKEN}`) {
|
|
90
|
+
return new Response("Unauthorized", { status: 401 });
|
|
91
|
+
}
|
|
92
|
+
return Response.json({ ok: true, ...TEST_USER });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Also support Slack-style openid.connect.userInfo
|
|
96
|
+
if (url.pathname === "/api/openid.connect.userInfo") {
|
|
97
|
+
return Response.json({ ok: true, ...TEST_USER });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// JWKS (empty - we don't validate JWTs in tests)
|
|
101
|
+
if (url.pathname === "/jwks") {
|
|
102
|
+
return Response.json({ keys: [] });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return new Response("Not found", { status: 404 });
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const base = `http://localhost:${server.port}`;
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
url: base,
|
|
113
|
+
port: server.port as number,
|
|
114
|
+
issuer: base,
|
|
115
|
+
clientId: TEST_CLIENT_ID,
|
|
116
|
+
clientSecret: TEST_CLIENT_SECRET,
|
|
117
|
+
testUser: TEST_USER,
|
|
118
|
+
accessToken: ACCESS_TOKEN,
|
|
119
|
+
stop: async () => {
|
|
120
|
+
server.stop();
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
* Defines the fundamental types for agent definitions, tools, and contexts.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import type { EventCallback, EventType } from "./events.js";
|
|
8
|
+
|
|
9
|
+
/** Internal listener entry stored on agents/tools */
|
|
10
|
+
export interface ListenerEntry {
|
|
11
|
+
eventType: EventType;
|
|
12
|
+
callback: EventCallback<EventType>;
|
|
13
|
+
toolScope?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
// ============================================
|
|
8
17
|
// JSON Schema
|
|
9
18
|
// ============================================
|
|
@@ -28,7 +37,6 @@ export type JsonSchema = {
|
|
|
28
37
|
[key: string]: unknown;
|
|
29
38
|
};
|
|
30
39
|
|
|
31
|
-
|
|
32
40
|
// ============================================
|
|
33
41
|
// Integration Config
|
|
34
42
|
// ============================================
|
|
@@ -67,19 +75,32 @@ export interface IntegrationMethodContext extends ToolContext {
|
|
|
67
75
|
*/
|
|
68
76
|
export interface IntegrationMethods {
|
|
69
77
|
/** Configure/initialize the integration (e.g., add a DB connection, set API key) */
|
|
70
|
-
setup(
|
|
78
|
+
setup(
|
|
79
|
+
params: Record<string, unknown>,
|
|
80
|
+
ctx: IntegrationMethodContext,
|
|
81
|
+
): Promise<IntegrationMethodResult>;
|
|
71
82
|
/** List configured instances (e.g., list DB connections, list repos) */
|
|
72
|
-
list(
|
|
83
|
+
list(
|
|
84
|
+
params: Record<string, unknown>,
|
|
85
|
+
ctx: IntegrationMethodContext,
|
|
86
|
+
): Promise<IntegrationMethodResult>;
|
|
73
87
|
/** Establish connection or authenticate (e.g., test DB connectivity, OAuth flow) */
|
|
74
|
-
connect(
|
|
88
|
+
connect(
|
|
89
|
+
params: Record<string, unknown>,
|
|
90
|
+
ctx: IntegrationMethodContext,
|
|
91
|
+
): Promise<IntegrationMethodResult>;
|
|
75
92
|
/** Get details of a specific instance */
|
|
76
|
-
get(
|
|
93
|
+
get(
|
|
94
|
+
params: Record<string, unknown>,
|
|
95
|
+
ctx: IntegrationMethodContext,
|
|
96
|
+
): Promise<IntegrationMethodResult>;
|
|
77
97
|
/** Modify an existing configuration */
|
|
78
|
-
update(
|
|
98
|
+
update(
|
|
99
|
+
params: Record<string, unknown>,
|
|
100
|
+
ctx: IntegrationMethodContext,
|
|
101
|
+
): Promise<IntegrationMethodResult>;
|
|
79
102
|
}
|
|
80
103
|
|
|
81
|
-
|
|
82
|
-
|
|
83
104
|
/** Hooks for agents that implement the integrations interface. */
|
|
84
105
|
export interface IntegrationHooks {
|
|
85
106
|
/** Provider metadata */
|
|
@@ -90,17 +111,35 @@ export interface IntegrationHooks {
|
|
|
90
111
|
description?: string;
|
|
91
112
|
|
|
92
113
|
/** Set up this integration (discover, configure, establish trust) */
|
|
93
|
-
setup?(
|
|
114
|
+
setup?(
|
|
115
|
+
params: Record<string, unknown>,
|
|
116
|
+
ctx: ToolContext,
|
|
117
|
+
): Promise<IntegrationMethodResult>;
|
|
94
118
|
/** Connect a user to this integration (OAuth, identity linking) */
|
|
95
|
-
connect?(
|
|
119
|
+
connect?(
|
|
120
|
+
params: Record<string, unknown>,
|
|
121
|
+
ctx: ToolContext,
|
|
122
|
+
): Promise<IntegrationMethodResult>;
|
|
96
123
|
/** Discover available instances of this integration */
|
|
97
|
-
discover?(
|
|
124
|
+
discover?(
|
|
125
|
+
params: Record<string, unknown>,
|
|
126
|
+
ctx: ToolContext,
|
|
127
|
+
): Promise<IntegrationMethodResult>;
|
|
98
128
|
/** List connected instances */
|
|
99
|
-
list?(
|
|
129
|
+
list?(
|
|
130
|
+
params: Record<string, unknown>,
|
|
131
|
+
ctx: ToolContext,
|
|
132
|
+
): Promise<IntegrationMethodResult>;
|
|
100
133
|
/** Get details of a specific instance */
|
|
101
|
-
get?(
|
|
134
|
+
get?(
|
|
135
|
+
params: Record<string, unknown>,
|
|
136
|
+
ctx: ToolContext,
|
|
137
|
+
): Promise<IntegrationMethodResult>;
|
|
102
138
|
/** Update an existing instance config */
|
|
103
|
-
update?(
|
|
139
|
+
update?(
|
|
140
|
+
params: Record<string, unknown>,
|
|
141
|
+
ctx: ToolContext,
|
|
142
|
+
): Promise<IntegrationMethodResult>;
|
|
104
143
|
}
|
|
105
144
|
export interface IntegrationConfig {
|
|
106
145
|
/** Provider identifier (e.g., "databases", "slack", "github") */
|
|
@@ -125,6 +164,100 @@ export interface IntegrationConfig {
|
|
|
125
164
|
connectSchema?: Record<string, unknown>;
|
|
126
165
|
}
|
|
127
166
|
|
|
167
|
+
// ============================================
|
|
168
|
+
// Security Scheme
|
|
169
|
+
// ============================================
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* OAuth 2.0 Authorization Code flow configuration.
|
|
173
|
+
* Used by agents that wrap APIs requiring user-authorized access
|
|
174
|
+
* (e.g. Notion, Slack, GitHub, Linear, Google).
|
|
175
|
+
*/
|
|
176
|
+
export interface OAuth2SecurityScheme {
|
|
177
|
+
type: "oauth2";
|
|
178
|
+
flows: {
|
|
179
|
+
authorizationCode: {
|
|
180
|
+
/** URL to redirect users for authorization */
|
|
181
|
+
authorizationUrl: string;
|
|
182
|
+
/** URL to exchange authorization code for tokens */
|
|
183
|
+
tokenUrl: string;
|
|
184
|
+
/** URL for token refresh (defaults to tokenUrl) */
|
|
185
|
+
refreshUrl?: string;
|
|
186
|
+
/** Available scopes: key = scope name, value = description */
|
|
187
|
+
scopes?: Record<string, string>;
|
|
188
|
+
/** How client credentials are sent */
|
|
189
|
+
clientAuth?: "client_secret_post" | "client_secret_basic";
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* API key authentication.
|
|
196
|
+
* Used by agents that wrap APIs using a single key
|
|
197
|
+
* (e.g. OpenAI, Anthropic, Stripe, Datadog).
|
|
198
|
+
*/
|
|
199
|
+
export interface ApiKeySecurityScheme {
|
|
200
|
+
type: "apiKey";
|
|
201
|
+
/** Where the key is sent */
|
|
202
|
+
in: "header" | "query";
|
|
203
|
+
/** Header or query parameter name (e.g. "X-API-Key", "Authorization") */
|
|
204
|
+
name: string;
|
|
205
|
+
/** Optional prefix (e.g. "Bearer" for Authorization header) */
|
|
206
|
+
prefix?: string;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* HTTP authentication (Bearer token, Basic auth).
|
|
211
|
+
*/
|
|
212
|
+
export interface HttpSecurityScheme {
|
|
213
|
+
type: "http";
|
|
214
|
+
scheme: "bearer" | "basic";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* No authentication required.
|
|
219
|
+
*/
|
|
220
|
+
export interface NoneSecurityScheme {
|
|
221
|
+
type: "none";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Security scheme for an agent — describes what authentication the
|
|
226
|
+
* agent's target API requires from consumers.
|
|
227
|
+
*
|
|
228
|
+
* Borrowed from OpenAPI Security Scheme Object, simplified for agents.
|
|
229
|
+
* The system uses this to:
|
|
230
|
+
* - Know what credentials a tenant admin needs to provide (OAuth app creds)
|
|
231
|
+
* - Know what flow a user needs to complete (OAuth exchange)
|
|
232
|
+
* - Know how to send credentials when calling the API
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* ```typescript
|
|
236
|
+
* // OAuth 2.0 (Notion, Slack, GitHub)
|
|
237
|
+
* security: {
|
|
238
|
+
* type: 'oauth2',
|
|
239
|
+
* flows: {
|
|
240
|
+
* authorizationCode: {
|
|
241
|
+
* authorizationUrl: 'https://api.notion.com/v1/oauth/authorize',
|
|
242
|
+
* tokenUrl: 'https://api.notion.com/v1/oauth/token',
|
|
243
|
+
* clientAuth: 'client_secret_basic',
|
|
244
|
+
* }
|
|
245
|
+
* }
|
|
246
|
+
* }
|
|
247
|
+
*
|
|
248
|
+
* // API Key (OpenAI, Stripe)
|
|
249
|
+
* security: { type: 'apiKey', in: 'header', name: 'Authorization', prefix: 'Bearer' }
|
|
250
|
+
*
|
|
251
|
+
* // No auth (public API)
|
|
252
|
+
* security: { type: 'none' }
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
export type SecurityScheme =
|
|
256
|
+
| OAuth2SecurityScheme
|
|
257
|
+
| ApiKeySecurityScheme
|
|
258
|
+
| HttpSecurityScheme
|
|
259
|
+
| NoneSecurityScheme;
|
|
260
|
+
|
|
128
261
|
// ============================================
|
|
129
262
|
// Agent Configuration
|
|
130
263
|
// ============================================
|
|
@@ -163,11 +296,11 @@ export interface AgentConfig {
|
|
|
163
296
|
modelParams?: {
|
|
164
297
|
maxTokens?: number;
|
|
165
298
|
temperature?: number;
|
|
166
|
-
|
|
167
|
-
|
|
299
|
+
/** Agent refs (paths to other agents this agent can call) */
|
|
300
|
+
refs?: Record<string, { description?: string }>;
|
|
168
301
|
|
|
169
|
-
|
|
170
|
-
|
|
302
|
+
/** Tools to expose publicly (by name) */
|
|
303
|
+
public?: { tools?: string[] };
|
|
171
304
|
[key: string]: unknown;
|
|
172
305
|
};
|
|
173
306
|
|
|
@@ -178,6 +311,15 @@ export interface AgentConfig {
|
|
|
178
311
|
*/
|
|
179
312
|
integration?: IntegrationConfig;
|
|
180
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Security scheme — describes what authentication the agent's
|
|
316
|
+
* target API requires. Used by the registry to communicate
|
|
317
|
+
* credential requirements to consumers.
|
|
318
|
+
*
|
|
319
|
+
* @see SecurityScheme
|
|
320
|
+
*/
|
|
321
|
+
security?: SecurityScheme;
|
|
322
|
+
|
|
181
323
|
/** Additional configuration */
|
|
182
324
|
/** Agent refs (paths to other agents this agent can call) */
|
|
183
325
|
refs?: Record<string, { description?: string }>;
|
|
@@ -487,6 +629,12 @@ export interface ToolDefinition<
|
|
|
487
629
|
* When set, rendered directly into the agent's system prompt.
|
|
488
630
|
*/
|
|
489
631
|
doc?: string;
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Internal: event listeners registered via tool.on().
|
|
635
|
+
* Collected by the registry on registration.
|
|
636
|
+
*/
|
|
637
|
+
_listeners?: ListenerEntry[];
|
|
490
638
|
}
|
|
491
639
|
|
|
492
640
|
/**
|
|
@@ -542,6 +690,12 @@ export interface AgentDefinition<TContext extends ToolContext = ToolContext> {
|
|
|
542
690
|
* Called once to load runtime hooks exported from the agent's entrypoint module.
|
|
543
691
|
*/
|
|
544
692
|
loadListeners?: () => Promise<unknown>;
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Internal: event listeners registered via agent.on().
|
|
696
|
+
* Collected by the registry on registration.
|
|
697
|
+
*/
|
|
698
|
+
_listeners?: ListenerEntry[];
|
|
545
699
|
}
|
|
546
700
|
|
|
547
701
|
// ============================================
|