@apitap/core 1.0.17 → 1.0.19
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/auth/handoff.d.ts +12 -0
- package/dist/auth/handoff.js +70 -3
- package/dist/auth/handoff.js.map +1 -1
- package/dist/auth/manager.d.ts +6 -0
- package/dist/auth/manager.js +28 -0
- package/dist/auth/manager.js.map +1 -1
- package/dist/auth/oauth-refresh.js +6 -0
- package/dist/auth/oauth-refresh.js.map +1 -1
- package/dist/contract/diff.d.ts +15 -0
- package/dist/contract/diff.js +65 -0
- package/dist/contract/diff.js.map +1 -0
- package/dist/contract/schema.d.ts +8 -0
- package/dist/contract/schema.js +38 -0
- package/dist/contract/schema.js.map +1 -0
- package/dist/mcp.js +2 -1
- package/dist/mcp.js.map +1 -1
- package/dist/replay/engine.d.ts +3 -0
- package/dist/replay/engine.js +47 -8
- package/dist/replay/engine.js.map +1 -1
- package/dist/skill/generator.js +7 -0
- package/dist/skill/generator.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/package.json +1 -1
- package/src/auth/handoff.ts +86 -3
- package/src/auth/manager.ts +29 -0
- package/src/auth/oauth-refresh.ts +7 -0
- package/src/contract/diff.ts +80 -0
- package/src/contract/schema.ts +44 -0
- package/src/mcp.ts +2 -1
- package/src/replay/engine.ts +50 -8
- package/src/skill/generator.ts +7 -0
- package/src/types.ts +12 -0
package/src/replay/engine.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { parseJwtClaims } from '../capture/entropy.js';
|
|
|
6
6
|
import { refreshTokens } from '../auth/refresh.js';
|
|
7
7
|
import { truncateResponse } from './truncate.js';
|
|
8
8
|
import { resolveAndValidateUrl } from '../skill/ssrf.js';
|
|
9
|
+
import { snapshotSchema } from '../contract/schema.js';
|
|
10
|
+
import { diffSchema, type ContractWarning } from '../contract/diff.js';
|
|
9
11
|
|
|
10
12
|
// Header security: block dangerous headers from skill files (blocklist approach).
|
|
11
13
|
// All other headers — including custom API headers like Client-ID — pass through.
|
|
@@ -66,6 +68,8 @@ export interface ReplayResult {
|
|
|
66
68
|
refreshed?: boolean;
|
|
67
69
|
/** Whether the response was truncated to fit maxBytes */
|
|
68
70
|
truncated?: boolean;
|
|
71
|
+
/** Contract warnings from schema drift detection */
|
|
72
|
+
contractWarnings?: ContractWarning[];
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
/**
|
|
@@ -228,7 +232,9 @@ export async function replayEndpoint(
|
|
|
228
232
|
|
|
229
233
|
// Inject auth header from auth manager (if available)
|
|
230
234
|
if (authManager && domain) {
|
|
231
|
-
const auth =
|
|
235
|
+
const auth = endpoint.isolatedAuth
|
|
236
|
+
? await authManager.retrieve(domain)
|
|
237
|
+
: await authManager.retrieveWithFallback(domain);
|
|
232
238
|
if (auth && auth.header && auth.value) {
|
|
233
239
|
headers[auth.header] = auth.value;
|
|
234
240
|
}
|
|
@@ -282,15 +288,36 @@ export async function replayEndpoint(
|
|
|
282
288
|
if (refreshResult.success) {
|
|
283
289
|
refreshed = true;
|
|
284
290
|
// Re-inject fresh auth header
|
|
285
|
-
const freshAuth =
|
|
291
|
+
const freshAuth = endpoint.isolatedAuth
|
|
292
|
+
? await authManager.retrieve(domain)
|
|
293
|
+
: await authManager.retrieveWithFallback(domain);
|
|
286
294
|
if (freshAuth) {
|
|
287
295
|
headers[freshAuth.header] = freshAuth.value;
|
|
288
296
|
}
|
|
289
297
|
}
|
|
290
298
|
} else {
|
|
291
|
-
// Proactive
|
|
292
|
-
const currentAuth =
|
|
293
|
-
|
|
299
|
+
// Proactive expiry check (30s buffer for clock skew)
|
|
300
|
+
const currentAuth = endpoint.isolatedAuth
|
|
301
|
+
? await authManager.retrieve(domain)
|
|
302
|
+
: await authManager.retrieveWithFallback(domain);
|
|
303
|
+
|
|
304
|
+
if (currentAuth?.expiresAt) {
|
|
305
|
+
// Check 1: expiresAt from OAuth/stored TTL (handles opaque tokens)
|
|
306
|
+
const expiresAtMs = new Date(currentAuth.expiresAt).getTime();
|
|
307
|
+
if (expiresAtMs < Date.now() + 30_000) {
|
|
308
|
+
const refreshResult = await refreshTokens(skill, authManager, { domain, _skipSsrfCheck: options._skipSsrfCheck });
|
|
309
|
+
if (refreshResult.success) {
|
|
310
|
+
refreshed = true;
|
|
311
|
+
const freshAuth = endpoint.isolatedAuth
|
|
312
|
+
? await authManager.retrieve(domain)
|
|
313
|
+
: await authManager.retrieveWithFallback(domain);
|
|
314
|
+
if (freshAuth) {
|
|
315
|
+
headers[freshAuth.header] = freshAuth.value;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
} else if (currentAuth?.value) {
|
|
320
|
+
// Check 2: JWT exp claim (existing logic)
|
|
294
321
|
const raw = currentAuth.value.startsWith('Bearer ')
|
|
295
322
|
? currentAuth.value.slice(7)
|
|
296
323
|
: currentAuth.value;
|
|
@@ -299,7 +326,9 @@ export async function replayEndpoint(
|
|
|
299
326
|
const refreshResult = await refreshTokens(skill, authManager, { domain, _skipSsrfCheck: options._skipSsrfCheck });
|
|
300
327
|
if (refreshResult.success) {
|
|
301
328
|
refreshed = true;
|
|
302
|
-
const freshAuth =
|
|
329
|
+
const freshAuth = endpoint.isolatedAuth
|
|
330
|
+
? await authManager.retrieve(domain)
|
|
331
|
+
: await authManager.retrieveWithFallback(domain);
|
|
303
332
|
if (freshAuth) {
|
|
304
333
|
headers[freshAuth.header] = freshAuth.value;
|
|
305
334
|
}
|
|
@@ -350,7 +379,9 @@ export async function replayEndpoint(
|
|
|
350
379
|
if (refreshResult.success) {
|
|
351
380
|
refreshed = true;
|
|
352
381
|
// Re-inject fresh auth
|
|
353
|
-
const freshAuth =
|
|
382
|
+
const freshAuth = endpoint.isolatedAuth
|
|
383
|
+
? await authManager.retrieve(domain)
|
|
384
|
+
: await authManager.retrieveWithFallback(domain);
|
|
354
385
|
if (freshAuth) {
|
|
355
386
|
headers[freshAuth.header] = freshAuth.value;
|
|
356
387
|
}
|
|
@@ -441,6 +472,16 @@ export async function replayEndpoint(
|
|
|
441
472
|
? wrapAuthError(response.status, data, skill.domain)
|
|
442
473
|
: data;
|
|
443
474
|
|
|
475
|
+
// Contract validation: diff response schema against captured baseline
|
|
476
|
+
let contractWarnings: ContractWarning[] | undefined;
|
|
477
|
+
if (endpoint.responseSchema && typeof data === 'object' && data !== null) {
|
|
478
|
+
const actualSchema = snapshotSchema(data);
|
|
479
|
+
const warnings = diffSchema(endpoint.responseSchema, actualSchema);
|
|
480
|
+
if (warnings.length > 0) {
|
|
481
|
+
contractWarnings = warnings;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
444
485
|
// Apply truncation if maxBytes is set
|
|
445
486
|
if (options.maxBytes) {
|
|
446
487
|
const truncated = truncateResponse(finalData, { maxBytes: options.maxBytes });
|
|
@@ -450,10 +491,11 @@ export async function replayEndpoint(
|
|
|
450
491
|
data: truncated.data,
|
|
451
492
|
...(refreshed ? { refreshed } : {}),
|
|
452
493
|
...(truncated.truncated ? { truncated: true } : {}),
|
|
494
|
+
...(contractWarnings ? { contractWarnings } : {}),
|
|
453
495
|
};
|
|
454
496
|
}
|
|
455
497
|
|
|
456
|
-
return { status: response.status, headers: responseHeaders, data: finalData, ...(refreshed ? { refreshed } : {}) };
|
|
498
|
+
return { status: response.status, headers: responseHeaders, data: finalData, ...(refreshed ? { refreshed } : {}), ...(contractWarnings ? { contractWarnings } : {}) };
|
|
457
499
|
}
|
|
458
500
|
|
|
459
501
|
// --- Batch replay ---
|
package/src/skill/generator.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { detectRefreshableTokens } from '../capture/token-detector.js';
|
|
|
9
9
|
import { isLikelyToken } from '../capture/entropy.js';
|
|
10
10
|
import { isOAuthTokenRequest, type OAuthInfo } from '../capture/oauth-detector.js';
|
|
11
11
|
import { diffBodies } from '../capture/body-diff.js';
|
|
12
|
+
import { snapshotSchema } from '../contract/schema.js';
|
|
12
13
|
|
|
13
14
|
/** Headers to strip (connection control, forwarding, browser-internal, encoding) */
|
|
14
15
|
const STRIP_HEADERS = new Set([
|
|
@@ -395,6 +396,12 @@ export class SkillGenerator {
|
|
|
395
396
|
// Store response bytes on endpoint
|
|
396
397
|
endpoint.responseBytes = exchange.response.body.length;
|
|
397
398
|
|
|
399
|
+
// Snapshot response schema for contract validation
|
|
400
|
+
try {
|
|
401
|
+
const parsed = JSON.parse(exchange.response.body);
|
|
402
|
+
endpoint.responseSchema = snapshotSchema(parsed);
|
|
403
|
+
} catch { /* non-JSON response, skip schema */ }
|
|
404
|
+
|
|
398
405
|
this.endpoints.set(key, endpoint);
|
|
399
406
|
|
|
400
407
|
// Store first body for cross-request diffing
|
package/src/types.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface StoredAuth {
|
|
|
29
29
|
// v0.9: OAuth credentials (stored encrypted, never in skill file)
|
|
30
30
|
refreshToken?: string;
|
|
31
31
|
clientSecret?: string;
|
|
32
|
+
// v1.1: computed expiry timestamp
|
|
33
|
+
expiresAt?: string;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/**
|
|
@@ -101,6 +103,14 @@ export interface RequestBody {
|
|
|
101
103
|
refreshableTokens?: string[]; // v0.8: system-refreshed tokens
|
|
102
104
|
}
|
|
103
105
|
|
|
106
|
+
/** Schema node for contract validation (recursive type tree) */
|
|
107
|
+
export interface SchemaNode {
|
|
108
|
+
type: 'object' | 'array' | 'string' | 'number' | 'boolean' | 'null';
|
|
109
|
+
fields?: Record<string, SchemaNode>;
|
|
110
|
+
items?: SchemaNode;
|
|
111
|
+
nullable?: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
104
114
|
/** A single API endpoint in a skill file */
|
|
105
115
|
export interface SkillEndpoint {
|
|
106
116
|
id: string;
|
|
@@ -117,6 +127,8 @@ export interface SkillEndpoint {
|
|
|
117
127
|
pagination?: PaginationInfo;
|
|
118
128
|
requestBody?: RequestBody;
|
|
119
129
|
responseBytes?: number; // v1.0: response body size in bytes
|
|
130
|
+
isolatedAuth?: boolean; // v1.1: opt out of cross-subdomain auth fallback
|
|
131
|
+
responseSchema?: SchemaNode; // v1.1: schema snapshot for contract validation
|
|
120
132
|
}
|
|
121
133
|
|
|
122
134
|
/** The full skill file written to disk */
|