@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/dist/index.cjs
ADDED
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
// @vaia/sdk — VAIA Platform Integration SDK
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var src_exports = {};
|
|
23
|
+
__export(src_exports, {
|
|
24
|
+
VAIAError: () => VAIAError,
|
|
25
|
+
defineCapability: () => defineCapability,
|
|
26
|
+
gandia: () => gandia,
|
|
27
|
+
handeia: () => handeia,
|
|
28
|
+
toManifest: () => toManifest
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(src_exports);
|
|
31
|
+
|
|
32
|
+
// src/gandia/index.ts
|
|
33
|
+
var gandia_exports = {};
|
|
34
|
+
__export(gandia_exports, {
|
|
35
|
+
can: () => can,
|
|
36
|
+
canAll: () => canAll,
|
|
37
|
+
canAny: () => canAny,
|
|
38
|
+
isProbe: () => isProbe,
|
|
39
|
+
jwt: () => jwt_exports,
|
|
40
|
+
make: () => make,
|
|
41
|
+
require: () => require2,
|
|
42
|
+
requireAll: () => requireAll,
|
|
43
|
+
respond: () => respond,
|
|
44
|
+
verify: () => verify
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// src/crypto.ts
|
|
48
|
+
var enc = new TextEncoder();
|
|
49
|
+
function hexToBytes(hex) {
|
|
50
|
+
if (hex.length % 2 !== 0) throw new Error("Invalid hex string");
|
|
51
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
52
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
53
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
54
|
+
}
|
|
55
|
+
return bytes;
|
|
56
|
+
}
|
|
57
|
+
async function importHMACKey(secret) {
|
|
58
|
+
return crypto.subtle.importKey(
|
|
59
|
+
"raw",
|
|
60
|
+
enc.encode(secret),
|
|
61
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
62
|
+
false,
|
|
63
|
+
["sign", "verify"]
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
async function hmacVerify(secret, data, hexSignature) {
|
|
67
|
+
try {
|
|
68
|
+
const key = await importHMACKey(secret);
|
|
69
|
+
const sigBytes = hexToBytes(hexSignature);
|
|
70
|
+
return await crypto.subtle.verify("HMAC", key, sigBytes, enc.encode(data));
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/types.ts
|
|
77
|
+
var VAIAError = class extends Error {
|
|
78
|
+
constructor(message, code, status = 500) {
|
|
79
|
+
super(message);
|
|
80
|
+
this.code = code;
|
|
81
|
+
this.status = status;
|
|
82
|
+
this.name = "VAIAError";
|
|
83
|
+
}
|
|
84
|
+
code;
|
|
85
|
+
status;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// src/gandia/verify.ts
|
|
89
|
+
var REPLAY_WINDOW_MS = 5 * 60 * 1e3;
|
|
90
|
+
var PROBE_HEADER = "x-gandia-probe";
|
|
91
|
+
async function verify(request, secret) {
|
|
92
|
+
const rawBody = await request.text();
|
|
93
|
+
const headers = request.headers;
|
|
94
|
+
const sigHeader = headers.get("x-gandia-signature") ?? "";
|
|
95
|
+
const tsHeader = headers.get("x-gandia-timestamp") ?? "";
|
|
96
|
+
const callId = headers.get("x-gandia-call-id") ?? "";
|
|
97
|
+
if (headers.get(PROBE_HEADER) === "1") {
|
|
98
|
+
return { ctx: buildProbeCtx(callId), raw: rawBody };
|
|
99
|
+
}
|
|
100
|
+
if (!sigHeader || !tsHeader) {
|
|
101
|
+
throw new VAIAError(
|
|
102
|
+
"Faltan headers de autenticaci\xF3n (X-Gandia-Signature, X-Gandia-Timestamp)",
|
|
103
|
+
"MISSING_AUTH_HEADERS",
|
|
104
|
+
401
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const ts = parseInt(tsHeader, 10);
|
|
108
|
+
if (isNaN(ts) || Math.abs(Date.now() - ts) > REPLAY_WINDOW_MS) {
|
|
109
|
+
throw new VAIAError("Timestamp fuera de ventana (\xB15 min)", "TIMESTAMP_OUT_OF_RANGE", 401);
|
|
110
|
+
}
|
|
111
|
+
const hexSig = sigHeader.startsWith("sha256=") ? sigHeader.slice(7) : sigHeader;
|
|
112
|
+
const signedData = `${tsHeader}.${rawBody}`;
|
|
113
|
+
const valid = await hmacVerify(secret, signedData, hexSig);
|
|
114
|
+
if (!valid) {
|
|
115
|
+
throw new VAIAError("Firma HMAC inv\xE1lida", "HMAC_INVALID", 401);
|
|
116
|
+
}
|
|
117
|
+
let body;
|
|
118
|
+
try {
|
|
119
|
+
body = JSON.parse(rawBody);
|
|
120
|
+
} catch {
|
|
121
|
+
throw new VAIAError("Body no es JSON v\xE1lido", "BODY_PARSE_ERROR", 400);
|
|
122
|
+
}
|
|
123
|
+
const ctx = parseContext(body, callId);
|
|
124
|
+
return { ctx, raw: rawBody };
|
|
125
|
+
}
|
|
126
|
+
function parseContext(body, fallbackCallId) {
|
|
127
|
+
if (typeof body !== "object" || body === null) {
|
|
128
|
+
throw new VAIAError("Body inv\xE1lido", "BODY_INVALID", 400);
|
|
129
|
+
}
|
|
130
|
+
const b = body;
|
|
131
|
+
return {
|
|
132
|
+
capability_id: str(b, "capability_id"),
|
|
133
|
+
call_id: str(b, "call_id", fallbackCallId),
|
|
134
|
+
tenant: {
|
|
135
|
+
id: nestedStr(b, "tenant", "id"),
|
|
136
|
+
name: nestedStr(b, "tenant", "name"),
|
|
137
|
+
sector: nestedStr(b, "tenant", "sector")
|
|
138
|
+
},
|
|
139
|
+
user: {
|
|
140
|
+
id: nestedStr(b, "user", "id"),
|
|
141
|
+
role: nestedStr(b, "user", "role"),
|
|
142
|
+
email: nestedStrOpt(b, "user", "email")
|
|
143
|
+
},
|
|
144
|
+
permissions: arr(b, "permissions"),
|
|
145
|
+
trigger: str(b, "context.trigger", "gaia_invoke"),
|
|
146
|
+
surface: str(b, "context.surface", "data"),
|
|
147
|
+
query: nestedStrOpt(b, "context", "query")
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function buildProbeCtx(callId) {
|
|
151
|
+
return {
|
|
152
|
+
capability_id: "__probe__",
|
|
153
|
+
call_id: callId || crypto.randomUUID(),
|
|
154
|
+
tenant: { id: "__probe__", name: "__probe__", sector: "__probe__" },
|
|
155
|
+
user: { id: "__probe__", role: "__probe__" },
|
|
156
|
+
permissions: [],
|
|
157
|
+
trigger: "gaia_invoke",
|
|
158
|
+
surface: "data"
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function str(obj, key, fallback = "") {
|
|
162
|
+
const parts = key.split(".");
|
|
163
|
+
let cur = obj;
|
|
164
|
+
for (const part of parts) {
|
|
165
|
+
if (typeof cur !== "object" || cur === null) return fallback;
|
|
166
|
+
cur = cur[part];
|
|
167
|
+
}
|
|
168
|
+
return typeof cur === "string" ? cur : fallback;
|
|
169
|
+
}
|
|
170
|
+
function nestedStr(obj, parent, key, fallback = "") {
|
|
171
|
+
const p = obj[parent];
|
|
172
|
+
if (typeof p !== "object" || p === null) return fallback;
|
|
173
|
+
const v = p[key];
|
|
174
|
+
return typeof v === "string" ? v : fallback;
|
|
175
|
+
}
|
|
176
|
+
function nestedStrOpt(obj, parent, key) {
|
|
177
|
+
const p = obj[parent];
|
|
178
|
+
if (typeof p !== "object" || p === null) return void 0;
|
|
179
|
+
const v = p[key];
|
|
180
|
+
return typeof v === "string" ? v : void 0;
|
|
181
|
+
}
|
|
182
|
+
function arr(obj, key) {
|
|
183
|
+
const v = obj[key];
|
|
184
|
+
if (!Array.isArray(v)) return [];
|
|
185
|
+
return v.filter((x) => typeof x === "string");
|
|
186
|
+
}
|
|
187
|
+
function isProbe(request) {
|
|
188
|
+
return request.headers.get(PROBE_HEADER) === "1";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/gandia/jwt.ts
|
|
192
|
+
var jwt_exports = {};
|
|
193
|
+
__export(jwt_exports, {
|
|
194
|
+
fromUrl: () => fromUrl,
|
|
195
|
+
sign: () => sign,
|
|
196
|
+
verify: () => verify2
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// src/jwt-utils.ts
|
|
200
|
+
var enc2 = new TextEncoder();
|
|
201
|
+
function b64urlEncode(data) {
|
|
202
|
+
const str2 = typeof data === "string" ? data : String.fromCharCode(...new Uint8Array(data));
|
|
203
|
+
return btoa(str2).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
204
|
+
}
|
|
205
|
+
function b64urlDecode(str2) {
|
|
206
|
+
const padded = str2.replace(/-/g, "+").replace(/_/g, "/");
|
|
207
|
+
const pad = padded.length % 4;
|
|
208
|
+
return atob(pad ? padded + "=".repeat(4 - pad) : padded);
|
|
209
|
+
}
|
|
210
|
+
async function importHMACKey2(secret) {
|
|
211
|
+
return crypto.subtle.importKey(
|
|
212
|
+
"raw",
|
|
213
|
+
enc2.encode(secret),
|
|
214
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
215
|
+
false,
|
|
216
|
+
["sign", "verify"]
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
var HEADER = b64urlEncode(JSON.stringify({ alg: "HS256", typ: "JWT" }));
|
|
220
|
+
async function jwtSign(payload, secret) {
|
|
221
|
+
const body = b64urlEncode(JSON.stringify(payload));
|
|
222
|
+
const unsigned = `${HEADER}.${body}`;
|
|
223
|
+
const key = await importHMACKey2(secret);
|
|
224
|
+
const sig = await crypto.subtle.sign("HMAC", key, enc2.encode(unsigned));
|
|
225
|
+
return `${unsigned}.${b64urlEncode(sig)}`;
|
|
226
|
+
}
|
|
227
|
+
async function jwtVerify(token, secret) {
|
|
228
|
+
const parts = token.split(".");
|
|
229
|
+
if (parts.length !== 3) {
|
|
230
|
+
throw new VAIAError("JWT malformado", "JWT_MALFORMED", 401);
|
|
231
|
+
}
|
|
232
|
+
const [header, body, sig] = parts;
|
|
233
|
+
const unsigned = `${header}.${body}`;
|
|
234
|
+
const key = await importHMACKey2(secret);
|
|
235
|
+
const sigBytes = Uint8Array.from(b64urlDecode(sig), (c) => c.charCodeAt(0));
|
|
236
|
+
const valid = await crypto.subtle.verify("HMAC", key, sigBytes, enc2.encode(unsigned));
|
|
237
|
+
if (!valid) {
|
|
238
|
+
throw new VAIAError("Firma JWT inv\xE1lida", "JWT_SIGNATURE_INVALID", 401);
|
|
239
|
+
}
|
|
240
|
+
let payload;
|
|
241
|
+
try {
|
|
242
|
+
payload = JSON.parse(b64urlDecode(body));
|
|
243
|
+
} catch {
|
|
244
|
+
throw new VAIAError("JWT payload inv\xE1lido", "JWT_PAYLOAD_INVALID", 401);
|
|
245
|
+
}
|
|
246
|
+
if (typeof payload.exp === "number" && Date.now() / 1e3 > payload.exp) {
|
|
247
|
+
throw new VAIAError("JWT expirado", "JWT_EXPIRED", 401);
|
|
248
|
+
}
|
|
249
|
+
return payload;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/gandia/jwt.ts
|
|
253
|
+
var DEFAULT_EXPIRES_IN = 3600;
|
|
254
|
+
async function sign(input, secret, opts = {}) {
|
|
255
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
256
|
+
return jwtSign(
|
|
257
|
+
{
|
|
258
|
+
...input,
|
|
259
|
+
permissions: input.permissions ?? [],
|
|
260
|
+
iat: now,
|
|
261
|
+
exp: now + (opts.expiresIn ?? DEFAULT_EXPIRES_IN)
|
|
262
|
+
},
|
|
263
|
+
secret
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
async function verify2(token, secret) {
|
|
267
|
+
return jwtVerify(token, secret);
|
|
268
|
+
}
|
|
269
|
+
async function fromUrl(url, secret) {
|
|
270
|
+
const u = typeof url === "string" ? new URL(url) : url;
|
|
271
|
+
const token = u.searchParams.get("gandia_token");
|
|
272
|
+
if (!token) {
|
|
273
|
+
throw new Error("gandia_token no encontrado en la URL");
|
|
274
|
+
}
|
|
275
|
+
return verify2(token, secret);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/gandia/respond.ts
|
|
279
|
+
var JSON_HEADERS = { "Content-Type": "application/json" };
|
|
280
|
+
function toResponse(body, status = 200) {
|
|
281
|
+
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
|
|
282
|
+
}
|
|
283
|
+
var make = {
|
|
284
|
+
card(payload, opts = {}) {
|
|
285
|
+
return { ok: true, output_type: "card", data: payload, ...opts };
|
|
286
|
+
},
|
|
287
|
+
table(payload, opts = {}) {
|
|
288
|
+
return { ok: true, output_type: "table", data: payload, ...opts };
|
|
289
|
+
},
|
|
290
|
+
text(content, opts = {}) {
|
|
291
|
+
const { markdown, ...rest } = opts;
|
|
292
|
+
return { ok: true, output_type: "text", text: content, markdown, ...rest };
|
|
293
|
+
},
|
|
294
|
+
widget(payload, opts = {}) {
|
|
295
|
+
return { ok: true, output_type: "widget", data: payload, ...opts };
|
|
296
|
+
},
|
|
297
|
+
action(payload, opts = {}) {
|
|
298
|
+
return { ok: true, output_type: "action", data: payload, ...opts };
|
|
299
|
+
},
|
|
300
|
+
data(payload, opts = {}) {
|
|
301
|
+
return { ok: true, output_type: "data", data: payload, ...opts };
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
var respond = {
|
|
305
|
+
/** A compact card: title, value, unit, trend, etc. */
|
|
306
|
+
card(payload, opts = {}) {
|
|
307
|
+
return toResponse(make.card(payload, opts));
|
|
308
|
+
},
|
|
309
|
+
/** Tabular data with columns + rows. */
|
|
310
|
+
table(payload, opts = {}) {
|
|
311
|
+
return toResponse(make.table(payload, opts));
|
|
312
|
+
},
|
|
313
|
+
/** Plain text or markdown — shown in chat or narrative views. */
|
|
314
|
+
text(content, opts = {}) {
|
|
315
|
+
return toResponse(make.text(content, opts));
|
|
316
|
+
},
|
|
317
|
+
/** An iframe pointing to the developer's own UI. */
|
|
318
|
+
widget(payload, opts = {}) {
|
|
319
|
+
return toResponse(make.widget(payload, opts));
|
|
320
|
+
},
|
|
321
|
+
/** Trigger an action (mutation, navigation, confirm dialog). */
|
|
322
|
+
action(payload, opts = {}) {
|
|
323
|
+
return toResponse(make.action(payload, opts));
|
|
324
|
+
},
|
|
325
|
+
/** Arbitrary JSON — HAIA decides how to render based on shape. */
|
|
326
|
+
data(payload, opts = {}) {
|
|
327
|
+
return toResponse(make.data(payload, opts));
|
|
328
|
+
},
|
|
329
|
+
/** Simple 200 OK — useful for probe responses. */
|
|
330
|
+
ok(opts = {}) {
|
|
331
|
+
return toResponse(make.data({ status: "ok" }, opts));
|
|
332
|
+
},
|
|
333
|
+
/** Returns a 4xx/5xx error response. */
|
|
334
|
+
error(message, status = 500, code) {
|
|
335
|
+
return toResponse({ ok: false, error: message, ...code ? { code } : {} }, status);
|
|
336
|
+
},
|
|
337
|
+
/**
|
|
338
|
+
* Multi-surface responder.
|
|
339
|
+
* GAIA sends `ctx.surface` telling you what to render.
|
|
340
|
+
* Map each surface to its handler and this picks the right one.
|
|
341
|
+
*
|
|
342
|
+
* return gandia.respond.surface(ctx.surface, {
|
|
343
|
+
* card: () => ({ title: 'Riesgo', value: 72, unit: '%' }),
|
|
344
|
+
* table: () => ({ columns: [...], rows: [...] }),
|
|
345
|
+
* text: () => `El riesgo es 72%`,
|
|
346
|
+
* }, opts)
|
|
347
|
+
*/
|
|
348
|
+
async surface(surface, handlers, opts = {}) {
|
|
349
|
+
const handler = handlers[surface];
|
|
350
|
+
if (!handler) {
|
|
351
|
+
const fallback = handlers.data ?? handlers.text ?? Object.values(handlers).find(Boolean);
|
|
352
|
+
if (!fallback) {
|
|
353
|
+
throw new VAIAError(
|
|
354
|
+
`Surface '${surface}' no soportada por esta capacidad`,
|
|
355
|
+
"SURFACE_NOT_SUPPORTED",
|
|
356
|
+
422
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
return respond.surface(
|
|
360
|
+
handlers.data ? "data" : handlers.text ? "text" : Object.keys(handlers)[0],
|
|
361
|
+
handlers,
|
|
362
|
+
opts
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
switch (surface) {
|
|
366
|
+
case "card":
|
|
367
|
+
return respond.card(await handlers.card(), opts);
|
|
368
|
+
case "table":
|
|
369
|
+
return respond.table(await handlers.table(), opts);
|
|
370
|
+
case "text":
|
|
371
|
+
return respond.text(await handlers.text(), opts);
|
|
372
|
+
case "widget":
|
|
373
|
+
return respond.widget(await handlers.widget(), opts);
|
|
374
|
+
case "action":
|
|
375
|
+
return respond.action(await handlers.action(), opts);
|
|
376
|
+
case "data":
|
|
377
|
+
return respond.data(await handlers.data(), opts);
|
|
378
|
+
default:
|
|
379
|
+
return respond.data(await (handlers.data ?? handlers.text ?? (() => ({})))(), opts);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// src/gandia/permissions.ts
|
|
385
|
+
function matches(permission, granted) {
|
|
386
|
+
if (granted.includes(permission)) return true;
|
|
387
|
+
const [action] = permission.split(":");
|
|
388
|
+
return granted.includes(`${action}:*`);
|
|
389
|
+
}
|
|
390
|
+
function can(ctx, permission) {
|
|
391
|
+
return matches(permission, ctx.permissions);
|
|
392
|
+
}
|
|
393
|
+
function canAll(ctx, ...permissions) {
|
|
394
|
+
return permissions.every((p) => matches(p, ctx.permissions));
|
|
395
|
+
}
|
|
396
|
+
function canAny(ctx, ...permissions) {
|
|
397
|
+
return permissions.some((p) => matches(p, ctx.permissions));
|
|
398
|
+
}
|
|
399
|
+
function require2(ctx, permission) {
|
|
400
|
+
if (!matches(permission, ctx.permissions)) {
|
|
401
|
+
throw new VAIAError(
|
|
402
|
+
`Permiso requerido: ${permission}`,
|
|
403
|
+
"PERMISSION_DENIED",
|
|
404
|
+
403
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function requireAll(ctx, ...permissions) {
|
|
409
|
+
const missing = permissions.filter((p) => !matches(p, ctx.permissions));
|
|
410
|
+
if (missing.length > 0) {
|
|
411
|
+
throw new VAIAError(
|
|
412
|
+
`Permisos requeridos: ${missing.join(", ")}`,
|
|
413
|
+
"PERMISSION_DENIED",
|
|
414
|
+
403
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/handeia/index.ts
|
|
420
|
+
var handeia_exports = {};
|
|
421
|
+
__export(handeia_exports, {
|
|
422
|
+
can: () => can,
|
|
423
|
+
canAll: () => canAll,
|
|
424
|
+
canAny: () => canAny,
|
|
425
|
+
isProbe: () => isProbe2,
|
|
426
|
+
jwt: () => jwt_exports2,
|
|
427
|
+
make: () => make,
|
|
428
|
+
require: () => require2,
|
|
429
|
+
requireAll: () => requireAll,
|
|
430
|
+
respond: () => respond,
|
|
431
|
+
verify: () => verify3
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// src/handeia/verify.ts
|
|
435
|
+
var REPLAY_WINDOW_MS2 = 5 * 60 * 1e3;
|
|
436
|
+
var PROBE_HEADER2 = "x-handeia-probe";
|
|
437
|
+
async function verify3(request, secret) {
|
|
438
|
+
const rawBody = await request.text();
|
|
439
|
+
const headers = request.headers;
|
|
440
|
+
const sigHeader = headers.get("x-handeia-signature") ?? "";
|
|
441
|
+
const tsHeader = headers.get("x-handeia-timestamp") ?? "";
|
|
442
|
+
const callId = headers.get("x-handeia-call-id") ?? "";
|
|
443
|
+
if (headers.get(PROBE_HEADER2) === "1") {
|
|
444
|
+
return { ctx: buildProbeCtx2(callId), raw: rawBody };
|
|
445
|
+
}
|
|
446
|
+
if (!sigHeader || !tsHeader) {
|
|
447
|
+
throw new VAIAError(
|
|
448
|
+
"Faltan headers de autenticaci\xF3n (X-Handeia-Signature, X-Handeia-Timestamp)",
|
|
449
|
+
"MISSING_AUTH_HEADERS",
|
|
450
|
+
401
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
const ts = parseInt(tsHeader, 10);
|
|
454
|
+
if (isNaN(ts) || Math.abs(Date.now() - ts) > REPLAY_WINDOW_MS2) {
|
|
455
|
+
throw new VAIAError("Timestamp fuera de ventana (\xB15 min)", "TIMESTAMP_OUT_OF_RANGE", 401);
|
|
456
|
+
}
|
|
457
|
+
const hexSig = sigHeader.startsWith("sha256=") ? sigHeader.slice(7) : sigHeader;
|
|
458
|
+
const signedData = `${tsHeader}.${rawBody}`;
|
|
459
|
+
const valid = await hmacVerify(secret, signedData, hexSig);
|
|
460
|
+
if (!valid) {
|
|
461
|
+
throw new VAIAError("Firma HMAC inv\xE1lida", "HMAC_INVALID", 401);
|
|
462
|
+
}
|
|
463
|
+
let body;
|
|
464
|
+
try {
|
|
465
|
+
body = JSON.parse(rawBody);
|
|
466
|
+
} catch {
|
|
467
|
+
throw new VAIAError("Body no es JSON v\xE1lido", "BODY_PARSE_ERROR", 400);
|
|
468
|
+
}
|
|
469
|
+
const ctx = parseContext2(body, callId);
|
|
470
|
+
return { ctx, raw: rawBody };
|
|
471
|
+
}
|
|
472
|
+
function parseContext2(body, fallbackCallId) {
|
|
473
|
+
if (typeof body !== "object" || body === null) {
|
|
474
|
+
throw new VAIAError("Body inv\xE1lido", "BODY_INVALID", 400);
|
|
475
|
+
}
|
|
476
|
+
const b = body;
|
|
477
|
+
const user = b["user"] ?? {};
|
|
478
|
+
const ctx = b["context"] ?? {};
|
|
479
|
+
return {
|
|
480
|
+
capability_id: s(b["capability_id"]),
|
|
481
|
+
call_id: s(b["call_id"], fallbackCallId),
|
|
482
|
+
user: {
|
|
483
|
+
id: s(user["id"]),
|
|
484
|
+
email: sOpt(user["email"]),
|
|
485
|
+
name: sOpt(user["name"])
|
|
486
|
+
},
|
|
487
|
+
permissions: arr2(b["permissions"]),
|
|
488
|
+
trigger: s(ctx["trigger"], "haia_invoke"),
|
|
489
|
+
surface: s(ctx["surface"], "data"),
|
|
490
|
+
query: sOpt(ctx["query"])
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function buildProbeCtx2(callId) {
|
|
494
|
+
return {
|
|
495
|
+
capability_id: "__probe__",
|
|
496
|
+
call_id: callId || crypto.randomUUID(),
|
|
497
|
+
user: { id: "__probe__" },
|
|
498
|
+
permissions: [],
|
|
499
|
+
trigger: "haia_invoke",
|
|
500
|
+
surface: "data"
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function s(v, fallback = "") {
|
|
504
|
+
return typeof v === "string" ? v : fallback;
|
|
505
|
+
}
|
|
506
|
+
function sOpt(v) {
|
|
507
|
+
return typeof v === "string" ? v : void 0;
|
|
508
|
+
}
|
|
509
|
+
function arr2(v) {
|
|
510
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
511
|
+
}
|
|
512
|
+
function isProbe2(request) {
|
|
513
|
+
return request.headers.get(PROBE_HEADER2) === "1";
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// src/handeia/jwt.ts
|
|
517
|
+
var jwt_exports2 = {};
|
|
518
|
+
__export(jwt_exports2, {
|
|
519
|
+
fromUrl: () => fromUrl2,
|
|
520
|
+
sign: () => sign2,
|
|
521
|
+
verify: () => verify4
|
|
522
|
+
});
|
|
523
|
+
var DEFAULT_EXPIRES_IN2 = 3600;
|
|
524
|
+
async function sign2(input, secret, opts = {}) {
|
|
525
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
526
|
+
return jwtSign(
|
|
527
|
+
{
|
|
528
|
+
...input,
|
|
529
|
+
permissions: input.permissions ?? [],
|
|
530
|
+
iat: now,
|
|
531
|
+
exp: now + (opts.expiresIn ?? DEFAULT_EXPIRES_IN2)
|
|
532
|
+
},
|
|
533
|
+
secret
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
async function verify4(token, secret) {
|
|
537
|
+
return jwtVerify(token, secret);
|
|
538
|
+
}
|
|
539
|
+
async function fromUrl2(url, secret) {
|
|
540
|
+
const u = typeof url === "string" ? new URL(url) : url;
|
|
541
|
+
const token = u.searchParams.get("handeia_token");
|
|
542
|
+
if (!token) throw new Error("handeia_token no encontrado en la URL");
|
|
543
|
+
return verify4(token, secret);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// src/define.ts
|
|
547
|
+
function defineCapability(config) {
|
|
548
|
+
const required = ["id", "name", "version", "target", "type", "sector", "permissions", "risk"];
|
|
549
|
+
for (const key of required) {
|
|
550
|
+
if (config[key] === void 0 || config[key] === "") {
|
|
551
|
+
throw new Error(`[@vaia/sdk] defineCapability: campo requerido faltante: '${key}'`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (!config.id.includes(".")) {
|
|
555
|
+
throw new Error(
|
|
556
|
+
`[@vaia/sdk] defineCapability: 'id' debe usar formato reverse-domain (ej. 'mx.mi-capacidad'). Recibido: '${config.id}'`
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
if (Object.keys(config.surfaces).length === 0) {
|
|
560
|
+
throw new Error(`[@vaia/sdk] defineCapability: 'surfaces' no puede estar vac\xEDo. Define al menos un surface con su endpoint.`);
|
|
561
|
+
}
|
|
562
|
+
return config;
|
|
563
|
+
}
|
|
564
|
+
function toManifest(config) {
|
|
565
|
+
const surfaces = Object.keys(config.surfaces);
|
|
566
|
+
return {
|
|
567
|
+
schema: "1.0",
|
|
568
|
+
capability_id: config.id,
|
|
569
|
+
name: config.name,
|
|
570
|
+
version: config.version,
|
|
571
|
+
target: config.target === "both" ? ["gandia", "handeia"] : config.target,
|
|
572
|
+
type: config.type,
|
|
573
|
+
level: config.level,
|
|
574
|
+
sector: config.sector,
|
|
575
|
+
surfaces,
|
|
576
|
+
permissions: config.permissions,
|
|
577
|
+
risk: config.risk,
|
|
578
|
+
has_own_auth: config.has_own_auth ?? false,
|
|
579
|
+
stores_data: config.stores_data ?? false,
|
|
580
|
+
trains_models: config.trains_models ?? false,
|
|
581
|
+
requires_consent: config.requires_consent ?? false,
|
|
582
|
+
description: config.description,
|
|
583
|
+
tags: config.tags,
|
|
584
|
+
linked: config.target === "both",
|
|
585
|
+
generated_by: "@vaia/sdk",
|
|
586
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// src/index.ts
|
|
591
|
+
var gandia = gandia_exports;
|
|
592
|
+
var handeia = handeia_exports;
|
|
593
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
594
|
+
0 && (module.exports = {
|
|
595
|
+
VAIAError,
|
|
596
|
+
defineCapability,
|
|
597
|
+
gandia,
|
|
598
|
+
handeia,
|
|
599
|
+
toManifest
|
|
600
|
+
});
|
|
601
|
+
//# sourceMappingURL=index.cjs.map
|