@amirhosseinnateghi/vibed-cli 0.1.0 → 0.1.2
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/index.js +268 -140
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -6018,141 +6018,6 @@ import { dirname, extname, isAbsolute, join as join2, resolve } from "node:path"
|
|
|
6018
6018
|
|
|
6019
6019
|
// ../sandbox/src/index.ts
|
|
6020
6020
|
var import_node_html_parser = __toESM(require_dist(), 1);
|
|
6021
|
-
var FORBIDDEN_API_PATTERNS = [
|
|
6022
|
-
{ re: /getUserMedia/, name: "getUserMedia" },
|
|
6023
|
-
{ re: /navigator\s*\.\s*geolocation/, name: "geolocation" },
|
|
6024
|
-
{ re: /new\s+Notification|Notification\s*\.\s*requestPermission/, name: "Notification" },
|
|
6025
|
-
{ re: /serviceWorker/, name: "serviceWorker" },
|
|
6026
|
-
{ re: /openDatabase/, name: "openDatabase" },
|
|
6027
|
-
{ re: /PaymentRequest|navigator\s*\.\s*payments/, name: "PaymentRequest" },
|
|
6028
|
-
{ re: /navigator\s*\.\s*credentials|PublicKeyCredential|WebAuthn/, name: "WebAuthn" },
|
|
6029
|
-
{ re: /requestPointerLock/, name: "pointerLock" }
|
|
6030
|
-
// allowed via gesture only; flag
|
|
6031
|
-
];
|
|
6032
|
-
var TOP_NAV_PATTERNS = [
|
|
6033
|
-
/window\s*\.\s*top/,
|
|
6034
|
-
/window\s*\.\s*parent\s*\.\s*location/,
|
|
6035
|
-
/\btop\s*\.\s*location/,
|
|
6036
|
-
/\bparent\s*\.\s*location/
|
|
6037
|
-
];
|
|
6038
|
-
var BRAND_NAMES = [
|
|
6039
|
-
"google",
|
|
6040
|
-
"gmail",
|
|
6041
|
-
"apple",
|
|
6042
|
-
"icloud",
|
|
6043
|
-
"microsoft",
|
|
6044
|
-
"outlook",
|
|
6045
|
-
"paypal",
|
|
6046
|
-
"facebook",
|
|
6047
|
-
"instagram",
|
|
6048
|
-
"telegram",
|
|
6049
|
-
"whatsapp",
|
|
6050
|
-
"metamask",
|
|
6051
|
-
"binance",
|
|
6052
|
-
"coinbase"
|
|
6053
|
-
];
|
|
6054
|
-
function extractExternalHosts(html) {
|
|
6055
|
-
const scrubbed = html.replace(/\bxmlns(:[a-z0-9-]+)?\s*=\s*("[^"]*"|'[^']*')/gi, "").replace(/<!DOCTYPE[^>]*>/gi, "");
|
|
6056
|
-
const hosts = /* @__PURE__ */ new Set();
|
|
6057
|
-
const re = /https?:\/\/([a-z0-9.-]+)/gi;
|
|
6058
|
-
let m;
|
|
6059
|
-
while (m = re.exec(scrubbed)) {
|
|
6060
|
-
const host = m[1]?.toLowerCase();
|
|
6061
|
-
if (host) hosts.add(host);
|
|
6062
|
-
}
|
|
6063
|
-
return [...hosts];
|
|
6064
|
-
}
|
|
6065
|
-
function looksLikeHtml(root) {
|
|
6066
|
-
const hasEl = root.querySelector("*") != null;
|
|
6067
|
-
return hasEl;
|
|
6068
|
-
}
|
|
6069
|
-
function validate(html, opts) {
|
|
6070
|
-
const errors = [];
|
|
6071
|
-
const warnings = [];
|
|
6072
|
-
const size = Buffer.byteLength(html, "utf8");
|
|
6073
|
-
if (size > opts.maxBytes) {
|
|
6074
|
-
errors.push({
|
|
6075
|
-
code: "SIZE_EXCEEDED",
|
|
6076
|
-
message: `Artifact is ${size} bytes; limit is ${opts.maxBytes}.`
|
|
6077
|
-
});
|
|
6078
|
-
}
|
|
6079
|
-
const root = (0, import_node_html_parser.parse)(html, {
|
|
6080
|
-
lowerCaseTagName: true,
|
|
6081
|
-
comment: true,
|
|
6082
|
-
voidTag: { closingSlash: true }
|
|
6083
|
-
});
|
|
6084
|
-
if (!looksLikeHtml(root)) {
|
|
6085
|
-
errors.push({ code: "NOT_HTML", message: "Content is not parseable as HTML." });
|
|
6086
|
-
return { ok: false, errors, warnings };
|
|
6087
|
-
}
|
|
6088
|
-
if (root.querySelector("iframe, frame, object, embed")) {
|
|
6089
|
-
errors.push({
|
|
6090
|
-
code: "FRAME_TAG",
|
|
6091
|
-
message: "Frames are not allowed (iframe/frame/object/embed)."
|
|
6092
|
-
});
|
|
6093
|
-
}
|
|
6094
|
-
if (root.querySelector("base")) {
|
|
6095
|
-
errors.push({ code: "BASE_TAG", message: "<base> tags are not allowed." });
|
|
6096
|
-
}
|
|
6097
|
-
for (const meta of root.querySelectorAll("meta")) {
|
|
6098
|
-
if ((meta.getAttribute("http-equiv") || "").toLowerCase() === "refresh") {
|
|
6099
|
-
errors.push({ code: "META_REFRESH", message: "meta-refresh redirects are not allowed." });
|
|
6100
|
-
break;
|
|
6101
|
-
}
|
|
6102
|
-
}
|
|
6103
|
-
for (const input of root.querySelectorAll("input")) {
|
|
6104
|
-
if ((input.getAttribute("type") || "").toLowerCase() === "password") {
|
|
6105
|
-
errors.push({
|
|
6106
|
-
code: "PASSWORD_INPUT",
|
|
6107
|
-
message: 'Password inputs are not allowed (<input type="password">).'
|
|
6108
|
-
});
|
|
6109
|
-
break;
|
|
6110
|
-
}
|
|
6111
|
-
}
|
|
6112
|
-
for (const re of TOP_NAV_PATTERNS) {
|
|
6113
|
-
if (re.test(html)) {
|
|
6114
|
-
errors.push({
|
|
6115
|
-
code: "TOP_NAVIGATION",
|
|
6116
|
-
message: "Top-level navigation attempts are not allowed.",
|
|
6117
|
-
detail: re.source
|
|
6118
|
-
});
|
|
6119
|
-
break;
|
|
6120
|
-
}
|
|
6121
|
-
}
|
|
6122
|
-
for (const { re, name } of FORBIDDEN_API_PATTERNS) {
|
|
6123
|
-
if (re.test(html)) {
|
|
6124
|
-
errors.push({
|
|
6125
|
-
code: "FORBIDDEN_API",
|
|
6126
|
-
message: `Forbidden API used: ${name}.`,
|
|
6127
|
-
detail: name
|
|
6128
|
-
});
|
|
6129
|
-
}
|
|
6130
|
-
}
|
|
6131
|
-
const hosts = extractExternalHosts(html);
|
|
6132
|
-
const offending = hosts.filter(
|
|
6133
|
-
(h) => !opts.allowedOrigins.some((allowed) => h === allowed || h.endsWith("." + allowed))
|
|
6134
|
-
);
|
|
6135
|
-
for (const host of offending) {
|
|
6136
|
-
errors.push({
|
|
6137
|
-
code: "EXTERNAL_URL",
|
|
6138
|
-
message: `External URL not in allowlist: ${host}`,
|
|
6139
|
-
detail: host
|
|
6140
|
-
});
|
|
6141
|
-
}
|
|
6142
|
-
const lower = html.toLowerCase();
|
|
6143
|
-
const hasCredField = root.querySelector('input[type="email"], input[type="text"], input[type="tel"]') != null && /(password|passcode|otp|verify|sign\s*in|log\s*in|account)/i.test(html);
|
|
6144
|
-
if (hasCredField) {
|
|
6145
|
-
const brand = BRAND_NAMES.find((b) => lower.includes(b));
|
|
6146
|
-
if (brand) {
|
|
6147
|
-
warnings.push({
|
|
6148
|
-
code: "BRAND_CREDENTIAL",
|
|
6149
|
-
message: `Possible phishing: brand "${brand}" near a credential field.`,
|
|
6150
|
-
detail: brand
|
|
6151
|
-
});
|
|
6152
|
-
}
|
|
6153
|
-
}
|
|
6154
|
-
return { ok: errors.length === 0, errors, warnings };
|
|
6155
|
-
}
|
|
6156
6021
|
|
|
6157
6022
|
// ../shared/src/constants.ts
|
|
6158
6023
|
var CATEGORIES = [
|
|
@@ -6181,6 +6046,17 @@ var config = {
|
|
|
6181
6046
|
MODERATION_AUTOPUBLISH: boolEnv("MODERATION_AUTOPUBLISH", true),
|
|
6182
6047
|
BUILDER_MODE: strEnv("BUILDER_MODE", "mock"),
|
|
6183
6048
|
// 'mock' | 'live'
|
|
6049
|
+
// Builder LLM — non-secret routing only (this object ships to the client).
|
|
6050
|
+
// The platform API key is read server-side from process.env.BUILDER_API_KEY.
|
|
6051
|
+
// Which platform provider serves the builder: 'gapgpt' | 'openrouter'.
|
|
6052
|
+
BUILDER_PROVIDER: strEnv("BUILDER_PROVIDER", "gapgpt"),
|
|
6053
|
+
BUILDER_BASE_URL: strEnv("BUILDER_BASE_URL", "https://api.gapgpt.app/v1"),
|
|
6054
|
+
BUILDER_MODEL: strEnv("BUILDER_MODEL", "claude-sonnet-4-20250514"),
|
|
6055
|
+
// Light model that auto-tags the experience category. Falls back to a keyword
|
|
6056
|
+
// heuristic if the call fails.
|
|
6057
|
+
BUILDER_CLASSIFIER_MODEL: strEnv("BUILDER_CLASSIFIER_MODEL", "gemini-2.5-flash"),
|
|
6058
|
+
BUILDER_MAX_TOKENS: intEnv("BUILDER_MAX_TOKENS", 8192),
|
|
6059
|
+
BUILDER_MAX_MESSAGES: intEnv("BUILDER_MAX_MESSAGES", 30),
|
|
6184
6060
|
DEFAULT_LOCALE: strEnv("DEFAULT_LOCALE", "en")
|
|
6185
6061
|
};
|
|
6186
6062
|
function strEnv(key, fallback) {
|
|
@@ -6203,6 +6079,37 @@ function boolEnv(key, fallback) {
|
|
|
6203
6079
|
return v === "true" || v === "1";
|
|
6204
6080
|
}
|
|
6205
6081
|
|
|
6082
|
+
// ../shared/src/policy.ts
|
|
6083
|
+
function boolEnv2(key, fallback) {
|
|
6084
|
+
const v = typeof process !== "undefined" ? process.env[key] : void 0;
|
|
6085
|
+
if (v == null || v === "") return fallback;
|
|
6086
|
+
return v === "true" || v === "1";
|
|
6087
|
+
}
|
|
6088
|
+
var POST_RULES = [
|
|
6089
|
+
{ id: "size_limit", code: "SIZE_EXCEEDED", title: "Size limit", default: true, env: "RULE_SIZE_LIMIT" },
|
|
6090
|
+
{ id: "frames", code: "FRAME_TAG", title: "No frames", default: true, env: "RULE_FRAMES" },
|
|
6091
|
+
{ id: "base_tag", code: "BASE_TAG", title: "No <base> tag", default: true, env: "RULE_BASE_TAG" },
|
|
6092
|
+
{ id: "meta_refresh", code: "META_REFRESH", title: "No meta-refresh", default: true, env: "RULE_META_REFRESH" },
|
|
6093
|
+
{ id: "password_input", code: "PASSWORD_INPUT", title: "No password inputs", default: true, env: "RULE_PASSWORD_INPUT" },
|
|
6094
|
+
{ id: "top_navigation", code: "TOP_NAVIGATION", title: "No top-navigation", default: true, env: "RULE_TOP_NAVIGATION" },
|
|
6095
|
+
{ id: "forbidden_apis", code: "FORBIDDEN_API", title: "No forbidden APIs (payments, background, capture\u2026)", default: true, env: "RULE_FORBIDDEN_APIS" },
|
|
6096
|
+
{ id: "external_urls", code: "EXTERNAL_URL", title: "No off-allowlist external URLs", default: true, env: "RULE_EXTERNAL_URLS" },
|
|
6097
|
+
{ id: "external_scripts", code: "EXTERNAL_SCRIPT", title: "No creator-selected JS packages (external <script>)", default: false, env: "RULE_EXTERNAL_SCRIPTS" },
|
|
6098
|
+
{ id: "external_links", code: "EXTERNAL_LINK", title: "No direct external links (<a> off-site)", default: false, env: "RULE_EXTERNAL_LINKS" },
|
|
6099
|
+
// Enforced by injecting a viewport clip at sanitize (not a reject) — a static
|
|
6100
|
+
// scan can't tell whether a post actually scrolls, so we make it can't.
|
|
6101
|
+
{ id: "no_vertical_scroll", code: "SANITIZE_CLIP", title: "No vertical page scroll (fit the viewport)", default: true, env: "RULE_NO_VERTICAL_SCROLL" }
|
|
6102
|
+
];
|
|
6103
|
+
function resolvePolicy() {
|
|
6104
|
+
const rules = {};
|
|
6105
|
+
for (const r of POST_RULES) rules[r.id] = boolEnv2(r.env, r.default);
|
|
6106
|
+
return { enabled: boolEnv2("POST_RESTRICTIONS_ENABLED", true), rules };
|
|
6107
|
+
}
|
|
6108
|
+
var postPolicy = resolvePolicy();
|
|
6109
|
+
function ruleActive(id, policy = postPolicy) {
|
|
6110
|
+
return policy.enabled && policy.rules[id] === true;
|
|
6111
|
+
}
|
|
6112
|
+
|
|
6206
6113
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
|
|
6207
6114
|
var external_exports = {};
|
|
6208
6115
|
__export(external_exports, {
|
|
@@ -10284,8 +10191,19 @@ var createBuildSchema = external_exports.object({
|
|
|
10284
10191
|
mode: external_exports.enum(["ai", "remix"]),
|
|
10285
10192
|
remixParentId: external_exports.string().uuid().optional()
|
|
10286
10193
|
});
|
|
10194
|
+
var builderAttachmentSchema = external_exports.object({
|
|
10195
|
+
kind: external_exports.enum(["image", "file"]),
|
|
10196
|
+
name: external_exports.string().max(200),
|
|
10197
|
+
dataUrl: external_exports.string().max(8e6).optional(),
|
|
10198
|
+
// ~6MB image as base64 data URL
|
|
10199
|
+
text: external_exports.string().max(5e4).optional()
|
|
10200
|
+
});
|
|
10287
10201
|
var builderMessageSchema = external_exports.object({
|
|
10288
|
-
content: external_exports.string().
|
|
10202
|
+
content: external_exports.string().max(4e3).default(""),
|
|
10203
|
+
attachments: external_exports.array(builderAttachmentSchema).max(6).optional(),
|
|
10204
|
+
model: external_exports.string().max(100).optional()
|
|
10205
|
+
}).refine((v) => v.content.trim().length > 0 || (v.attachments?.length ?? 0) > 0, {
|
|
10206
|
+
message: "Send a message or an attachment."
|
|
10289
10207
|
});
|
|
10290
10208
|
var importSchema = external_exports.object({
|
|
10291
10209
|
html: external_exports.string().min(1).optional(),
|
|
@@ -10296,6 +10214,7 @@ var importSchema = external_exports.object({
|
|
|
10296
10214
|
var saveSchema = external_exports.object({
|
|
10297
10215
|
collectionId: external_exports.string().uuid().optional()
|
|
10298
10216
|
});
|
|
10217
|
+
var AI_PROVIDERS = ["gapgpt", "moonshot", "openai", "openrouter"];
|
|
10299
10218
|
var collectionSchema = external_exports.object({
|
|
10300
10219
|
name: external_exports.string().min(1).max(60)
|
|
10301
10220
|
});
|
|
@@ -10313,8 +10232,8 @@ var patchMeSchema = external_exports.object({
|
|
|
10313
10232
|
// once
|
|
10314
10233
|
});
|
|
10315
10234
|
var aiConnectionSchema = external_exports.object({
|
|
10316
|
-
provider: external_exports.enum(
|
|
10317
|
-
apiKey: external_exports.string().min(
|
|
10235
|
+
provider: external_exports.enum(AI_PROVIDERS),
|
|
10236
|
+
apiKey: external_exports.string().trim().min(12).max(300)
|
|
10318
10237
|
});
|
|
10319
10238
|
var feedQuerySchema = external_exports.object({
|
|
10320
10239
|
tab: external_exports.enum(["explore", "following"]).default("explore"),
|
|
@@ -10330,6 +10249,24 @@ var searchQuerySchema = external_exports.object({
|
|
|
10330
10249
|
limit: external_exports.coerce.number().int().min(1).max(30).default(15)
|
|
10331
10250
|
});
|
|
10332
10251
|
|
|
10252
|
+
// ../shared/src/models.ts
|
|
10253
|
+
var BUILDER_MODELS = [
|
|
10254
|
+
// ── Free (selectable now) — good quality, reasonable price ─────────
|
|
10255
|
+
{ id: "claude-sonnet-4-20250514", label: "Claude Sonnet 4", tier: "free", note: "Best for polished HTML", vision: true },
|
|
10256
|
+
{ id: "gemini-2.5-flash", label: "Gemini 2.5 Flash", tier: "free", note: "Fast \xB7 great UI \xB7 cheap", vision: true },
|
|
10257
|
+
{ id: "gpt-4o-mini", label: "GPT-4o mini", tier: "free", note: "Cheap & reliable", vision: true },
|
|
10258
|
+
{ id: "gpt-5-mini", label: "GPT-5 mini", tier: "free", note: "Newer, capable", vision: true },
|
|
10259
|
+
{ id: "gpt-5.1-codex", label: "GPT-5.1 Codex", tier: "free", note: "Code specialist" },
|
|
10260
|
+
{ id: "deepseek-v4-pro", label: "DeepSeek V4 Pro", tier: "free", note: "Strong coder \xB7 great value" },
|
|
10261
|
+
{ id: "deepseek-v4-flash", label: "DeepSeek V4 Flash", tier: "free", note: "Fast \xB7 very cheap" },
|
|
10262
|
+
{ id: "gapgpt-qwen-3.6", label: "Qwen 3.6", tier: "free", note: "Good value" },
|
|
10263
|
+
// ── Subscription (locked until billing is enabled) — premium ──────
|
|
10264
|
+
{ id: "claude-opus-4-8", label: "Claude Opus 4.8", tier: "subscription", note: "Top quality", vision: true },
|
|
10265
|
+
{ id: "gpt-5.2-pro", label: "GPT-5.2 Pro", tier: "subscription", note: "Premium" },
|
|
10266
|
+
{ id: "gemini-3-pro-preview", label: "Gemini 3 Pro", tier: "subscription", note: "Premium", vision: true }
|
|
10267
|
+
];
|
|
10268
|
+
var FREE_IDS = new Set(BUILDER_MODELS.filter((m) => m.tier === "free").map((m) => m.id));
|
|
10269
|
+
|
|
10333
10270
|
// ../shared/src/tokens.ts
|
|
10334
10271
|
var colors = {
|
|
10335
10272
|
brand: "#6C4CF1",
|
|
@@ -10361,6 +10298,175 @@ var toggle = {
|
|
|
10361
10298
|
knobTo: "22px"
|
|
10362
10299
|
};
|
|
10363
10300
|
|
|
10301
|
+
// ../sandbox/src/index.ts
|
|
10302
|
+
var FORBIDDEN_API_PATTERNS = [
|
|
10303
|
+
{ re: /getUserMedia/, name: "getUserMedia" },
|
|
10304
|
+
{ re: /navigator\s*\.\s*geolocation/, name: "geolocation" },
|
|
10305
|
+
{ re: /new\s+Notification|Notification\s*\.\s*requestPermission/, name: "Notification" },
|
|
10306
|
+
{ re: /serviceWorker/, name: "serviceWorker" },
|
|
10307
|
+
{ re: /openDatabase/, name: "openDatabase" },
|
|
10308
|
+
{ re: /PaymentRequest|navigator\s*\.\s*payments/, name: "PaymentRequest" },
|
|
10309
|
+
{ re: /navigator\s*\.\s*credentials|PublicKeyCredential|WebAuthn/, name: "WebAuthn" },
|
|
10310
|
+
{ re: /requestPointerLock/, name: "pointerLock" }
|
|
10311
|
+
// allowed via gesture only; flag
|
|
10312
|
+
];
|
|
10313
|
+
var TOP_NAV_PATTERNS = [
|
|
10314
|
+
/window\s*\.\s*top/,
|
|
10315
|
+
/window\s*\.\s*parent\s*\.\s*location/,
|
|
10316
|
+
/\btop\s*\.\s*location/,
|
|
10317
|
+
/\bparent\s*\.\s*location/
|
|
10318
|
+
];
|
|
10319
|
+
var BRAND_NAMES = [
|
|
10320
|
+
"google",
|
|
10321
|
+
"gmail",
|
|
10322
|
+
"apple",
|
|
10323
|
+
"icloud",
|
|
10324
|
+
"microsoft",
|
|
10325
|
+
"outlook",
|
|
10326
|
+
"paypal",
|
|
10327
|
+
"facebook",
|
|
10328
|
+
"instagram",
|
|
10329
|
+
"telegram",
|
|
10330
|
+
"whatsapp",
|
|
10331
|
+
"metamask",
|
|
10332
|
+
"binance",
|
|
10333
|
+
"coinbase"
|
|
10334
|
+
];
|
|
10335
|
+
function extractExternalHosts(html) {
|
|
10336
|
+
const scrubbed = html.replace(/\bxmlns(:[a-z0-9-]+)?\s*=\s*("[^"]*"|'[^']*')/gi, "").replace(/<!DOCTYPE[^>]*>/gi, "");
|
|
10337
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
10338
|
+
const re = /https?:\/\/([a-z0-9.-]+)/gi;
|
|
10339
|
+
let m;
|
|
10340
|
+
while (m = re.exec(scrubbed)) {
|
|
10341
|
+
const host = m[1]?.toLowerCase();
|
|
10342
|
+
if (host) hosts.add(host);
|
|
10343
|
+
}
|
|
10344
|
+
return [...hosts];
|
|
10345
|
+
}
|
|
10346
|
+
function looksLikeHtml(root) {
|
|
10347
|
+
const hasEl = root.querySelector("*") != null;
|
|
10348
|
+
return hasEl;
|
|
10349
|
+
}
|
|
10350
|
+
function validate(html, opts) {
|
|
10351
|
+
const errors = [];
|
|
10352
|
+
const warnings = [];
|
|
10353
|
+
const size = Buffer.byteLength(html, "utf8");
|
|
10354
|
+
const policy = opts.policy ?? postPolicy;
|
|
10355
|
+
const on = (id) => ruleActive(id, policy);
|
|
10356
|
+
if (on("size_limit") && size > opts.maxBytes) {
|
|
10357
|
+
errors.push({
|
|
10358
|
+
code: "SIZE_EXCEEDED",
|
|
10359
|
+
message: `Artifact is ${size} bytes; limit is ${opts.maxBytes}.`
|
|
10360
|
+
});
|
|
10361
|
+
}
|
|
10362
|
+
const root = (0, import_node_html_parser.parse)(html, {
|
|
10363
|
+
lowerCaseTagName: true,
|
|
10364
|
+
comment: true,
|
|
10365
|
+
voidTag: { closingSlash: true }
|
|
10366
|
+
});
|
|
10367
|
+
if (!looksLikeHtml(root)) {
|
|
10368
|
+
errors.push({ code: "NOT_HTML", message: "Content is not parseable as HTML." });
|
|
10369
|
+
return { ok: false, errors, warnings };
|
|
10370
|
+
}
|
|
10371
|
+
if (on("frames") && root.querySelector("iframe, frame, object, embed")) {
|
|
10372
|
+
errors.push({
|
|
10373
|
+
code: "FRAME_TAG",
|
|
10374
|
+
message: "Frames are not allowed (iframe/frame/object/embed)."
|
|
10375
|
+
});
|
|
10376
|
+
}
|
|
10377
|
+
if (on("base_tag") && root.querySelector("base")) {
|
|
10378
|
+
errors.push({ code: "BASE_TAG", message: "<base> tags are not allowed." });
|
|
10379
|
+
}
|
|
10380
|
+
if (on("meta_refresh")) {
|
|
10381
|
+
for (const meta of root.querySelectorAll("meta")) {
|
|
10382
|
+
if ((meta.getAttribute("http-equiv") || "").toLowerCase() === "refresh") {
|
|
10383
|
+
errors.push({ code: "META_REFRESH", message: "meta-refresh redirects are not allowed." });
|
|
10384
|
+
break;
|
|
10385
|
+
}
|
|
10386
|
+
}
|
|
10387
|
+
}
|
|
10388
|
+
if (on("password_input")) {
|
|
10389
|
+
for (const input of root.querySelectorAll("input")) {
|
|
10390
|
+
if ((input.getAttribute("type") || "").toLowerCase() === "password") {
|
|
10391
|
+
errors.push({
|
|
10392
|
+
code: "PASSWORD_INPUT",
|
|
10393
|
+
message: 'Password inputs are not allowed (<input type="password">).'
|
|
10394
|
+
});
|
|
10395
|
+
break;
|
|
10396
|
+
}
|
|
10397
|
+
}
|
|
10398
|
+
}
|
|
10399
|
+
if (on("top_navigation")) {
|
|
10400
|
+
for (const re of TOP_NAV_PATTERNS) {
|
|
10401
|
+
if (re.test(html)) {
|
|
10402
|
+
errors.push({
|
|
10403
|
+
code: "TOP_NAVIGATION",
|
|
10404
|
+
message: "Top-level navigation attempts are not allowed.",
|
|
10405
|
+
detail: re.source
|
|
10406
|
+
});
|
|
10407
|
+
break;
|
|
10408
|
+
}
|
|
10409
|
+
}
|
|
10410
|
+
}
|
|
10411
|
+
if (on("forbidden_apis")) {
|
|
10412
|
+
for (const { re, name } of FORBIDDEN_API_PATTERNS) {
|
|
10413
|
+
if (re.test(html)) {
|
|
10414
|
+
errors.push({ code: "FORBIDDEN_API", message: `Forbidden API used: ${name}.`, detail: name });
|
|
10415
|
+
}
|
|
10416
|
+
}
|
|
10417
|
+
}
|
|
10418
|
+
if (on("external_urls")) {
|
|
10419
|
+
const hosts = extractExternalHosts(html);
|
|
10420
|
+
const offending = hosts.filter(
|
|
10421
|
+
(h) => !opts.allowedOrigins.some((allowed) => h === allowed || h.endsWith("." + allowed))
|
|
10422
|
+
);
|
|
10423
|
+
for (const host of offending) {
|
|
10424
|
+
errors.push({
|
|
10425
|
+
code: "EXTERNAL_URL",
|
|
10426
|
+
message: `External URL not in allowlist: ${host}`,
|
|
10427
|
+
detail: host
|
|
10428
|
+
});
|
|
10429
|
+
}
|
|
10430
|
+
}
|
|
10431
|
+
if (on("external_scripts")) {
|
|
10432
|
+
for (const s of root.querySelectorAll("script")) {
|
|
10433
|
+
const src = s.getAttribute("src");
|
|
10434
|
+
if (src && /^(https?:)?\/\//i.test(src.trim())) {
|
|
10435
|
+
errors.push({
|
|
10436
|
+
code: "EXTERNAL_SCRIPT",
|
|
10437
|
+
message: `External script not allowed (inline your code): ${src}`,
|
|
10438
|
+
detail: src
|
|
10439
|
+
});
|
|
10440
|
+
}
|
|
10441
|
+
}
|
|
10442
|
+
}
|
|
10443
|
+
if (on("external_links")) {
|
|
10444
|
+
for (const a of root.querySelectorAll("a")) {
|
|
10445
|
+
const href = (a.getAttribute("href") || "").trim();
|
|
10446
|
+
if (/^(https?:)?\/\//i.test(href)) {
|
|
10447
|
+
errors.push({
|
|
10448
|
+
code: "EXTERNAL_LINK",
|
|
10449
|
+
message: `External link not allowed: ${href}`,
|
|
10450
|
+
detail: href
|
|
10451
|
+
});
|
|
10452
|
+
}
|
|
10453
|
+
}
|
|
10454
|
+
}
|
|
10455
|
+
const lower = html.toLowerCase();
|
|
10456
|
+
const hasCredField = root.querySelector('input[type="email"], input[type="text"], input[type="tel"]') != null && /(password|passcode|otp|verify|sign\s*in|log\s*in|account)/i.test(html);
|
|
10457
|
+
if (hasCredField) {
|
|
10458
|
+
const brand = BRAND_NAMES.find((b) => lower.includes(b));
|
|
10459
|
+
if (brand) {
|
|
10460
|
+
warnings.push({
|
|
10461
|
+
code: "BRAND_CREDENTIAL",
|
|
10462
|
+
message: `Possible phishing: brand "${brand}" near a credential field.`,
|
|
10463
|
+
detail: brand
|
|
10464
|
+
});
|
|
10465
|
+
}
|
|
10466
|
+
}
|
|
10467
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
10468
|
+
}
|
|
10469
|
+
|
|
10364
10470
|
// src/bundle.ts
|
|
10365
10471
|
var MIME = {
|
|
10366
10472
|
".css": "text/css",
|
|
@@ -10611,10 +10717,11 @@ async function bundleProject(projectDir, opts = {}) {
|
|
|
10611
10717
|
rootDir: dirname(resolved.entry),
|
|
10612
10718
|
allowedOrigins
|
|
10613
10719
|
});
|
|
10614
|
-
const result = validate(html, { maxBytes, allowedOrigins });
|
|
10720
|
+
const result = validate(html, { maxBytes, allowedOrigins, policy: opts.policy });
|
|
10615
10721
|
return {
|
|
10616
10722
|
ok: result.ok,
|
|
10617
10723
|
html,
|
|
10724
|
+
policy: opts.policy,
|
|
10618
10725
|
entry: resolved.entry,
|
|
10619
10726
|
builtWith: resolved.builtWith,
|
|
10620
10727
|
sizeBytes: Buffer.byteLength(html, "utf8"),
|
|
@@ -10634,7 +10741,9 @@ var FIXES = {
|
|
|
10634
10741
|
TOP_NAVIGATION: "Remove top-level navigation (window.top / parent.location). The artifact runs sandboxed and can't drive the page it's embedded in.",
|
|
10635
10742
|
FORBIDDEN_API: "This browser API is blocked in the sandbox (e.g. getUserMedia, geolocation, serviceWorker, Notification, PaymentRequest, WebAuthn). Remove it or gate it behind a no-op fallback.",
|
|
10636
10743
|
PASSWORD_INPUT: `Password inputs aren't allowed (anti-phishing). Remove <input type="password">.`,
|
|
10637
|
-
EXTERNAL_URL: "References a host that isn't on the allowlist \u2014 often a call to your own backend/API or a third-party script. vibed artifacts are a single static file: inline the asset, drop the call, or move it to an allowlisted CDN (fonts.googleapis.com, cdn.jsdelivr.net, unpkg.com, cdnjs.cloudflare.com, fonts.gstatic.com)."
|
|
10744
|
+
EXTERNAL_URL: "References a host that isn't on the allowlist \u2014 often a call to your own backend/API or a third-party script. vibed artifacts are a single static file: inline the asset, drop the call, or move it to an allowlisted CDN (fonts.googleapis.com, cdn.jsdelivr.net, unpkg.com, cdnjs.cloudflare.com, fonts.gstatic.com).",
|
|
10745
|
+
EXTERNAL_SCRIPT: "Loads JavaScript from an external <script src>. Creator-selected packages aren't allowed \u2014 inline the code you need directly into the file.",
|
|
10746
|
+
EXTERNAL_LINK: "Links off the platform (<a href> to another site). Posts can't send viewers elsewhere \u2014 remove the link or make it in-page (e.g. an anchor to a section)."
|
|
10638
10747
|
};
|
|
10639
10748
|
var KB = 1024;
|
|
10640
10749
|
function fmtSize(bytes) {
|
|
@@ -10677,6 +10786,12 @@ function formatReport(result) {
|
|
|
10677
10786
|
out2.push(`\u2717 Not vibeable yet \u2014 ${result.blockers.length} blocker(s):`);
|
|
10678
10787
|
out2.push(...summarize(result).lines);
|
|
10679
10788
|
}
|
|
10789
|
+
if (result.policy && ruleActive("no_vertical_scroll", result.policy)) {
|
|
10790
|
+
out2.push(
|
|
10791
|
+
"",
|
|
10792
|
+
"\u2139 Single screen: the page can't scroll vertically (auto-clipped at publish) \u2014 design it to fit."
|
|
10793
|
+
);
|
|
10794
|
+
}
|
|
10680
10795
|
if (result.warnings.length) {
|
|
10681
10796
|
out2.push("", "Flags (published but sent to moderation):");
|
|
10682
10797
|
for (const w of result.warnings) out2.push(` \u2022 ${w.message}`);
|
|
@@ -10736,6 +10851,13 @@ async function apiFetch({ apiBase, token }, method, path, body) {
|
|
|
10736
10851
|
}
|
|
10737
10852
|
return json.data;
|
|
10738
10853
|
}
|
|
10854
|
+
async function fetchPolicy(apiBase) {
|
|
10855
|
+
try {
|
|
10856
|
+
return await apiFetch({ apiBase }, "GET", "/policy");
|
|
10857
|
+
} catch {
|
|
10858
|
+
return void 0;
|
|
10859
|
+
}
|
|
10860
|
+
}
|
|
10739
10861
|
|
|
10740
10862
|
// src/login.ts
|
|
10741
10863
|
function openBrowser(url) {
|
|
@@ -10889,9 +11011,13 @@ Publish only:
|
|
|
10889
11011
|
async function cmdCheck(args) {
|
|
10890
11012
|
const json = isTrue(args.flags.json);
|
|
10891
11013
|
const dir2 = resolve2(args._[1] ?? ".");
|
|
11014
|
+
const cfg = await loadConfig({ api: str(args.flags.api) });
|
|
11015
|
+
const policy = isTrue(args.flags["local-policy"]) ? void 0 : await fetchPolicy(cfg.apiBase);
|
|
11016
|
+
if (!json && policy) console.error(`Rules: ${cfg.apiBase}`);
|
|
10892
11017
|
const result = await bundleProject(dir2, {
|
|
10893
11018
|
entry: str(args.flags.entry),
|
|
10894
11019
|
build: !isTrue(args.flags["no-build"]),
|
|
11020
|
+
policy,
|
|
10895
11021
|
onLog: (m) => !json && console.error(m)
|
|
10896
11022
|
});
|
|
10897
11023
|
out(json, formatReport(result), {
|
|
@@ -10914,9 +11040,11 @@ async function cmdPublish(args) {
|
|
|
10914
11040
|
console.error("Not signed in. Run `vibed login` first.");
|
|
10915
11041
|
return 1;
|
|
10916
11042
|
}
|
|
11043
|
+
const policy = await fetchPolicy(cfg.apiBase);
|
|
10917
11044
|
const result = await bundleProject(dir2, {
|
|
10918
11045
|
entry: str(args.flags.entry),
|
|
10919
11046
|
build: !isTrue(args.flags["no-build"]),
|
|
11047
|
+
policy,
|
|
10920
11048
|
onLog: (m) => !json && console.error(m)
|
|
10921
11049
|
});
|
|
10922
11050
|
if (!result.ok || !result.html) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amirhosseinnateghi/vibed-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "make it vibed — check, bundle, and publish a project to vibed from the terminal",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
".": "./dist/index.js",
|
|
12
12
|
"./lib": "./src/lib.ts"
|
|
13
13
|
},
|
|
14
|
-
"files": [
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
15
18
|
"publishConfig": {
|
|
16
19
|
"access": "public"
|
|
17
20
|
},
|