@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 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
+ });