@vaia-lab/sdk 0.1.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/README.md +225 -0
- package/dist/cli.js +205 -0
- package/dist/index.cjs +601 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +521 -0
- package/dist/index.d.ts +521 -0
- package/dist/index.js +576 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# @vaia/sdk
|
|
2
|
+
|
|
3
|
+
VAIA Platform Integration SDK — connect your app, agent, or skill to [Gandia-7](https://gandia7.com) and [Handeia](https://handeia.com).
|
|
4
|
+
|
|
5
|
+
Zero runtime dependencies · Works in Node 18+, Edge, Bun, Deno · TypeScript-first
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @vaia/sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Quick start (Next.js — Gandia-7)
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// app/api/gandia/invoke/route.ts
|
|
21
|
+
import { gandia, VAIAError } from '@vaia/sdk'
|
|
22
|
+
|
|
23
|
+
export const runtime = 'edge'
|
|
24
|
+
|
|
25
|
+
export async function POST(req: Request) {
|
|
26
|
+
try {
|
|
27
|
+
const { ctx } = await gandia.verify(req, process.env.GANDIA_KEY_SECRET!)
|
|
28
|
+
gandia.require(ctx, 'read:students')
|
|
29
|
+
|
|
30
|
+
const data = await myDb.getStudents(ctx.tenant.id)
|
|
31
|
+
|
|
32
|
+
return gandia.respond.surface(ctx.surface, {
|
|
33
|
+
card: () => ({ title: 'Alumnos', value: data.length }),
|
|
34
|
+
table: () => ({ columns: ['nombre', 'riesgo'], rows: data }),
|
|
35
|
+
text: () => `${data.length} alumnos en ${ctx.tenant.name}`,
|
|
36
|
+
})
|
|
37
|
+
} catch (err) {
|
|
38
|
+
if (err instanceof VAIAError) return gandia.respond.error(err.message, err.status)
|
|
39
|
+
throw err
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## API Reference
|
|
47
|
+
|
|
48
|
+
### `gandia.verify(request, secret)`
|
|
49
|
+
|
|
50
|
+
Verifies the HMAC-SHA256 signature on an incoming invoke call from Gandia-7.
|
|
51
|
+
|
|
52
|
+
- Checks `X-Gandia-Signature`, `X-Gandia-Timestamp` (replay window ±5 min)
|
|
53
|
+
- Returns `{ ctx: GandiaContext, raw: string }`
|
|
54
|
+
- Throws `VAIAError` (status 401) if invalid
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const { ctx } = await gandia.verify(req, process.env.GANDIA_KEY_SECRET!)
|
|
58
|
+
// ctx.tenant.id, ctx.tenant.name, ctx.tenant.sector
|
|
59
|
+
// ctx.user.id, ctx.user.role
|
|
60
|
+
// ctx.permissions → ['read:students', 'read:grades']
|
|
61
|
+
// ctx.surface → 'card' | 'table' | 'text' | 'widget' | 'action' | 'data'
|
|
62
|
+
// ctx.query → user's original question (if trigger = 'user_query')
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### `gandia.require(ctx, permission)`
|
|
66
|
+
|
|
67
|
+
Throws `VAIAError` (403) if the context doesn't have the required permission.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
gandia.require(ctx, 'write:alerts') // single
|
|
71
|
+
gandia.requireAll(ctx, 'read:students', 'read:grades') // all
|
|
72
|
+
const ok = gandia.can(ctx, 'read:health') // boolean check
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### `gandia.respond.surface(surface, handlers, opts?)`
|
|
76
|
+
|
|
77
|
+
Multi-surface responder — GAIA tells you which surface it wants via `ctx.surface`.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
return gandia.respond.surface(ctx.surface, {
|
|
81
|
+
card: () => ({ title: 'Riesgo', value: 72, unit: '%', trend: 'up' }),
|
|
82
|
+
table: () => ({ columns: ['alumno', 'score'], rows }),
|
|
83
|
+
text: () => `El riesgo promedio es 72%`,
|
|
84
|
+
}, { audit: { data_sources: ['supabase:students'], records_accessed: 120 } })
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Available surfaces: `card` · `table` · `text` · `widget` · `action` · `data`
|
|
88
|
+
|
|
89
|
+
### `gandia.respond.*` — individual builders
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
gandia.respond.card({ title, value, unit, trend, subtitle, color })
|
|
93
|
+
gandia.respond.table({ columns, rows, total })
|
|
94
|
+
gandia.respond.text(content, { markdown: true })
|
|
95
|
+
gandia.respond.widget({ url, height, width })
|
|
96
|
+
gandia.respond.action({ type, label, params, confirm })
|
|
97
|
+
gandia.respond.data(payload)
|
|
98
|
+
gandia.respond.ok()
|
|
99
|
+
gandia.respond.error(message, status, code?)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
All return a `Response` (Web API standard). For Express/Fastify use `gandia.make.*` instead (returns plain JSON object).
|
|
103
|
+
|
|
104
|
+
### `gandia.jwt.verify(token, secret)`
|
|
105
|
+
|
|
106
|
+
Verifies a Gandia-7 iframe JWT (`gandia_token` URL param). Use in iframe entry routes to skip your own login.
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
// In your iframe entry route:
|
|
110
|
+
const claims = await gandia.jwt.fromUrl(request.url, process.env.GANDIA_KEY_SECRET!)
|
|
111
|
+
// claims.sub → user_id
|
|
112
|
+
// claims.tenant_id → institution id
|
|
113
|
+
// claims.email → user email (if available)
|
|
114
|
+
// claims.permissions → what the user can do
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
### `handeia.*`
|
|
120
|
+
|
|
121
|
+
Same API as `gandia.*` but for Handeia (personal platform). No tenant — just the user.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const { ctx } = await handeia.verify(req, process.env.HANDEIA_KEY_SECRET!)
|
|
125
|
+
// ctx.user.id, ctx.user.email
|
|
126
|
+
// ctx.permissions, ctx.surface, ctx.query
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
### `defineCapability(config)`
|
|
132
|
+
|
|
133
|
+
Declares your capability metadata in code. The Shazam engine reads this with 100% confidence — no manual confirmation needed in the Developer Portal.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
// vaia.config.ts
|
|
137
|
+
import { defineCapability } from '@vaia/sdk'
|
|
138
|
+
|
|
139
|
+
export default defineCapability({
|
|
140
|
+
id: 'mx.monitor-riesgo-academico', // reverse-domain
|
|
141
|
+
name: 'Monitor de Riesgo Académico',
|
|
142
|
+
version: '1.0.0',
|
|
143
|
+
target: 'gandia', // 'gandia' | 'handeia' | 'both'
|
|
144
|
+
type: 'app', // 'app' | 'ia' | 'skill' | 'eco'
|
|
145
|
+
level: 'artefacto', // 'widget' | 'artefacto' | 'espacio'
|
|
146
|
+
sector: 'educacion',
|
|
147
|
+
surfaces: {
|
|
148
|
+
card: { endpoint: '/api/gandia/invoke' },
|
|
149
|
+
table: { endpoint: '/api/gandia/invoke' },
|
|
150
|
+
text: { endpoint: '/api/gandia/invoke' },
|
|
151
|
+
},
|
|
152
|
+
permissions: ['read:students', 'read:grades', 'write:alerts'],
|
|
153
|
+
risk: 'medium',
|
|
154
|
+
})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Generate `gandia.manifest.json`:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# Compile vaia.config.ts first, then:
|
|
161
|
+
npx vaia manifest
|
|
162
|
+
|
|
163
|
+
# Validate existing manifest:
|
|
164
|
+
npx vaia manifest --validate
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Error handling
|
|
170
|
+
|
|
171
|
+
All verification functions throw `VAIAError` on failure:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import { VAIAError } from '@vaia/sdk'
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const { ctx } = await gandia.verify(req, secret)
|
|
178
|
+
// ...
|
|
179
|
+
} catch (err) {
|
|
180
|
+
if (err instanceof VAIAError) {
|
|
181
|
+
// err.message → human-readable message (Spanish)
|
|
182
|
+
// err.code → machine-readable: 'HMAC_INVALID', 'JWT_EXPIRED', 'PERMISSION_DENIED', etc.
|
|
183
|
+
// err.status → HTTP status: 401, 403, 400, 422
|
|
184
|
+
return gandia.respond.error(err.message, err.status, err.code)
|
|
185
|
+
}
|
|
186
|
+
throw err
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Error codes
|
|
191
|
+
|
|
192
|
+
| Code | Status | When |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| `MISSING_AUTH_HEADERS` | 401 | X-Gandia-Signature or X-Gandia-Timestamp missing |
|
|
195
|
+
| `TIMESTAMP_OUT_OF_RANGE` | 401 | Timestamp outside ±5 min window |
|
|
196
|
+
| `HMAC_INVALID` | 401 | Signature doesn't match |
|
|
197
|
+
| `BODY_PARSE_ERROR` | 400 | Body is not valid JSON |
|
|
198
|
+
| `JWT_MALFORMED` | 401 | JWT doesn't have 3 parts |
|
|
199
|
+
| `JWT_SIGNATURE_INVALID` | 401 | JWT signature check failed |
|
|
200
|
+
| `JWT_EXPIRED` | 401 | JWT exp claim is in the past |
|
|
201
|
+
| `PERMISSION_DENIED` | 403 | Missing required permission |
|
|
202
|
+
| `SURFACE_NOT_SUPPORTED` | 422 | No handler for requested surface |
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## CLI
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
npx vaia manifest # generate gandia.manifest.json
|
|
210
|
+
npx vaia manifest --validate # validate existing manifest
|
|
211
|
+
npx vaia sign payload.json # sign a payload (needs GANDIA_KEY_SECRET)
|
|
212
|
+
npx vaia version # SDK version
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Publishing
|
|
218
|
+
|
|
219
|
+
Publication to npm is done manually by the VAIA team via `npm login` + `npm publish`.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
MIT — VAIA
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
|
|
4
|
+
// src/cli.ts
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
6
|
+
import { resolve, join } from "path";
|
|
7
|
+
import { createInterface } from "readline";
|
|
8
|
+
|
|
9
|
+
// src/crypto.ts
|
|
10
|
+
var enc = new TextEncoder();
|
|
11
|
+
async function importHMACKey(secret) {
|
|
12
|
+
return crypto.subtle.importKey(
|
|
13
|
+
"raw",
|
|
14
|
+
enc.encode(secret),
|
|
15
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
16
|
+
false,
|
|
17
|
+
["sign", "verify"]
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
async function hmacSign(secret, data) {
|
|
21
|
+
const key = await importHMACKey(secret);
|
|
22
|
+
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(data));
|
|
23
|
+
return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/define.ts
|
|
27
|
+
function defineCapability(config) {
|
|
28
|
+
const required = ["id", "name", "version", "target", "type", "sector", "permissions", "risk"];
|
|
29
|
+
for (const key of required) {
|
|
30
|
+
if (config[key] === void 0 || config[key] === "") {
|
|
31
|
+
throw new Error(`[@vaia/sdk] defineCapability: campo requerido faltante: '${key}'`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!config.id.includes(".")) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`[@vaia/sdk] defineCapability: 'id' debe usar formato reverse-domain (ej. 'mx.mi-capacidad'). Recibido: '${config.id}'`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
if (Object.keys(config.surfaces).length === 0) {
|
|
40
|
+
throw new Error(`[@vaia/sdk] defineCapability: 'surfaces' no puede estar vac\xEDo. Define al menos un surface con su endpoint.`);
|
|
41
|
+
}
|
|
42
|
+
return config;
|
|
43
|
+
}
|
|
44
|
+
function toManifest(config) {
|
|
45
|
+
const surfaces = Object.keys(config.surfaces);
|
|
46
|
+
return {
|
|
47
|
+
schema: "1.0",
|
|
48
|
+
capability_id: config.id,
|
|
49
|
+
name: config.name,
|
|
50
|
+
version: config.version,
|
|
51
|
+
target: config.target === "both" ? ["gandia", "handeia"] : config.target,
|
|
52
|
+
type: config.type,
|
|
53
|
+
level: config.level,
|
|
54
|
+
sector: config.sector,
|
|
55
|
+
surfaces,
|
|
56
|
+
permissions: config.permissions,
|
|
57
|
+
risk: config.risk,
|
|
58
|
+
has_own_auth: config.has_own_auth ?? false,
|
|
59
|
+
stores_data: config.stores_data ?? false,
|
|
60
|
+
trains_models: config.trains_models ?? false,
|
|
61
|
+
requires_consent: config.requires_consent ?? false,
|
|
62
|
+
description: config.description,
|
|
63
|
+
tags: config.tags,
|
|
64
|
+
linked: config.target === "both",
|
|
65
|
+
generated_by: "@vaia/sdk",
|
|
66
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/cli.ts
|
|
71
|
+
var pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
72
|
+
var args = process.argv.slice(2);
|
|
73
|
+
var cmd = args[0];
|
|
74
|
+
async function main() {
|
|
75
|
+
switch (cmd) {
|
|
76
|
+
case "manifest":
|
|
77
|
+
return cmdManifest();
|
|
78
|
+
case "sign":
|
|
79
|
+
return cmdSign();
|
|
80
|
+
case "version":
|
|
81
|
+
case "-v":
|
|
82
|
+
case "--version":
|
|
83
|
+
console.log(`@vaia/sdk v${pkg.version}`);
|
|
84
|
+
break;
|
|
85
|
+
default:
|
|
86
|
+
printHelp();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function cmdManifest() {
|
|
90
|
+
const cwd = process.cwd();
|
|
91
|
+
const configPath = resolve(cwd, "vaia.config.js");
|
|
92
|
+
const outPath = join(cwd, "gandia.manifest.json");
|
|
93
|
+
const validate = args.includes("--validate");
|
|
94
|
+
if (validate) {
|
|
95
|
+
if (!existsSync(outPath)) {
|
|
96
|
+
console.error("\u2717 gandia.manifest.json no encontrado.");
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
const raw = JSON.parse(readFileSync(outPath, "utf8"));
|
|
100
|
+
const required = ["schema", "capability_id", "name", "version", "target", "type", "sector", "permissions", "risk"];
|
|
101
|
+
const missing = required.filter((k) => !raw[k]);
|
|
102
|
+
if (missing.length > 0) {
|
|
103
|
+
console.error(`\u2717 Campos requeridos faltantes: ${missing.join(", ")}`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
console.log(`\u2713 gandia.manifest.json v\xE1lido (${raw["capability_id"]})`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (!existsSync(configPath)) {
|
|
110
|
+
console.error([
|
|
111
|
+
"\u2717 vaia.config.js no encontrado en:",
|
|
112
|
+
cwd,
|
|
113
|
+
"",
|
|
114
|
+
"Crea vaia.config.ts en la ra\xEDz de tu proyecto:",
|
|
115
|
+
"",
|
|
116
|
+
" import { defineCapability } from '@vaia/sdk'",
|
|
117
|
+
" export default defineCapability({",
|
|
118
|
+
" id: 'mx.mi-capacidad',",
|
|
119
|
+
" name: 'Mi Capacidad',",
|
|
120
|
+
" version: '1.0.0',",
|
|
121
|
+
" target: 'gandia',",
|
|
122
|
+
" type: 'app',",
|
|
123
|
+
" sector: 'educacion',",
|
|
124
|
+
" surfaces: { card: { endpoint: '/api/gandia/invoke' } },",
|
|
125
|
+
" permissions: ['read:data'],",
|
|
126
|
+
" risk: 'low',",
|
|
127
|
+
" })",
|
|
128
|
+
"",
|
|
129
|
+
"Compila primero con tsc o tsx, luego corre: npx vaia manifest"
|
|
130
|
+
].join("\n"));
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
let config;
|
|
134
|
+
try {
|
|
135
|
+
const mod = await import(configPath);
|
|
136
|
+
config = mod.default;
|
|
137
|
+
defineCapability(config);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
140
|
+
console.error(`\u2717 Error al cargar vaia.config.js:
|
|
141
|
+
${msg}`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
if (!config) {
|
|
145
|
+
console.error("\u2717 vaia.config.js no export\xF3 ninguna configuraci\xF3n.");
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
const manifest = toManifest(config);
|
|
149
|
+
writeFileSync(outPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
150
|
+
console.log(`\u2713 gandia.manifest.json generado \u2192 ${outPath}`);
|
|
151
|
+
console.log(` capability_id : ${manifest.capability_id}`);
|
|
152
|
+
console.log(` target : ${Array.isArray(manifest.target) ? manifest.target.join(" + ") : manifest.target}`);
|
|
153
|
+
console.log(` surfaces : ${manifest.surfaces.join(", ")}`);
|
|
154
|
+
console.log(` permissions : ${manifest.permissions.join(", ")}`);
|
|
155
|
+
console.log(` risk : ${manifest.risk}`);
|
|
156
|
+
}
|
|
157
|
+
async function cmdSign() {
|
|
158
|
+
const payloadArg = args[1];
|
|
159
|
+
const secret = process.env["GANDIA_KEY_SECRET"] ?? process.env["HANDEIA_KEY_SECRET"];
|
|
160
|
+
if (!secret) {
|
|
161
|
+
console.error("\u2717 GANDIA_KEY_SECRET o HANDEIA_KEY_SECRET requerido en el entorno.");
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
let payload;
|
|
165
|
+
if (payloadArg && existsSync(payloadArg)) {
|
|
166
|
+
payload = readFileSync(payloadArg, "utf8").trim();
|
|
167
|
+
} else if (payloadArg) {
|
|
168
|
+
payload = payloadArg;
|
|
169
|
+
} else {
|
|
170
|
+
payload = await readStdin();
|
|
171
|
+
}
|
|
172
|
+
const timestamp = Date.now().toString();
|
|
173
|
+
const signed = `${timestamp}.${payload}`;
|
|
174
|
+
const sig = await hmacSign(secret, signed);
|
|
175
|
+
console.log(JSON.stringify({
|
|
176
|
+
"X-Gandia-Signature": `sha256=${sig}`,
|
|
177
|
+
"X-Gandia-Timestamp": timestamp,
|
|
178
|
+
signed_payload: signed
|
|
179
|
+
}, null, 2));
|
|
180
|
+
}
|
|
181
|
+
function printHelp() {
|
|
182
|
+
console.log([
|
|
183
|
+
`@vaia/sdk v${pkg.version}`,
|
|
184
|
+
"",
|
|
185
|
+
"Comandos:",
|
|
186
|
+
" vaia manifest Genera gandia.manifest.json desde vaia.config.js",
|
|
187
|
+
" vaia manifest --validate Valida un gandia.manifest.json existente",
|
|
188
|
+
" vaia sign [payload] Firma un payload con GANDIA_KEY_SECRET",
|
|
189
|
+
" vaia version Muestra la versi\xF3n del SDK",
|
|
190
|
+
"",
|
|
191
|
+
"Docs: https://github.com/diegoramirez772/vaia-sdk"
|
|
192
|
+
].join("\n"));
|
|
193
|
+
}
|
|
194
|
+
function readStdin() {
|
|
195
|
+
return new Promise((resolve2) => {
|
|
196
|
+
const rl = createInterface({ input: process.stdin });
|
|
197
|
+
const lines = [];
|
|
198
|
+
rl.on("line", (l) => lines.push(l));
|
|
199
|
+
rl.on("close", () => resolve2(lines.join("\n")));
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
main().catch((err) => {
|
|
203
|
+
console.error(err instanceof Error ? err.message : err);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
});
|