@nordsym/apiclaw 2.0.0 → 2.2.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/.claude/settings.local.json +5 -1
- package/convex/_generated/api.d.ts +8 -0
- package/convex/_listWorkspaces.ts +13 -0
- package/convex/crons.ts +15 -0
- package/convex/funnel.ts +431 -0
- package/convex/guards.ts +174 -0
- package/convex/http.ts +334 -8
- package/convex/nurture.ts +355 -0
- package/convex/schema.ts +70 -0
- package/convex/workspaces.ts +185 -0
- package/dist/funnel-client.d.ts +24 -0
- package/dist/funnel-client.d.ts.map +1 -0
- package/dist/funnel-client.js +131 -0
- package/dist/funnel-client.js.map +1 -0
- package/dist/funnel.test.d.ts +2 -0
- package/dist/funnel.test.d.ts.map +1 -0
- package/dist/funnel.test.js +145 -0
- package/dist/funnel.test.js.map +1 -0
- package/dist/index.js +338 -120
- package/dist/index.js.map +1 -1
- package/dist/postinstall.d.ts +0 -5
- package/dist/postinstall.d.ts.map +1 -1
- package/dist/postinstall.js +24 -3
- package/dist/postinstall.js.map +1 -1
- package/dist/registration-guard.d.ts +29 -0
- package/dist/registration-guard.d.ts.map +1 -0
- package/dist/registration-guard.js +87 -0
- package/dist/registration-guard.js.map +1 -0
- package/package.json +1 -1
- package/src/funnel-client.ts +168 -0
- package/src/funnel.test.ts +187 -0
- package/src/index.ts +381 -145
- package/src/postinstall.ts +24 -2
- package/src/registration-guard.ts +117 -0
package/convex/http.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { httpRouter } from "convex/server";
|
|
2
2
|
import { httpAction } from "./_generated/server";
|
|
3
3
|
import { api, internal } from "./_generated/api";
|
|
4
|
+
import { resolveVerifiedOwnerByWorkspaceId } from "./guards";
|
|
4
5
|
import {
|
|
5
6
|
createCheckoutSession,
|
|
6
7
|
createPortalSession,
|
|
@@ -2347,6 +2348,27 @@ async function requireApiKeyAuth(
|
|
|
2347
2348
|
401
|
|
2348
2349
|
);
|
|
2349
2350
|
}
|
|
2351
|
+
// Verified-owner gate: API key alone isn't enough — workspace must be active+verified.
|
|
2352
|
+
const verified = await resolveVerifiedOwnerByWorkspaceId(ctx, auth.workspaceId);
|
|
2353
|
+
if (!verified.ok) {
|
|
2354
|
+
// Fire-and-forget blocked diagnostic.
|
|
2355
|
+
ctx.runMutation(api.funnel.recordEvent, {
|
|
2356
|
+
event: "call_api_blocked",
|
|
2357
|
+
classification: "human",
|
|
2358
|
+
workspaceId: auth.workspaceId as any,
|
|
2359
|
+
props: { reason: verified.reason, channel: "http:chat_or_embed" },
|
|
2360
|
+
}).catch(() => {});
|
|
2361
|
+
return jsonResponse(
|
|
2362
|
+
{
|
|
2363
|
+
error: {
|
|
2364
|
+
message: verified.message,
|
|
2365
|
+
type: "verification_required",
|
|
2366
|
+
code: verified.reason,
|
|
2367
|
+
},
|
|
2368
|
+
},
|
|
2369
|
+
verified.reason === "quota_exceeded" ? 429 : 403
|
|
2370
|
+
);
|
|
2371
|
+
}
|
|
2350
2372
|
return { workspaceId: auth.workspaceId, keyId: auth.keyId };
|
|
2351
2373
|
}
|
|
2352
2374
|
|
|
@@ -3023,6 +3045,273 @@ function buildManagedRequest(
|
|
|
3023
3045
|
}
|
|
3024
3046
|
return null;
|
|
3025
3047
|
}
|
|
3048
|
+
case "apilayer": {
|
|
3049
|
+
// 14 unified APILayer actions + popular legacy APIs — ported from src/execute.ts
|
|
3050
|
+
// Reads product-specific env keys where APILayer requires them; falls back to unified.
|
|
3051
|
+
const p = (params as Record<string, any>) || {};
|
|
3052
|
+
const buildUrl = (base: string, qs?: Record<string, any>) => {
|
|
3053
|
+
const u = new URL(base);
|
|
3054
|
+
if (qs) for (const [k, v] of Object.entries(qs)) if (v !== undefined && v !== null && v !== "") u.searchParams.set(k, String(v));
|
|
3055
|
+
return u.toString();
|
|
3056
|
+
};
|
|
3057
|
+
const envKey = (name: string) => process.env[name] || apiKey;
|
|
3058
|
+
|
|
3059
|
+
switch (action) {
|
|
3060
|
+
// Unified (apikey header)
|
|
3061
|
+
case "exchange_rates": {
|
|
3062
|
+
const endpoint = p.date ? "historical" : "latest";
|
|
3063
|
+
return {
|
|
3064
|
+
url: buildUrl(`https://api.apilayer.com/exchangerates_data/${endpoint}`, { base: p.base || "USD", symbols: p.symbols, date: p.date }),
|
|
3065
|
+
method: "GET",
|
|
3066
|
+
headers: { apikey: apiKey },
|
|
3067
|
+
};
|
|
3068
|
+
}
|
|
3069
|
+
case "verify_email":
|
|
3070
|
+
if (!p.email) return null;
|
|
3071
|
+
return {
|
|
3072
|
+
url: buildUrl("https://api.apilayer.com/email_verification/check", { email: p.email }),
|
|
3073
|
+
method: "GET",
|
|
3074
|
+
headers: { apikey: apiKey },
|
|
3075
|
+
};
|
|
3076
|
+
case "verify_number":
|
|
3077
|
+
if (!p.number) return null;
|
|
3078
|
+
return {
|
|
3079
|
+
url: buildUrl("https://api.apilayer.com/number_verification/validate", { number: p.number }),
|
|
3080
|
+
method: "GET",
|
|
3081
|
+
headers: { apikey: apiKey },
|
|
3082
|
+
};
|
|
3083
|
+
case "world_news":
|
|
3084
|
+
if (!p.url) return null;
|
|
3085
|
+
return {
|
|
3086
|
+
url: buildUrl("https://api.apilayer.com/world_news/extract-news", { url: p.url, analyze: p.analyze !== false ? "true" : "false" }),
|
|
3087
|
+
method: "GET",
|
|
3088
|
+
headers: { apikey: apiKey },
|
|
3089
|
+
};
|
|
3090
|
+
case "finance_news":
|
|
3091
|
+
return {
|
|
3092
|
+
url: buildUrl("https://api.apilayer.com/financelayer/news", { tickers: p.tickers, keywords: p.text, limit: p.number || 5 }),
|
|
3093
|
+
method: "GET",
|
|
3094
|
+
headers: { apikey: apiKey },
|
|
3095
|
+
};
|
|
3096
|
+
case "scrape":
|
|
3097
|
+
if (!p.url) return null;
|
|
3098
|
+
return {
|
|
3099
|
+
url: buildUrl("https://api.apilayer.com/adv_scraper/scraper", { url: p.url }),
|
|
3100
|
+
method: "GET",
|
|
3101
|
+
headers: { apikey: apiKey },
|
|
3102
|
+
};
|
|
3103
|
+
case "skills":
|
|
3104
|
+
if (!p.q) return null;
|
|
3105
|
+
return {
|
|
3106
|
+
url: buildUrl("https://api.promptapi.com/skills", { q: p.q, count: p.count }),
|
|
3107
|
+
method: "GET",
|
|
3108
|
+
headers: { apikey: apiKey },
|
|
3109
|
+
};
|
|
3110
|
+
case "image_crop": {
|
|
3111
|
+
if (!p.url) return null;
|
|
3112
|
+
const formData = new URLSearchParams();
|
|
3113
|
+
formData.set("url", p.url);
|
|
3114
|
+
if (p.width) formData.set("width", String(p.width));
|
|
3115
|
+
if (p.height) formData.set("height", String(p.height));
|
|
3116
|
+
return {
|
|
3117
|
+
url: "https://api.apilayer.com/smart_crop/url",
|
|
3118
|
+
method: "POST",
|
|
3119
|
+
headers: { apikey: apiKey, "Content-Type": "application/x-www-form-urlencoded" },
|
|
3120
|
+
body: formData.toString(),
|
|
3121
|
+
};
|
|
3122
|
+
}
|
|
3123
|
+
case "form_submit": {
|
|
3124
|
+
if (!p.endpoint) return null;
|
|
3125
|
+
return {
|
|
3126
|
+
url: `https://api.apilayer.com/form_api/${p.endpoint}`,
|
|
3127
|
+
method: "POST",
|
|
3128
|
+
headers: { apikey: apiKey, "Content-Type": "application/json" },
|
|
3129
|
+
body: JSON.stringify(p.data || {}),
|
|
3130
|
+
};
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
// Product-specific access_key query (legacy domain) — binary response
|
|
3134
|
+
case "pdf_generate": {
|
|
3135
|
+
if (!p.document_url && !p.document_html) return null;
|
|
3136
|
+
const pdfKey = envKey("PDFLAYER_API_KEY");
|
|
3137
|
+
const url = buildUrl("https://api.pdflayer.com/api/convert", {
|
|
3138
|
+
access_key: pdfKey,
|
|
3139
|
+
page_size: p.page_size || "A4",
|
|
3140
|
+
document_url: p.document_url,
|
|
3141
|
+
});
|
|
3142
|
+
if (p.document_html && !p.document_url) {
|
|
3143
|
+
return {
|
|
3144
|
+
url: buildUrl("https://api.pdflayer.com/api/convert", { access_key: pdfKey, page_size: p.page_size || "A4" }),
|
|
3145
|
+
method: "POST",
|
|
3146
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
3147
|
+
body: `document_html=${encodeURIComponent(p.document_html)}`,
|
|
3148
|
+
};
|
|
3149
|
+
}
|
|
3150
|
+
return { url, method: "GET", headers: {} };
|
|
3151
|
+
}
|
|
3152
|
+
case "screenshot": {
|
|
3153
|
+
if (!p.url) return null;
|
|
3154
|
+
return {
|
|
3155
|
+
url: buildUrl("https://api.screenshotlayer.com/api/capture", {
|
|
3156
|
+
access_key: envKey("SCREENSHOTLAYER_API_KEY"),
|
|
3157
|
+
url: p.url,
|
|
3158
|
+
viewport: p.viewport || "1440x900",
|
|
3159
|
+
fullpage: p.fullpage ? "1" : "0",
|
|
3160
|
+
}),
|
|
3161
|
+
method: "GET",
|
|
3162
|
+
headers: {},
|
|
3163
|
+
};
|
|
3164
|
+
}
|
|
3165
|
+
case "vat_check": {
|
|
3166
|
+
if (!p.vat_number) return null;
|
|
3167
|
+
return {
|
|
3168
|
+
url: buildUrl("http://apilayer.net/api/validate", { access_key: envKey("VATLAYER_API_KEY"), vat_number: p.vat_number }),
|
|
3169
|
+
method: "GET",
|
|
3170
|
+
headers: {},
|
|
3171
|
+
};
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
// Legacy domains (each uses product-specific access_key query param)
|
|
3175
|
+
case "market_data": {
|
|
3176
|
+
if (!p.symbols) return null;
|
|
3177
|
+
return {
|
|
3178
|
+
url: buildUrl("http://api.marketstack.com/v1/eod", {
|
|
3179
|
+
access_key: envKey("MARKETSTACK_API_KEY"), symbols: p.symbols, limit: p.limit || 10, date_from: p.date_from, date_to: p.date_to,
|
|
3180
|
+
}),
|
|
3181
|
+
method: "GET", headers: {},
|
|
3182
|
+
};
|
|
3183
|
+
}
|
|
3184
|
+
case "aviation": {
|
|
3185
|
+
return {
|
|
3186
|
+
url: buildUrl("http://api.aviationstack.com/v1/flights", {
|
|
3187
|
+
access_key: envKey("AVIATIONSTACK_API_KEY"), flight_iata: p.flight_iata, dep_iata: p.dep_iata, arr_iata: p.arr_iata, airline_iata: p.airline_iata,
|
|
3188
|
+
}),
|
|
3189
|
+
method: "GET", headers: {},
|
|
3190
|
+
};
|
|
3191
|
+
}
|
|
3192
|
+
case "weatherstack_current":
|
|
3193
|
+
case "weather": {
|
|
3194
|
+
if (!p.query) return null;
|
|
3195
|
+
return {
|
|
3196
|
+
url: buildUrl("http://api.weatherstack.com/current", { access_key: envKey("WEATHERSTACK_API_KEY"), query: p.query, units: p.units || "m" }),
|
|
3197
|
+
method: "GET", headers: {},
|
|
3198
|
+
};
|
|
3199
|
+
}
|
|
3200
|
+
case "weatherstack_forecast": {
|
|
3201
|
+
if (!p.query) return null;
|
|
3202
|
+
return {
|
|
3203
|
+
url: buildUrl("http://api.weatherstack.com/forecast", { access_key: envKey("WEATHERSTACK_API_KEY"), query: p.query, forecast_days: p.forecast_days || 3 }),
|
|
3204
|
+
method: "GET", headers: {},
|
|
3205
|
+
};
|
|
3206
|
+
}
|
|
3207
|
+
case "ipstack_lookup": {
|
|
3208
|
+
if (!p.ip) return null;
|
|
3209
|
+
return {
|
|
3210
|
+
url: buildUrl(`http://api.ipstack.com/${encodeURIComponent(p.ip)}`, { access_key: envKey("IPSTACK_API_KEY") }),
|
|
3211
|
+
method: "GET", headers: {},
|
|
3212
|
+
};
|
|
3213
|
+
}
|
|
3214
|
+
case "ipapi_lookup": {
|
|
3215
|
+
if (!p.ip) return null;
|
|
3216
|
+
return {
|
|
3217
|
+
url: buildUrl(`https://api.ipapi.com/api/${encodeURIComponent(p.ip)}`, { access_key: envKey("IPAPI_API_KEY") }),
|
|
3218
|
+
method: "GET", headers: {},
|
|
3219
|
+
};
|
|
3220
|
+
}
|
|
3221
|
+
case "currencylayer_live": {
|
|
3222
|
+
return {
|
|
3223
|
+
url: buildUrl("http://api.currencylayer.com/live", { access_key: envKey("CURRENCYLAYER_API_KEY"), source: p.source || "USD", currencies: p.currencies }),
|
|
3224
|
+
method: "GET", headers: {},
|
|
3225
|
+
};
|
|
3226
|
+
}
|
|
3227
|
+
case "currencylayer_convert": {
|
|
3228
|
+
if (!p.from || !p.to || !p.amount) return null;
|
|
3229
|
+
return {
|
|
3230
|
+
url: buildUrl("http://api.currencylayer.com/convert", {
|
|
3231
|
+
access_key: envKey("CURRENCYLAYER_API_KEY"), from: p.from, to: p.to, amount: p.amount, date: p.date,
|
|
3232
|
+
}),
|
|
3233
|
+
method: "GET", headers: {},
|
|
3234
|
+
};
|
|
3235
|
+
}
|
|
3236
|
+
case "coinlayer_live": {
|
|
3237
|
+
return {
|
|
3238
|
+
url: buildUrl("http://api.coinlayer.com/live", { access_key: envKey("COINLAYER_API_KEY"), target: p.target || "USD", symbols: p.symbols }),
|
|
3239
|
+
method: "GET", headers: {},
|
|
3240
|
+
};
|
|
3241
|
+
}
|
|
3242
|
+
case "positionstack_forward": {
|
|
3243
|
+
if (!p.query) return null;
|
|
3244
|
+
return {
|
|
3245
|
+
url: buildUrl("http://api.positionstack.com/v1/forward", { access_key: envKey("POSITIONSTACK_API_KEY"), query: p.query, limit: p.limit || 1 }),
|
|
3246
|
+
method: "GET", headers: {},
|
|
3247
|
+
};
|
|
3248
|
+
}
|
|
3249
|
+
case "positionstack_reverse": {
|
|
3250
|
+
if (!p.query) return null;
|
|
3251
|
+
return {
|
|
3252
|
+
url: buildUrl("http://api.positionstack.com/v1/reverse", { access_key: envKey("POSITIONSTACK_API_KEY"), query: p.query, limit: p.limit || 1 }),
|
|
3253
|
+
method: "GET", headers: {},
|
|
3254
|
+
};
|
|
3255
|
+
}
|
|
3256
|
+
case "fixer_latest": {
|
|
3257
|
+
return {
|
|
3258
|
+
url: buildUrl("http://data.fixer.io/api/latest", { access_key: envKey("FIXER_API_KEY"), base: p.base || "EUR", symbols: p.symbols }),
|
|
3259
|
+
method: "GET", headers: {},
|
|
3260
|
+
};
|
|
3261
|
+
}
|
|
3262
|
+
case "fixer_convert": {
|
|
3263
|
+
if (!p.from || !p.to || !p.amount) return null;
|
|
3264
|
+
return {
|
|
3265
|
+
url: buildUrl("http://data.fixer.io/api/convert", {
|
|
3266
|
+
access_key: envKey("FIXER_API_KEY"), from: p.from, to: p.to, amount: p.amount, date: p.date,
|
|
3267
|
+
}),
|
|
3268
|
+
method: "GET", headers: {},
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
case "languagelayer_detect": {
|
|
3272
|
+
if (!p.query) return null;
|
|
3273
|
+
return {
|
|
3274
|
+
url: buildUrl("http://api.languagelayer.com/detect", { access_key: envKey("LANGUAGELAYER_API_KEY"), query: p.query }),
|
|
3275
|
+
method: "GET", headers: {},
|
|
3276
|
+
};
|
|
3277
|
+
}
|
|
3278
|
+
case "scrapestack_scrape": {
|
|
3279
|
+
if (!p.url) return null;
|
|
3280
|
+
return {
|
|
3281
|
+
url: buildUrl("http://api.scrapestack.com/scrape", { access_key: envKey("SCRAPESTACK_API_KEY"), url: p.url, render_js: p.render_js ? "1" : "0" }),
|
|
3282
|
+
method: "GET", headers: {},
|
|
3283
|
+
};
|
|
3284
|
+
}
|
|
3285
|
+
case "serpstack_search": {
|
|
3286
|
+
if (!p.query) return null;
|
|
3287
|
+
return {
|
|
3288
|
+
url: buildUrl("http://api.serpstack.com/search", { access_key: envKey("SERPSTACK_API_KEY"), query: p.query, num: p.num || 10 }),
|
|
3289
|
+
method: "GET", headers: {},
|
|
3290
|
+
};
|
|
3291
|
+
}
|
|
3292
|
+
case "mediastack_news": {
|
|
3293
|
+
return {
|
|
3294
|
+
url: buildUrl("http://api.mediastack.com/v1/news", { access_key: envKey("MEDIASTACK_API_KEY"), keywords: p.keywords, categories: p.categories, limit: p.limit || 10 }),
|
|
3295
|
+
method: "GET", headers: {},
|
|
3296
|
+
};
|
|
3297
|
+
}
|
|
3298
|
+
case "userstack_detect": {
|
|
3299
|
+
if (!p.ua) return null;
|
|
3300
|
+
return {
|
|
3301
|
+
url: buildUrl("http://api.userstack.com/detect", { access_key: envKey("USERSTACK_API_KEY"), ua: p.ua }),
|
|
3302
|
+
method: "GET", headers: {},
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
3305
|
+
case "exchangeratehost_latest": {
|
|
3306
|
+
return {
|
|
3307
|
+
url: buildUrl("https://api.exchangerate.host/live", { access_key: envKey("EXCHANGERATEHOST_API_KEY"), source: p.source || "USD", currencies: p.currencies }),
|
|
3308
|
+
method: "GET", headers: {},
|
|
3309
|
+
};
|
|
3310
|
+
}
|
|
3311
|
+
default:
|
|
3312
|
+
return null;
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3026
3315
|
default:
|
|
3027
3316
|
return null;
|
|
3028
3317
|
}
|
|
@@ -3048,6 +3337,20 @@ async function resolveExecuteAuth(
|
|
|
3048
3337
|
// 2. Check for API key auth (Bearer sk-claw-...)
|
|
3049
3338
|
const auth = await resolveWorkspaceFromRequest(ctx, request);
|
|
3050
3339
|
if (auth.authMethod === "api-key" && auth.workspaceId && auth.keyId) {
|
|
3340
|
+
// Verified-owner gate: API key alone isn't enough — workspace must be active+verified.
|
|
3341
|
+
const verified = await resolveVerifiedOwnerByWorkspaceId(ctx, auth.workspaceId);
|
|
3342
|
+
if (!verified.ok) {
|
|
3343
|
+
ctx.runMutation(api.funnel.recordEvent, {
|
|
3344
|
+
event: "call_api_blocked",
|
|
3345
|
+
classification: "human",
|
|
3346
|
+
workspaceId: auth.workspaceId as any,
|
|
3347
|
+
props: { reason: verified.reason, channel: "http:execute" },
|
|
3348
|
+
}).catch(() => {});
|
|
3349
|
+
return jsonResponse(
|
|
3350
|
+
{ error: { message: verified.message, type: "verification_required", code: verified.reason } },
|
|
3351
|
+
verified.reason === "quota_exceeded" ? 429 : 403
|
|
3352
|
+
);
|
|
3353
|
+
}
|
|
3051
3354
|
return { workspaceId: auth.workspaceId, keyId: auth.keyId, authMethod: "api-key" };
|
|
3052
3355
|
}
|
|
3053
3356
|
|
|
@@ -3271,20 +3574,43 @@ http.route({
|
|
|
3271
3574
|
const response = await fetch(req.url, fetchOpts);
|
|
3272
3575
|
const latencyMs = Date.now() - startTime;
|
|
3273
3576
|
|
|
3274
|
-
// Handle binary responses (
|
|
3577
|
+
// Handle binary responses (audio, PDF, image, octet-stream)
|
|
3275
3578
|
const contentType = response.headers.get("Content-Type") || "";
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3579
|
+
const isBinary =
|
|
3580
|
+
contentType.includes("audio/") ||
|
|
3581
|
+
contentType.includes("image/") ||
|
|
3582
|
+
contentType.includes("application/pdf") ||
|
|
3583
|
+
contentType.includes("application/octet-stream");
|
|
3584
|
+
if (isBinary) {
|
|
3585
|
+
const buf = await response.arrayBuffer();
|
|
3586
|
+
const bytes = new Uint8Array(buf);
|
|
3587
|
+
let binary = "";
|
|
3588
|
+
const chunk = 0x8000;
|
|
3589
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
3590
|
+
binary += String.fromCharCode.apply(null, Array.from(bytes.subarray(i, i + chunk)) as any);
|
|
3591
|
+
}
|
|
3592
|
+
const base64 = btoa(binary);
|
|
3593
|
+
return jsonResponse({
|
|
3594
|
+
success: response.ok,
|
|
3595
|
+
provider,
|
|
3596
|
+
action,
|
|
3597
|
+
data: {
|
|
3598
|
+
message: response.ok ? "Binary asset returned" : "Binary error",
|
|
3599
|
+
content_type: contentType,
|
|
3600
|
+
size: buf.byteLength,
|
|
3601
|
+
base64,
|
|
3602
|
+
},
|
|
3603
|
+
_apiclaw: { latencyMs, route: routeDetail, gateway: true },
|
|
3604
|
+
}, response.ok ? 200 : response.status);
|
|
3281
3605
|
}
|
|
3282
3606
|
|
|
3607
|
+
// For text/json responses read once as text then try json parse
|
|
3608
|
+
const raw = await response.text();
|
|
3283
3609
|
let data: any;
|
|
3284
3610
|
try {
|
|
3285
|
-
data =
|
|
3611
|
+
data = JSON.parse(raw);
|
|
3286
3612
|
} catch {
|
|
3287
|
-
data = { raw
|
|
3613
|
+
data = { raw };
|
|
3288
3614
|
}
|
|
3289
3615
|
|
|
3290
3616
|
return jsonResponse({
|