@ankhorage/flow 0.0.0-stage → 0.4.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.
Files changed (109) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/LICENSE +21 -0
  3. package/README.md +17 -2
  4. package/dist/ai.d.ts +7 -0
  5. package/dist/ai.d.ts.map +1 -0
  6. package/dist/ai.js +7 -0
  7. package/dist/ai.js.map +1 -0
  8. package/dist/features/execution/application/executeFlowAsync.d.ts +4 -0
  9. package/dist/features/execution/application/executeFlowAsync.d.ts.map +1 -0
  10. package/dist/features/execution/application/executeFlowAsync.js +211 -0
  11. package/dist/features/execution/application/executeFlowAsync.js.map +1 -0
  12. package/dist/features/execution/domain/assertFlowGraphSchemasSupported.d.ts +5 -0
  13. package/dist/features/execution/domain/assertFlowGraphSchemasSupported.d.ts.map +1 -0
  14. package/dist/features/execution/domain/assertFlowGraphSchemasSupported.js +11 -0
  15. package/dist/features/execution/domain/assertFlowGraphSchemasSupported.js.map +1 -0
  16. package/dist/features/execution/domain/compileFlowGraph.d.ts +5 -0
  17. package/dist/features/execution/domain/compileFlowGraph.d.ts.map +1 -0
  18. package/dist/features/execution/domain/compileFlowGraph.js +76 -0
  19. package/dist/features/execution/domain/compileFlowGraph.js.map +1 -0
  20. package/dist/features/execution/domain/createDefaultNodeRegistry.d.ts +4 -0
  21. package/dist/features/execution/domain/createDefaultNodeRegistry.d.ts.map +1 -0
  22. package/dist/features/execution/domain/createDefaultNodeRegistry.js +15 -0
  23. package/dist/features/execution/domain/createDefaultNodeRegistry.js.map +1 -0
  24. package/dist/features/execution/domain/validateFlowDataSchema.d.ts +6 -0
  25. package/dist/features/execution/domain/validateFlowDataSchema.d.ts.map +1 -0
  26. package/dist/features/execution/domain/validateFlowDataSchema.js +144 -0
  27. package/dist/features/execution/domain/validateFlowDataSchema.js.map +1 -0
  28. package/dist/features/mcp/McpToolError.d.ts +6 -0
  29. package/dist/features/mcp/McpToolError.d.ts.map +1 -0
  30. package/dist/features/mcp/McpToolError.js +10 -0
  31. package/dist/features/mcp/McpToolError.js.map +1 -0
  32. package/dist/features/mcp/createMcpRemoteTransport.d.ts +8 -0
  33. package/dist/features/mcp/createMcpRemoteTransport.d.ts.map +1 -0
  34. package/dist/features/mcp/createMcpRemoteTransport.js +23 -0
  35. package/dist/features/mcp/createMcpRemoteTransport.js.map +1 -0
  36. package/dist/features/mcp/createMcpToolRegistry.d.ts +10 -0
  37. package/dist/features/mcp/createMcpToolRegistry.d.ts.map +1 -0
  38. package/dist/features/mcp/createMcpToolRegistry.js +185 -0
  39. package/dist/features/mcp/createMcpToolRegistry.js.map +1 -0
  40. package/dist/features/mcp/loadMcpSdkAsync.d.ts +12 -0
  41. package/dist/features/mcp/loadMcpSdkAsync.d.ts.map +1 -0
  42. package/dist/features/mcp/loadMcpSdkAsync.js +20 -0
  43. package/dist/features/mcp/loadMcpSdkAsync.js.map +1 -0
  44. package/dist/features/mcp/prepareMcpProviders.d.ts +4 -0
  45. package/dist/features/mcp/prepareMcpProviders.d.ts.map +1 -0
  46. package/dist/features/mcp/prepareMcpProviders.js +97 -0
  47. package/dist/features/mcp/prepareMcpProviders.js.map +1 -0
  48. package/dist/features/mcp/validateMcpSchema.d.ts +6 -0
  49. package/dist/features/mcp/validateMcpSchema.d.ts.map +1 -0
  50. package/dist/features/mcp/validateMcpSchema.js +220 -0
  51. package/dist/features/mcp/validateMcpSchema.js.map +1 -0
  52. package/dist/features/security/FlowSecurityError.d.ts +6 -0
  53. package/dist/features/security/FlowSecurityError.d.ts.map +1 -0
  54. package/dist/features/security/FlowSecurityError.js +10 -0
  55. package/dist/features/security/FlowSecurityError.js.map +1 -0
  56. package/dist/features/security/application/createFlowSecurityPort.d.ts +12 -0
  57. package/dist/features/security/application/createFlowSecurityPort.d.ts.map +1 -0
  58. package/dist/features/security/application/createFlowSecurityPort.js +208 -0
  59. package/dist/features/security/application/createFlowSecurityPort.js.map +1 -0
  60. package/dist/features/security/domain/validateFlowSecurityTarget.d.ts +4 -0
  61. package/dist/features/security/domain/validateFlowSecurityTarget.d.ts.map +1 -0
  62. package/dist/features/security/domain/validateFlowSecurityTarget.js +57 -0
  63. package/dist/features/security/domain/validateFlowSecurityTarget.js.map +1 -0
  64. package/dist/index.d.ts +9 -0
  65. package/dist/index.d.ts.map +1 -0
  66. package/dist/index.js +7 -0
  67. package/dist/index.js.map +1 -0
  68. package/dist/mcp.d.ts +4 -0
  69. package/dist/mcp.d.ts.map +1 -0
  70. package/dist/mcp.js +3 -0
  71. package/dist/mcp.js.map +1 -0
  72. package/dist/social.d.ts +7 -0
  73. package/dist/social.d.ts.map +1 -0
  74. package/dist/social.js +7 -0
  75. package/dist/social.js.map +1 -0
  76. package/dist/types/execution.d.ts +33 -0
  77. package/dist/types/execution.d.ts.map +1 -0
  78. package/dist/types/execution.js +2 -0
  79. package/dist/types/execution.js.map +1 -0
  80. package/dist/types/mcp.d.ts +61 -0
  81. package/dist/types/mcp.d.ts.map +1 -0
  82. package/dist/types/mcp.js +2 -0
  83. package/dist/types/mcp.js.map +1 -0
  84. package/dist/types/security.d.ts +123 -0
  85. package/dist/types/security.d.ts.map +1 -0
  86. package/dist/types/security.js +2 -0
  87. package/dist/types/security.js.map +1 -0
  88. package/package.json +86 -4
  89. package/src/ai.ts +6 -0
  90. package/src/features/execution/application/executeFlowAsync.ts +338 -0
  91. package/src/features/execution/domain/assertFlowGraphSchemasSupported.ts +17 -0
  92. package/src/features/execution/domain/compileFlowGraph.ts +97 -0
  93. package/src/features/execution/domain/createDefaultNodeRegistry.ts +26 -0
  94. package/src/features/execution/domain/validateFlowDataSchema.ts +218 -0
  95. package/src/features/mcp/McpToolError.ts +10 -0
  96. package/src/features/mcp/createMcpRemoteTransport.ts +34 -0
  97. package/src/features/mcp/createMcpToolRegistry.ts +324 -0
  98. package/src/features/mcp/loadMcpSdkAsync.ts +32 -0
  99. package/src/features/mcp/prepareMcpProviders.ts +135 -0
  100. package/src/features/mcp/validateMcpSchema.ts +269 -0
  101. package/src/features/security/FlowSecurityError.ts +17 -0
  102. package/src/features/security/application/createFlowSecurityPort.ts +339 -0
  103. package/src/features/security/domain/validateFlowSecurityTarget.ts +73 -0
  104. package/src/index.ts +35 -0
  105. package/src/mcp.ts +11 -0
  106. package/src/social.ts +6 -0
  107. package/src/types/execution.ts +41 -0
  108. package/src/types/mcp.ts +70 -0
  109. package/src/types/security.ts +148 -0
@@ -0,0 +1,73 @@
1
+ import type { FlowSecurityTarget } from '../../../types/security.js';
2
+ import { FlowSecurityError } from '../FlowSecurityError.js';
3
+
4
+ /*** Validates target syntax before the host transport verifies every resolved network peer. */
5
+ export const validateFlowSecurityTarget = (
6
+ target: FlowSecurityTarget | undefined,
7
+ ): string | undefined => {
8
+ if (!target) return undefined;
9
+ let url: URL;
10
+ try {
11
+ url = new URL(target.url);
12
+ } catch {
13
+ throw new FlowSecurityError('invalid_target', 'External targets must be absolute HTTPS URLs.');
14
+ }
15
+ const host = url.hostname.replace(/^\[|\]$/g, '').toLowerCase();
16
+ if (
17
+ url.protocol !== 'https:' ||
18
+ url.username ||
19
+ url.password ||
20
+ url.hash ||
21
+ !uniqueStrings(target.allowedHosts) ||
22
+ !target.allowedHosts.includes(host) ||
23
+ isUnsafeLiteralHost(host)
24
+ )
25
+ throw new FlowSecurityError(
26
+ 'invalid_target',
27
+ 'External targets must use an explicitly allowlisted public HTTPS host.',
28
+ );
29
+ return host;
30
+ };
31
+
32
+ /*** Rejects normalized loopback, local, private, and special-use IP literals before transport access. */
33
+ const isUnsafeLiteralHost = (host: string): boolean =>
34
+ host === 'localhost' || host.endsWith('.localhost') || isUnsafeIpv4(host) || isUnsafeIpv6(host);
35
+
36
+ /*** Identifies IPv4 ranges that must never be an outbound target. */
37
+ const isUnsafeIpv4 = (host: string): boolean => {
38
+ const parts = host.split('.').map(Number);
39
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255))
40
+ return false;
41
+ const [first, second] = parts as [number, number, number, number];
42
+ return (
43
+ first === 0 ||
44
+ first === 10 ||
45
+ first === 127 ||
46
+ first >= 224 ||
47
+ (first === 169 && second === 254) ||
48
+ (first === 172 && second >= 16 && second <= 31) ||
49
+ (first === 192 && second === 168)
50
+ );
51
+ };
52
+
53
+ /*** Identifies IPv6 loopback, unspecified, unique-local, link-local, and mapped literal forms. */
54
+ const isUnsafeIpv6 = (host: string): boolean => {
55
+ const normalized = host.toLowerCase();
56
+ return (
57
+ normalized === '::' ||
58
+ normalized === '::1' ||
59
+ normalized.startsWith('fc') ||
60
+ normalized.startsWith('fd') ||
61
+ normalized.startsWith('fe8') ||
62
+ normalized.startsWith('fe9') ||
63
+ normalized.startsWith('fea') ||
64
+ normalized.startsWith('feb') ||
65
+ normalized.startsWith('::ffff:')
66
+ );
67
+ };
68
+
69
+ /*** Checks unique non-empty authority strings. */
70
+ const uniqueStrings = (values: readonly string[]): boolean =>
71
+ values.length > 0 &&
72
+ values.every((value) => value.length > 0) &&
73
+ new Set(values).size === values.length;
package/src/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ /*** Headless graph compilation and deterministic single-process execution. */
2
+ export { executeFlowAsync } from './features/execution/application/executeFlowAsync.js';
3
+ export { compileFlowGraph } from './features/execution/domain/compileFlowGraph.js';
4
+ export { createDefaultNodeRegistry } from './features/execution/domain/createDefaultNodeRegistry.js';
5
+ export { createFlowSecurityPort } from './features/security/application/createFlowSecurityPort.js';
6
+ export { FlowSecurityError } from './features/security/FlowSecurityError.js';
7
+ export type {
8
+ FlowClock,
9
+ FlowCompiledGraph,
10
+ FlowExecutionResult,
11
+ FlowNodeOperation,
12
+ FlowNodeOperationResult,
13
+ FlowNodeRegistry,
14
+ FlowRunRequest,
15
+ } from './types/execution.js';
16
+ export type {
17
+ FlowApprovalStorePort,
18
+ FlowOperationStorePort,
19
+ FlowOutboundTransportPort,
20
+ FlowRateLimitPort,
21
+ FlowResolvedSecrets,
22
+ FlowSecretCreateInput,
23
+ FlowSecretMetadata,
24
+ FlowSecretRevokeInput,
25
+ FlowSecurityActor,
26
+ FlowSecurityApproval,
27
+ FlowSecurityAuditEvent,
28
+ FlowSecurityAuditPort,
29
+ FlowSecurityGrant,
30
+ FlowSecurityOperation,
31
+ FlowSecurityOperationIdentity,
32
+ FlowSecurityPermit,
33
+ FlowSecurityPort,
34
+ FlowSecurityTarget,
35
+ } from './types/security.js';
package/src/mcp.ts ADDED
@@ -0,0 +1,11 @@
1
+ /*** Optional, allowlist-only MCP tool discovery and invocation integration. */
2
+ export { createMcpToolRegistry } from './features/mcp/createMcpToolRegistry.js';
3
+ export type {
4
+ McpProviderDefinition,
5
+ McpToolInvocation,
6
+ McpToolMetadata,
7
+ McpToolPermission,
8
+ McpToolRegistry,
9
+ McpToolResult,
10
+ McpTrustedStdioProvider,
11
+ } from './types/mcp.js';
package/src/social.ts ADDED
@@ -0,0 +1,6 @@
1
+ /***
2
+ * Reserved public subpath for future social-provider adapters.
3
+ *
4
+ * This bootstrap intentionally provides no social execution, configuration, or dependencies.
5
+ */
6
+ export {};
@@ -0,0 +1,41 @@
1
+ import type { DataSchemaRegistry } from '@ankhorage/contracts/data';
2
+ import type {
3
+ FlowDataRef,
4
+ FlowDefinition,
5
+ FlowEdge,
6
+ FlowNode,
7
+ FlowNodeConfig,
8
+ FlowPortId,
9
+ FlowRunSnapshot,
10
+ } from '@ankhorage/contracts/flow';
11
+
12
+ export interface FlowClock {
13
+ readonly now: () => Date;
14
+ }
15
+ export interface FlowNodeOperationResult {
16
+ readonly output?: Readonly<Record<FlowPortId, FlowDataRef>>;
17
+ }
18
+ export interface FlowNodeOperation {
19
+ readonly executeAsync: (input: {
20
+ readonly config: FlowNodeConfig | undefined;
21
+ readonly input: Readonly<Record<FlowPortId, FlowDataRef>>;
22
+ readonly node: FlowNode;
23
+ readonly signal: AbortSignal;
24
+ }) => Promise<FlowNodeOperationResult>;
25
+ }
26
+ export type FlowNodeRegistry = Readonly<Record<string, FlowNodeOperation>>;
27
+ export interface FlowCompiledGraph {
28
+ readonly definition: FlowDefinition;
29
+ readonly edgesBySourceNodeId: Readonly<Record<string, readonly FlowEdge[]>>;
30
+ readonly topologicalNodeIds: readonly string[];
31
+ }
32
+ export interface FlowRunRequest {
33
+ readonly input?: Readonly<Record<FlowPortId, FlowDataRef>>;
34
+ readonly runId: string;
35
+ readonly schemaRegistry?: DataSchemaRegistry;
36
+ readonly signal?: AbortSignal;
37
+ readonly triggerId: string;
38
+ }
39
+ export interface FlowExecutionResult {
40
+ readonly run: FlowRunSnapshot;
41
+ }
@@ -0,0 +1,70 @@
1
+ import type { SecretRef } from '@ankhorage/contracts/secrets';
2
+
3
+ import type { FlowSecurityActor } from './security.js';
4
+
5
+ /** Public configuration and results for the optional MCP integration. */
6
+ export interface McpToolPermission {
7
+ readonly name: string;
8
+ readonly grantedScopes: readonly string[];
9
+ }
10
+
11
+ export type McpProviderDefinition =
12
+ | {
13
+ readonly id: string;
14
+ readonly transport: 'remote';
15
+ readonly endpoint: string;
16
+ readonly allowedHosts: readonly string[];
17
+ readonly allowedTools: readonly McpToolPermission[];
18
+ readonly grantedScopes: readonly string[];
19
+ readonly timeoutMs?: number;
20
+ }
21
+ | {
22
+ readonly id: string;
23
+ readonly transport: 'stdio';
24
+ readonly allowedTools: readonly McpToolPermission[];
25
+ readonly grantedScopes: readonly string[];
26
+ readonly timeoutMs?: number;
27
+ };
28
+
29
+ /** Host-owned command configuration. It is deliberately separate from workflow provider data. */
30
+ export interface McpTrustedStdioProvider {
31
+ readonly command: string;
32
+ readonly args?: readonly string[];
33
+ }
34
+
35
+ export interface McpToolMetadata {
36
+ readonly providerId: string;
37
+ readonly name: string;
38
+ readonly title?: string;
39
+ readonly description?: string;
40
+ readonly inputSchema: Readonly<Record<string, unknown>>;
41
+ readonly schemaFingerprint: string;
42
+ readonly scopes: readonly string[];
43
+ readonly readOnly: boolean;
44
+ }
45
+
46
+ export interface McpToolInvocation {
47
+ readonly providerId: string;
48
+ readonly toolName: string;
49
+ readonly schemaFingerprint: string;
50
+ readonly arguments: Readonly<Record<string, unknown>>;
51
+ readonly scopes: readonly string[];
52
+ readonly security: {
53
+ readonly actor: FlowSecurityActor;
54
+ readonly idempotencyKey: string;
55
+ readonly operationId: string;
56
+ readonly secretRefs?: readonly SecretRef[];
57
+ };
58
+ readonly signal?: AbortSignal;
59
+ }
60
+
61
+ export interface McpToolResult {
62
+ readonly content: readonly unknown[];
63
+ readonly structuredContent?: Readonly<Record<string, unknown>>;
64
+ readonly isError: boolean;
65
+ }
66
+
67
+ export interface McpToolRegistry {
68
+ discoverAsync(signal?: AbortSignal): Promise<readonly McpToolMetadata[]>;
69
+ invokeAsync(invocation: McpToolInvocation): Promise<McpToolResult>;
70
+ }
@@ -0,0 +1,148 @@
1
+ import type { SecretMetadata, SecretPayload, SecretRef } from '@ankhorage/contracts/secrets';
2
+
3
+ /** Trusted execution identity supplied by the host authentication boundary. */
4
+ export interface FlowSecurityActor {
5
+ readonly id: string;
6
+ readonly tenantId: string;
7
+ readonly projectId: string;
8
+ readonly environment: string;
9
+ }
10
+
11
+ /** An exact capability grant; omitted dimensions never imply access. */
12
+ export interface FlowSecurityGrant {
13
+ readonly actorId: string;
14
+ readonly capability: string;
15
+ readonly tenantId: string;
16
+ readonly projectId: string;
17
+ readonly environment: string;
18
+ }
19
+
20
+ /** A protected external operation presented to the security boundary. */
21
+ export interface FlowSecurityOperation {
22
+ readonly actor: FlowSecurityActor;
23
+ readonly capability: string;
24
+ readonly idempotencyKey: string;
25
+ readonly operationId: string;
26
+ readonly secretRefs?: readonly SecretRef[];
27
+ readonly target?: FlowSecurityTarget;
28
+ }
29
+
30
+ /** An outbound destination and its exact host authority. */
31
+ export interface FlowSecurityTarget {
32
+ readonly allowedHosts: readonly string[];
33
+ readonly url: string;
34
+ }
35
+
36
+ /** The complete immutable identity that grants, claims, and consumes one operation. */
37
+ export interface FlowSecurityOperationIdentity {
38
+ readonly actor: FlowSecurityActor;
39
+ readonly capability: string;
40
+ readonly idempotencyKey: string;
41
+ readonly operationId: string;
42
+ readonly secretRefs: readonly SecretRef[];
43
+ readonly target?: FlowSecurityTarget;
44
+ }
45
+
46
+ /** A durable human decision bound to exactly one complete operation identity. */
47
+ export interface FlowSecurityApproval {
48
+ readonly operation: FlowSecurityOperationIdentity;
49
+ readonly status: 'approved' | 'pending' | 'rejected';
50
+ }
51
+
52
+ /** Host-owned persistence boundary for approval-gate state. */
53
+ export interface FlowApprovalStorePort {
54
+ getAsync(operation: FlowSecurityOperationIdentity): Promise<FlowSecurityApproval | undefined>;
55
+ }
56
+
57
+ /**
58
+ * Host-owned durable exactly-once boundary. Every transition must be atomic across workers and
59
+ * restarts. `releaseAsync` may only release an unconsumed claim for this exact identity.
60
+ */
61
+ export interface FlowOperationStorePort {
62
+ claimAsync(operation: FlowSecurityOperationIdentity): Promise<boolean>;
63
+ consumeAsync(operation: FlowSecurityOperationIdentity): Promise<boolean>;
64
+ releaseAsync(operation: FlowSecurityOperationIdentity): Promise<void>;
65
+ }
66
+
67
+ /**
68
+ * Host-owned network boundary that verifies every actual peer before opening a connection.
69
+ * It must enforce the supplied host allowlist, resolve immediately before every connection,
70
+ * reject private or special-use resolved addresses, and reapply that policy to redirects.
71
+ */
72
+ export interface FlowOutboundTransportPort {
73
+ fetchAsync(input: {
74
+ readonly allowedHosts: readonly string[];
75
+ readonly init?: RequestInit;
76
+ readonly url: string;
77
+ }): Promise<Response>;
78
+ }
79
+
80
+ /** Host-owned rate limiter. A denied acquisition must have no external side effect. */
81
+ export interface FlowRateLimitPort {
82
+ acquireAsync(input: {
83
+ readonly actor: FlowSecurityActor;
84
+ readonly capability: string;
85
+ readonly operationId: string;
86
+ }): Promise<boolean>;
87
+ }
88
+
89
+ /** Redaction-safe audit edge. Secret values and payloads are intentionally absent. */
90
+ export interface FlowSecurityAuditPort {
91
+ writeAsync(event: FlowSecurityAuditEvent): Promise<void>;
92
+ }
93
+
94
+ /** Immutable audit metadata for one permit or denial. */
95
+ export interface FlowSecurityAuditEvent {
96
+ readonly actorId: string;
97
+ readonly capability: string;
98
+ readonly decision: 'denied' | 'permitted';
99
+ readonly operationId: string;
100
+ readonly reason:
101
+ | 'approval_pending'
102
+ | 'approval_rejected'
103
+ | 'credential_create'
104
+ | 'credential_revoke'
105
+ | 'idempotency_conflict'
106
+ | 'invalid_target'
107
+ | 'permission_denied'
108
+ | 'rate_limited'
109
+ | 'secret_unavailable';
110
+ readonly secretRefs: readonly SecretRef[];
111
+ readonly targetHost?: string;
112
+ }
113
+
114
+ /** Metadata-only credential view safe for administrative or graph-facing callers. */
115
+ export type FlowSecretMetadata = SecretMetadata;
116
+ export type FlowResolvedSecrets = Readonly<Record<SecretRef, Readonly<Record<string, string>>>>;
117
+
118
+ /** Protected server-side credential creation input. Its scope is always derived from the actor. */
119
+ export interface FlowSecretCreateInput {
120
+ readonly actor: FlowSecurityActor;
121
+ readonly kind: string;
122
+ readonly payload: SecretPayload;
123
+ readonly provider?: string;
124
+ readonly ref: SecretRef;
125
+ }
126
+
127
+ /** Protected server-side credential revocation input. Its scope is always derived from the actor. */
128
+ export interface FlowSecretRevokeInput {
129
+ readonly actor: FlowSecurityActor;
130
+ readonly ref: SecretRef;
131
+ }
132
+
133
+ /** One-use authorization result. Raw secrets stay private to the returned server closure. */
134
+ export interface FlowSecurityPermit {
135
+ readonly operation: FlowSecurityOperationIdentity;
136
+ executeAsync<T>(operation: (secrets: FlowResolvedSecrets) => Promise<T>): Promise<T>;
137
+ }
138
+
139
+ /** Public server-side boundary consumed by provider, AI, MCP, and social adapters. */
140
+ export interface FlowSecurityPort {
141
+ authorizeAsync(operation: FlowSecurityOperation): Promise<FlowSecurityPermit>;
142
+ createSecretAsync(input: FlowSecretCreateInput): Promise<FlowSecretMetadata>;
143
+ getSecretMetadataAsync(input: {
144
+ readonly actor: FlowSecurityActor;
145
+ readonly ref: SecretRef;
146
+ }): Promise<FlowSecretMetadata>;
147
+ revokeSecretAsync(input: FlowSecretRevokeInput): Promise<void>;
148
+ }