kreftjs 0.0.20-beta → 0.0.22-beta

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.
Files changed (64) hide show
  1. package/README.md +10 -1
  2. package/bin/artisan.js +4 -2
  3. package/bin/backend/cache.js +31 -5
  4. package/bin/backend/gc.js +193 -43
  5. package/bin/backend/http.js +6 -2
  6. package/bin/backend/index.js +6 -0
  7. package/bin/backend/mcp-client.js +133 -0
  8. package/bin/backend/mcp.js +17 -0
  9. package/bin/backend/model.js +15 -1
  10. package/bin/backend/shield.js +266 -0
  11. package/bin/backend/stream.js +74 -0
  12. package/bin/build.js +12 -5
  13. package/bin/db-api.js +9 -1
  14. package/bin/kreftjs.js +50 -12
  15. package/book/ar/16-virtual-list.md +131 -0
  16. package/book/ar/17-combobox.md +169 -0
  17. package/book/ar/18-tabs-offline.md +168 -0
  18. package/book/ar/19-wizard.md +196 -0
  19. package/book/ar/20-pipes-match-props.md +75 -0
  20. package/book/ar/README.md +12 -1
  21. package/book/kreft-book-ar.docx +0 -0
  22. package/docs/README.md +209 -13
  23. package/editors/sublime/Comments.tmPreferences +26 -0
  24. package/editors/sublime/Component.sublime-snippet +20 -0
  25. package/editors/sublime/Kreft.sublime-syntax +39 -0
  26. package/editors/sublime/LICENSE +95 -0
  27. package/editors/sublime/README.md +7 -0
  28. package/editors/sublime/action.sublime-snippet +10 -0
  29. package/editors/sublime/derive.sublime-snippet +8 -0
  30. package/editors/sublime/for.sublime-snippet +12 -0
  31. package/editors/sublime/k-case.sublime-snippet +8 -0
  32. package/editors/sublime/k-if.sublime-snippet +8 -0
  33. package/editors/sublime/k-match.sublime-snippet +12 -0
  34. package/editors/sublime/kreft-wait.sublime-snippet +12 -0
  35. package/editors/sublime/layout.sublime-snippet +8 -0
  36. package/editors/sublime/load.sublime-snippet +8 -0
  37. package/editors/sublime/on.sublime-snippet +10 -0
  38. package/editors/sublime/pipe.sublime-snippet +8 -0
  39. package/editors/sublime/prop-typed.sublime-snippet +8 -0
  40. package/editors/sublime/saved.sublime-snippet +8 -0
  41. package/editors/sublime/section.sublime-snippet +10 -0
  42. package/editors/sublime/state.sublime-snippet +8 -0
  43. package/editors/sublime/watch.sublime-snippet +10 -0
  44. package/editors/sublime/when.sublime-snippet +10 -0
  45. package/editors/sublime/yield.sublime-snippet +8 -0
  46. package/editors/vscode/snippets/kreftjs.json +143 -15
  47. package/editors/zed/LICENSE +95 -0
  48. package/editors/zed/README.md +17 -0
  49. package/editors/zed/extension.toml +11 -0
  50. package/editors/zed/languages/kreft/brackets.scm +4 -0
  51. package/editors/zed/languages/kreft/config.toml +17 -0
  52. package/editors/zed/languages/kreft/highlights.scm +18 -0
  53. package/editors/zed/languages/kreft/indents.scm +5 -0
  54. package/editors/zed/languages/kreft/injections.scm +14 -0
  55. package/editors/zed/languages/kreft/outline.scm +3 -0
  56. package/kreftjs.d.ts +1 -1
  57. package/kreftjs.js +219 -14
  58. package/package.json +3 -2
  59. package/server.d.ts +44 -0
  60. package/templates/app/backend/app/Auth/Session.js +8 -4
  61. package/templates/app/backend/app/Http/Middleware/Authenticate.js +7 -4
  62. package/templates/app/backend/database/seeders/DatabaseSeeder.js +4 -0
  63. package/templates/app/frontend/kreftjs.d.ts +1 -1
  64. package/templates/app/frontend/kreftjs.js +219 -14
package/kreftjs.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Kreft v0.0.20-beta — reactive by nature
2
+ * Kreft v0.0.22-beta — reactive by nature
3
3
  * The .kreft component language (state · derive · action · when · watch · on),
4
4
  * { } views with k-if / k-each / k-model, components, slots, router, stores,
5
5
  * share / receive, event bus and a template engine.
@@ -1026,6 +1026,7 @@
1026
1026
  onceDepth++;
1027
1027
  try { return compile(el, scope, inst); } finally { onceDepth--; }
1028
1028
  }
1029
+ if (el.hasAttribute('k-match')) return compileMatch(el, scope, inst); // becomes k-if / k-elif / k-else on its children
1029
1030
  if (el.hasAttribute('k-if')) return compileIf(el, scope, inst); // k-if is checked before k-each
1030
1031
  if (el.hasAttribute('k-elif') || el.hasAttribute('k-else')) {
1031
1032
  warn('k-elif / k-else has no k-if right before it.', el);
@@ -1060,6 +1061,157 @@
1060
1061
  compileElement(el, scope, inst, needAttrs, hasAnim, info);
1061
1062
  }
1062
1063
 
1064
+ /* ---------- pipes: { price | currency:'EUR' | upper } ---------- */
1065
+ // positions of `want` at the top level of an expression: not inside (), [], {}, strings or regular expressions.
1066
+ // For '|' a lone bar only: `a || b` and `a |= b` are not pipes.
1067
+ function topLevel(s, want) {
1068
+ const hits = [];
1069
+ let depth = 0, prev = '';
1070
+ for (let i = 0; i < s.length; i++) {
1071
+ const c = s[i];
1072
+ if (c === '"' || c === "'" || c === '`') {
1073
+ for (i++; i < s.length && s[i] !== c; i++) if (s[i] === '\\') i++;
1074
+ prev = c;
1075
+ continue;
1076
+ }
1077
+ if (c === '/' && s[i + 1] !== '/' && s[i + 1] !== '*' && (prev === '' || '(,=:?!&|{[;+-*%<>~^'.includes(prev))) { // a regular expression
1078
+ let cls = false;
1079
+ for (i++; i < s.length; i++) {
1080
+ if (s[i] === '\\') i++;
1081
+ else if (s[i] === '[') cls = true;
1082
+ else if (s[i] === ']') cls = false;
1083
+ else if (s[i] === '/' && !cls) break;
1084
+ }
1085
+ prev = '/';
1086
+ continue;
1087
+ }
1088
+ if (c === '(' || c === '[' || c === '{') depth++;
1089
+ else if (c === ')' || c === ']' || c === '}') depth--;
1090
+ else if (depth === 0 && c === want && (want !== '|' || (s[i + 1] !== '|' && s[i - 1] !== '|' && s[i + 1] !== '='))) hits.push(i);
1091
+ if (!/\s/.test(c)) prev = c;
1092
+ }
1093
+ return hits;
1094
+ }
1095
+ const cutAt = (s, cuts) => { const out = []; let from = 0; for (const i of cuts) { out.push(s.slice(from, i)); from = i + 1; } out.push(s.slice(from)); return out; };
1096
+ const pipeCache = new Map();
1097
+ // "price | currency:'EUR' | upper" → Kreftjs.__pipe("upper")(Kreftjs.__pipe("currency")((price), 'EUR'))
1098
+ // A pipe is a function in your component (a local function wins) or one added with Kreftjs.pipe(name, fn).
1099
+ function applyPipes(expr) {
1100
+ if (typeof expr !== 'string' || expr.indexOf('|') < 0) return expr;
1101
+ let r = pipeCache.get(expr);
1102
+ if (r !== undefined) return r;
1103
+ r = expr;
1104
+ const cuts = topLevel(expr, '|');
1105
+ if (cuts.length) {
1106
+ const segs = cutAt(expr, cuts);
1107
+ let code = '(' + segs[0].trim() + ')', ok = true;
1108
+ for (const seg of segs.slice(1)) {
1109
+ const m = /^\s*([A-Za-z_$][\w$]*)\s*([\s\S]*)$/.exec(seg);
1110
+ if (!m) { ok = false; break; }
1111
+ const name = m[1];
1112
+ const rest = m[2].trim();
1113
+ let args = '';
1114
+ if (rest[0] === ':') args = cutAt(rest.slice(1), topLevel(rest.slice(1), ':')).map((a) => a.trim()).join(', ');
1115
+ else if (rest[0] === '(' && rest[rest.length - 1] === ')') args = rest.slice(1, -1).trim();
1116
+ else if (rest) { ok = false; break; }
1117
+ code = `Kreftjs.__pipe(${JSON.stringify(name)}, typeof ${name} === "function" ? ${name} : null)(${code}${args ? ', ' + args : ''})`;
1118
+ }
1119
+ if (ok) r = code;
1120
+ }
1121
+ if (pipeCache.size > 5000) pipeCache.clear();
1122
+ pipeCache.set(expr, r);
1123
+ return r;
1124
+ }
1125
+ const pipes = Object.create(null);
1126
+ const asText = (v) => (v == null ? '' : String(v));
1127
+ // "2026-11-05 15:39:58" (a date from the Kreft database) has no time zone: the database stores UTC, so read it as UTC
1128
+ const toDate = (v) => (v instanceof Date ? v : typeof v === 'string' && /^\d{4}-\d\d-\d\d \d\d:\d\d(:\d\d)?$/.test(v) ? new Date(v.replace(' ', 'T') + 'Z') : new Date(v));
1129
+ Object.assign(pipes, {
1130
+ upper: (v) => asText(v).toUpperCase(),
1131
+ lower: (v) => asText(v).toLowerCase(),
1132
+ capitalize: (v) => { const t = asText(v); return t.charAt(0).toUpperCase() + t.slice(1); },
1133
+ trim: (v) => asText(v).trim(),
1134
+ truncate: (v, n = 50, tail = '…') => { const t = asText(v); return t.length > n ? t.slice(0, Math.max(0, n)).trimEnd() + tail : t; },
1135
+ currency: (v, code = 'USD') => (v == null || v === '' ? '' : formatCurrency(Number(v), code)),
1136
+ number: (v, digits) => (v == null || v === '' ? '' : formatNumber(Number(v), digits && typeof digits === 'object' ? digits : digits == null ? undefined : { maximumFractionDigits: digits, minimumFractionDigits: digits })),
1137
+ percent: (v, digits = 0) => (v == null || v === '' ? '' : formatNumber(Number(v), { style: 'percent', maximumFractionDigits: digits, minimumFractionDigits: digits })),
1138
+ date: (v, style = 'medium') => (v == null || v === '' ? '' : formatDate(toDate(v), typeof style === 'object' ? style : { dateStyle: style })),
1139
+ time: (v, style = 'short') => (v == null || v === '' ? '' : formatDate(toDate(v), typeof style === 'object' ? style : { timeStyle: style })),
1140
+ ago: (v) => {
1141
+ if (v == null || v === '') return '';
1142
+ const secs = Math.round((toDate(v).getTime() - Date.now()) / 1000);
1143
+ if (isNaN(secs)) return '';
1144
+ const rtf = new Intl.RelativeTimeFormat(currentLocaleRef.value, { numeric: 'auto' });
1145
+ const steps = [['year', 31536000], ['month', 2592000], ['day', 86400], ['hour', 3600], ['minute', 60]];
1146
+ for (const [unit, size] of steps) if (Math.abs(secs) >= size) return rtf.format(Math.round(secs / size), unit);
1147
+ return rtf.format(secs, 'second');
1148
+ },
1149
+ json: (v, indent = 2) => { try { return JSON.stringify(v, null, indent); } catch (_) { return String(v); } },
1150
+ fallback: (v, d = '') => (v == null || v === '' ? d : v),
1151
+ join: (v, sep = ', ') => (isArray(v) ? v.join(sep) : asText(v)),
1152
+ first: (v, n) => (n == null ? (v == null ? v : v[0]) : (v || []).slice(0, n)),
1153
+ last: (v, n) => (n == null ? (v == null ? v : v[v.length - 1]) : (v || []).slice(-n)),
1154
+ slice: (v, a, b) => (v == null ? v : v.slice(a, b)),
1155
+ reverse: (v) => (isArray(v) ? [...v].reverse() : [...asText(v)].reverse().join('')),
1156
+ sortBy: (v, key, dir = 'asc') => (isArray(v) ? [...v].sort((x, y) => { const a = key ? x[key] : x, b = key ? y[key] : y; return (a > b ? 1 : a < b ? -1 : 0) * (dir === 'desc' ? -1 : 1); }) : v),
1157
+ count: (v) => (v == null ? 0 : typeof v === 'object' && !isArray(v) ? Object.keys(v).length : v.length || 0),
1158
+ bytes: (v) => {
1159
+ let n = Number(v);
1160
+ if (!isFinite(n)) return '';
1161
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
1162
+ let u = 0;
1163
+ while (Math.abs(n) >= 1024 && u < units.length - 1) { n /= 1024; u++; }
1164
+ return (u ? n.toFixed(n < 10 ? 1 : 0) : n) + ' ' + units[u];
1165
+ },
1166
+ });
1167
+ const definePipe = (name, fn) => { if (!isFunction(fn)) throw new Error(`Kreftjs.pipe("${name}", fn): fn must be a function.`); pipes[name] = fn; };
1168
+ function lookupPipe(name, local) {
1169
+ const f = local || pipes[name];
1170
+ if (f) return f;
1171
+ warn(`Unknown pipe "${name}". Use a function of your own named ${name}, or Kreftjs.pipe('${name}', fn).`);
1172
+ return (v) => v;
1173
+ }
1174
+
1175
+ /* ---------- prop types: prop size: 'sm' | 'lg' = 'sm' ---------- */
1176
+ // TypeScript type text → (value) => boolean, or null when it can't be checked (your own type names, any)
1177
+ function typeCheck(text) {
1178
+ const members = cutAt(String(text), topLevel(String(text), '|')).map((t) => t.trim()).filter(Boolean);
1179
+ if (!members.length) return null;
1180
+ const checks = [];
1181
+ for (const m of members) {
1182
+ let c;
1183
+ if (m === 'string') c = (v) => typeof v === 'string';
1184
+ else if (m === 'number') c = (v) => typeof v === 'number' && !isNaN(v);
1185
+ else if (m === 'boolean') c = (v) => typeof v === 'boolean';
1186
+ else if (m === 'null') c = (v) => v === null;
1187
+ else if (m === 'undefined' || m === 'void') c = (v) => v === undefined;
1188
+ else if (m === 'true' || m === 'false') c = (v) => v === (m === 'true');
1189
+ else if (m === 'object') c = (v) => v !== null && typeof v === 'object';
1190
+ else if (m === 'Date') c = (v) => v instanceof Date;
1191
+ else if (m === 'Function' || /=>/.test(m)) c = isFunction;
1192
+ else if (/\[\]$/.test(m) || /^(?:readonly\s+)?Array\s*</.test(m) || /^\[/.test(m)) c = isArray;
1193
+ else if (/^(?:Record|Partial|Map|Set)\s*</.test(m) || m[0] === '{') c = (v) => v !== null && typeof v === 'object';
1194
+ else if (/^(['"`])[\s\S]*\1$/.test(m)) { const lit = m.slice(1, -1); c = (v) => v === lit; }
1195
+ else if (/^-?\d+(?:\.\d+)?$/.test(m)) { const n = Number(m); c = (v) => v === n; }
1196
+ else return null; // any, unknown, User, … : not checked
1197
+ checks.push(c);
1198
+ }
1199
+ return (v) => checks.some((c) => c(v));
1200
+ }
1201
+ const simpleType = (t) => ({ string: String, number: Number, boolean: Boolean }[String(t).trim()]);
1202
+ const describeValue = (v) => (v === null ? 'null' : isArray(v) ? 'an array' : typeof v === 'string' ? `"${v.length > 20 ? v.slice(0, 20) + '…' : v}"` : typeof v === 'object' ? 'an object' : typeof v + (typeof v === 'number' || typeof v === 'boolean' ? ' ' + v : ''));
1203
+ function checkProp(comp, k, opt, v) {
1204
+ if (opt.check && v !== undefined && !opt.check(v)) warn(`<${comp}> prop "${k}" should be ${opt.typeText}, but got ${describeValue(v)}.`);
1205
+ }
1206
+ function typedProp(opt, info) {
1207
+ opt.typeText = info.type;
1208
+ const chk = typeCheck(info.type);
1209
+ if (chk) opt.check = chk;
1210
+ if (info.required) opt.required = true;
1211
+ if (!opt.type) { const t = simpleType(info.type); if (t) opt.type = t; }
1212
+ return opt;
1213
+ }
1214
+
1063
1215
  /* ---------- { expr } interpolation ---------- */
1064
1216
  // "Hi {name}!" → [{ text: 'Hi ' }, { expr: 'name' }, { text: '!' }]; write \{ for a literal brace
1065
1217
  const interpCache = new Map();
@@ -1087,7 +1239,7 @@
1087
1239
  if (!expr) { buf += str.slice(i, end); i = end; continue; }
1088
1240
  if (buf) parts.push({ text: buf });
1089
1241
  buf = '';
1090
- parts.push({ expr });
1242
+ parts.push({ expr: applyPipes(expr) });
1091
1243
  i = end;
1092
1244
  continue;
1093
1245
  }
@@ -1108,7 +1260,7 @@
1108
1260
  function exactBraceRaw(v) {
1109
1261
  const t = (v || '').trim();
1110
1262
  if (t[0] !== '{') return null;
1111
- try { return matchBrace(t, 0) === t.length ? t.slice(1, -1).trim() : null; } catch (_) { return null; }
1263
+ try { return matchBrace(t, 0) === t.length ? applyPipes(t.slice(1, -1).trim()) : null; } catch (_) { return null; }
1112
1264
  }
1113
1265
  const stripBrace = (v) => { const e = exactBrace(v); return e != null ? e : v; };
1114
1266
  // "btn {active ? 'on' : ''}" → "`btn ${…}`"; null when there is nothing to interpolate
@@ -1791,6 +1943,41 @@
1791
1943
  };
1792
1944
  }
1793
1945
 
1946
+ /* ---------- k-match / k-case / k-default ---------- */
1947
+ // <div k-match="status">
1948
+ // <b k-case="'ok'">Fine</b>
1949
+ // <i k-case="'wait', 'queued'">Waiting</i> several values: one of them
1950
+ // <u k-default>Unknown</u>
1951
+ // </div>
1952
+ // The children turn into k-if / k-elif / k-else, so animations, components and templates work as they do there.
1953
+ function caseCond(subject, raw) {
1954
+ const list = cutAt(raw, topLevel(raw, ',')).map((v) => v.trim()).filter(Boolean);
1955
+ return list.length > 1 ? `[${list.join(', ')}].includes(${subject})` : `(${subject}) === (${list[0] || 'undefined'})`;
1956
+ }
1957
+ function compileMatch(el, scope, inst) {
1958
+ const subject = stripBrace(el.getAttribute('k-match'));
1959
+ el.removeAttribute('k-match');
1960
+ let started = false, fallback = null;
1961
+ for (const child of [...el.children]) {
1962
+ if (child.hasAttribute('k-case')) {
1963
+ const raw = stripBrace(child.getAttribute('k-case'));
1964
+ child.removeAttribute('k-case');
1965
+ child.setAttribute(started ? 'k-elif' : 'k-if', caseCond(subject, raw));
1966
+ started = true;
1967
+ } else if (child.hasAttribute('k-default')) {
1968
+ child.removeAttribute('k-default');
1969
+ if (fallback) warn('k-match has more than one k-default; only the first is used.', child);
1970
+ else fallback = child;
1971
+ }
1972
+ }
1973
+ if (fallback) {
1974
+ if (fallback.nextElementSibling) warn('k-default belongs last in k-match.', fallback);
1975
+ if (started) { fallback.setAttribute('k-else', ''); el.appendChild(fallback); } // right after the last case
1976
+ else fallback.setAttribute('k-if', 'true');
1977
+ }
1978
+ return compile(el, scope, inst);
1979
+ }
1980
+
1794
1981
  /* ---------- k-if / k-elif / k-else ---------- */
1795
1982
  function compileIf(el, scope, inst) {
1796
1983
  const anchor = text();
@@ -2811,12 +2998,13 @@
2811
2998
  if (hasType(opt, Number) && v !== '' && !isNaN(v)) v = Number(v);
2812
2999
  } else if (hasOwn(extra, k)) { v = extra[k]; present = true; }
2813
3000
  props[k] = resolvePropValue(opt, v, present);
3001
+ checkProp(def.__name || def.name || 'component', k, opt, props[k]);
2814
3002
  if (opt.required && !present) warn(`Missing required prop: "${k}"`);
2815
3003
  }
2816
3004
  for (const k in extra) if (!hasOwn(propsOptions, k)) attrs[k] = extra[k];
2817
3005
  dynamicProps.forEach(([k, expr]) => {
2818
3006
  if (!hasOwn(propsOptions, k)) return void dynamicAttrs.push([k, expr]);
2819
- renderEffect(() => { props[k] = resolvePropValue(propsOptions[k], evaluate(parentScope, expr, el), true); });
3007
+ renderEffect(() => { props[k] = resolvePropValue(propsOptions[k], evaluate(parentScope, expr, el), true); checkProp(def.__name || def.name || 'component', k, propsOptions[k], props[k]); });
2820
3008
  });
2821
3009
  dynamicAttrs.forEach(([k, expr]) => renderEffect(() => { attrs[k] = evaluate(parentScope, expr, el); }));
2822
3010
  if (options.liveProps) renderEffect(() => { // props that follow the route (<kreft-view>)
@@ -3993,7 +4181,17 @@
3993
4181
  try { return parseKreftScriptInner(src, url); } finally { scanScript = false; }
3994
4182
  }
3995
4183
  function parseKreftScriptInner(src, url) {
3996
- const out = [], imports = [], props = {}, events = [];
4184
+ const out = [], imports = [], props = {}, events = [], propTypes = {};
4185
+ // `prop size: 'sm' | 'lg' = 'sm'` keeps its type, to warn when a parent sends something else
4186
+ const notePropType = (lhs, hasDefault) => {
4187
+ const c = lhs.indexOf(':');
4188
+ if (c < 0) return;
4189
+ const raw = lhs.slice(0, c).trim();
4190
+ const name = raw.replace(/\?$/, '').trim();
4191
+ const type = lhs.slice(c + 1).trim();
4192
+ if (!type || !IDENT.test(name)) return;
4193
+ propTypes[name] = { type, required: !hasDefault && !/\?$/.test(raw) && !/(?:^|[|\s])(?:null|undefined|any|unknown)(?:[|\s]|$)/.test(type) };
4194
+ };
3997
4195
  const kw = /(prop|state|derive|derived|action|when|watch|on|use|share|receive|event|import|saved|load|layout|interface|type)(?![\w$])/y;
3998
4196
  let layout;
3999
4197
  // `saved` and `load` are keywords when followed by `name =` or `name: Type =`, so `load(…)` stays a normal call
@@ -4089,6 +4287,7 @@
4089
4287
  if (word === 'prop' && eq > 0) {
4090
4288
  const { name, value } = nameValue(j, end, 'prop');
4091
4289
  props[name] = value;
4290
+ notePropType(text.slice(0, eq), true);
4092
4291
  } else if (word === 'use' && eq > 0) { // use mouse = trackMouse() or use mouse: MousePos = trackMouse()
4093
4292
  const { name, value } = nameValue(j, end, 'use');
4094
4293
  out.push(`$h.use(${JSON.stringify(name)}, () => (${value}));`);
@@ -4097,7 +4296,7 @@
4097
4296
  const colon = text.indexOf(':');
4098
4297
  const name = text.slice(0, colon).replace(/\?\s*$/, '').trim();
4099
4298
  if (!IDENT.test(name) && word !== 'event') fail(j, `Invalid name "${name}"`);
4100
- if (word === 'prop') props[name] = null;
4299
+ if (word === 'prop') { props[name] = null; notePropType(text, false); }
4101
4300
  else events.push(name);
4102
4301
  } else {
4103
4302
  text.split(',').map((s) => s.trim()).filter(Boolean).forEach((rawN) => {
@@ -4194,7 +4393,7 @@
4194
4393
  fail(i, e.message);
4195
4394
  }
4196
4395
  }
4197
- return { code: out.join('\n'), imports, props, events, layout };
4396
+ return { code: out.join('\n'), imports, props, propTypes, events, layout };
4198
4397
  }
4199
4398
 
4200
4399
  // a CSS file imported by a .kreft file: one <link> for the whole page, before the components' own styles
@@ -4451,6 +4650,7 @@
4451
4650
  code: parsed.code,
4452
4651
  imports: parsed.imports,
4453
4652
  props: parsed.props, // name → default expression (or null)
4653
+ propTypes: parsed.propTypes, // name → { type, required } from `prop name: Type`
4454
4654
  events: parsed.events,
4455
4655
  layout: parsed.layout, // undefined: not said; false: none; or a name / path
4456
4656
  styles: styles.map((m) => ({ css: m[2], scoped: /\bscoped\b/.test(m[1] || '') })),
@@ -4483,11 +4683,14 @@
4483
4683
  const props = {};
4484
4684
  for (const p in sp.props) {
4485
4685
  const expr = sp.props[p];
4486
- if (expr == null) { props[p] = {}; continue; }
4487
- const make = new Function(`return (${expr})`);
4488
- let sample;
4489
- try { sample = make(); } catch (_) { /* default uses something only known later */ }
4490
- props[p] = isFunction(sample) ? { type: Function, default: sample } : { type: propType(sample), default: make };
4686
+ if (expr == null) props[p] = {};
4687
+ else {
4688
+ const make = new Function(`return (${expr})`);
4689
+ let sample;
4690
+ try { sample = make(); } catch (_) { /* default uses something only known later */ }
4691
+ props[p] = isFunction(sample) ? { type: Function, default: sample } : { type: propType(sample), default: make };
4692
+ }
4693
+ if (sp.propTypes && sp.propTypes[p]) typedProp(props[p], sp.propTypes[p]);
4491
4694
  }
4492
4695
 
4493
4696
  let scopeId = null;
@@ -5398,7 +5601,7 @@
5398
5601
  stores, storeLog, busLog,
5399
5602
  };
5400
5603
 
5401
- const VERSION = '0.0.20-beta';
5604
+ const VERSION = '0.0.22-beta';
5402
5605
  const Kreftjs = {
5403
5606
  version: VERSION,
5404
5607
  // PUBLIC_ values from .env (kreft dev and kreft build put them here); never secrets
@@ -5450,6 +5653,8 @@
5450
5653
  choice: choiceTranslation, transChoice: choiceTranslation, trans_choice: choiceTranslation,
5451
5654
  hasTranslation,
5452
5655
  formatNumber, formatCurrency, formatDate,
5656
+ // pipes: { price | currency }; add yours with Kreftjs.pipe('name', fn)
5657
+ pipe: definePipe, pipes, __pipe: lookupPipe, __typeCheck: typeCheck,
5453
5658
  // reacting to changes
5454
5659
  watch, effect: watchEffect, tick: nextTick, onCleanup: onScopeDispose,
5455
5660
  // sharing data
@@ -5464,7 +5669,7 @@
5464
5669
 
5465
5670
  // the text-only helpers the build tool reuses, so built code matches the browser exactly
5466
5671
  Object.defineProperty(Kreftjs, '__internal', {
5467
- value: { expandBlocks, prepareTemplate, splitKreft, splitInterp, exactBrace, stripBrace, interpToExpr, quoteBraceAttrs, expandSelfClosing, scopeCss, hashString, propType, simplePathRE, fnExpRE, forAliasRE, makeKreftDef, compileFn, evaluate, stripTypeScript, stripTsParams },
5672
+ value: { expandBlocks, prepareTemplate, splitKreft, splitInterp, exactBrace, stripBrace, interpToExpr, quoteBraceAttrs, expandSelfClosing, scopeCss, hashString, propType, typeCheck, caseCond, simplePathRE, fnExpRE, forAliasRE, makeKreftDef, compileFn, evaluate, stripTypeScript, stripTsParams },
5468
5673
  });
5469
5674
 
5470
5675
  if (typeof module !== 'undefined' && module.exports) module.exports = Kreftjs;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kreftjs",
3
- "version": "0.0.20-beta",
3
+ "version": "0.0.22-beta",
4
4
  "description": "A tiny reactive framework with its own component language (.kreft files): router with nested routes, stores, saved data, animations, an API client, hot reload, DevTools and a template engine.",
5
5
  "license": "Elastic-2.0",
6
6
  "main": "kreftjs.js",
@@ -14,7 +14,7 @@
14
14
  "start": "node bin/kreftjs.js dev --api demo/db.json --db sqlite:demo/database/demo.db --migrations demo/database/migrations --server demo/server --backend demo --seed",
15
15
  "build": "node bin/kreftjs.js build demo --out dist/kreftjs-com --base / --include docs",
16
16
  "preview": "node bin/kreftjs.js dev dist --api demo/db.json --db sqlite:demo/database/demo.db --migrations demo/database/migrations --server demo/server --backend demo --seed",
17
- "test": "node --test test/security.test.js test/prerender.test.js test/seo.test.js test/rebrand.test.js test/noauth.test.js test/winspawn.test.js test/htaccess.test.js test/cookie-secure.test.js test/password-rule.test.js test/headline.test.js test/menu.test.js test/electron-all.test.js test/build-minify.test.js",
17
+ "test": "node --test test/security.test.js test/prerender.test.js test/seo.test.js test/rebrand.test.js test/noauth.test.js test/winspawn.test.js test/htaccess.test.js test/cookie-secure.test.js test/password-rule.test.js test/headline.test.js test/menu.test.js test/electron-all.test.js test/build-minify.test.js test/stream.test.js test/syntax.test.js test/memory.test.js test/shield.test.js test/mcp-client.test.js test/dbapi-secrets.test.js test/auth-csrf.test.js test/not-a-project.test.js",
18
18
  "electron": "kreftjs electron",
19
19
  "electron:dev": "kreftjs electron:dev",
20
20
  "electron:build": "kreftjs electron:build"
@@ -31,6 +31,7 @@
31
31
  "docs/",
32
32
  "book/",
33
33
  "assets/logo.png",
34
+ "!book/~$*",
34
35
  "README.md"
35
36
  ],
36
37
  "keywords": [
package/server.d.ts CHANGED
@@ -79,6 +79,8 @@ export declare function response(body?: any, status?: number, headers?: Record<s
79
79
  export declare namespace response {
80
80
  function json(body: any, status?: number, headers?: Record<string, string>): Response
81
81
  function noContent(): Response
82
+ /** send a file with byte ranges (206), so video and audio can seek: return response.stream(path, { type?, download?, headers? }) */
83
+ function stream(file: string, options?: { type?: string; download?: boolean; headers?: Record<string, string> }): Response
82
84
  }
83
85
  export declare function noContent(): Response
84
86
  export declare function abort(status: number, message?: string): never
@@ -971,6 +973,23 @@ export declare function namespace(prefix: string): Record<string, any>
971
973
  export declare function startAutoGarbageCollector(options?: { thresholdMB?: number; intervalMs?: number; log?: boolean }): void
972
974
  export declare function stopAutoGarbageCollector(): void
973
975
  export declare function getMemoryStats(): { rssMB: number; heapTotalMB: number; heapUsedMB: number; externalMB: number; gcExposed: boolean }
976
+ export interface MemoryAlert { type: 'memory-high' | 'memory-leak'; message: string; at: string; heapUsedMB: number; rssMB: number; [k: string]: any }
977
+ export interface MemoryReport {
978
+ heapUsedMB: number; heapTotalMB: number; rssMB: number; externalMB: number; gcExposed: boolean
979
+ running: boolean; startedAt: string | null; checks: number; cleanups: number; gcRuns: number; v8Collections: number
980
+ reclaimedMB: number; evictedEntries: number; trendMBPerMin: number | null; overStreak: number
981
+ leak: { suspected: boolean; riseMB: number; floorsMB: number[] }
982
+ alerts: MemoryAlert[]; history: Array<{ t: number; heapUsedMB: number; rssMB: number }>; config: any
983
+ }
984
+ /** Everything a dashboard or alert needs, as plain JSON */
985
+ export declare function getMemoryReport(): MemoryReport
986
+ /** Register a cache cleaner: fn(fraction) drops that share of its least-used entries and returns how many went */
987
+ export declare function onMemoryPressure(name: string, fn: (fraction: number) => number | void): () => void
988
+ /** Called when memory stays high after cleaning, or a leak is suspected */
989
+ export declare function onMemoryAlert(fn: (alert: MemoryAlert) => void): () => void
990
+ /** Writes a V8 heap snapshot (open in Chrome DevTools > Memory); returns the file path */
991
+ export declare function writeHeapSnapshot(file?: string): string
992
+ export declare function checkMemory(): { action: string; [k: string]: any }
974
993
 
975
994
  /** Native Model Context Protocol (MCP) Server */
976
995
  export declare class McpServer {
@@ -982,6 +1001,31 @@ export declare class McpServer {
982
1001
  }
983
1002
  export declare function getMcpServer(options?: { baseDir?: string }): McpServer
984
1003
 
1004
+ /** Kreft Shield: SHIELD=true in .env limits and bans abusive addresses */
1005
+ export interface ShieldBan { ip: string; until: string; level: number; secondsLeft: number }
1006
+ export declare const Shield: {
1007
+ enabled(): boolean
1008
+ bans(): ShieldBan[]
1009
+ ban(ip: string, minutes?: number, reason?: string): { ip: string; minutes: number; reason: string; until: string; level: number }
1010
+ unban(ip: string): boolean
1011
+ status(): { enabled: boolean; tracked: number; banned: number; blocked: number; limited: number; bans: number; eventLoopLagMs: number; overloaded: boolean; limits: Record<string, number> }
1012
+ on(event: 'ban', fn: (info: { ip: string; minutes: number; reason: string; until: string; level: number }) => void): typeof Shield
1013
+ off(event: 'ban', fn: (...a: any[]) => void): typeof Shield
1014
+ }
1015
+
1016
+ /** MCP client (Streamable HTTP): call tools on a remote MCP server */
1017
+ export declare class McpClient {
1018
+ constructor(url: string, options?: { headers?: Record<string, string>; timeout?: number })
1019
+ server: any
1020
+ connect(): Promise<this>
1021
+ tools(): Promise<Array<{ name: string; description?: string; inputSchema?: any }>>
1022
+ call(name: string, args?: Record<string, any>): Promise<any>
1023
+ resources(): Promise<any[]>
1024
+ readResource(uri: string): Promise<any>
1025
+ close(): Promise<void>
1026
+ }
1027
+ export declare const Mcp: { connect(url: string, options?: { headers?: Record<string, string>; timeout?: number }): Promise<McpClient> }
1028
+
985
1029
  export interface CleanHtmlOptions {
986
1030
  allowedTags?: string[]
987
1031
  allowedAttributes?: Record<string, string[]>
@@ -6,11 +6,15 @@
6
6
  //
7
7
  // An app or script that sends `device_name` when it logs in gets an API token in the answer instead:
8
8
  // Authorization: Bearer 5|k3J9…
9
- const { response, Str, Crypt, cookieSecure } = require('kreftjs/server')
9
+ const { response, Str, Crypt, Hash, cookieSecure } = require('kreftjs/server')
10
10
  const PersonalAccessToken = require('../Models/PersonalAccessToken')
11
11
 
12
12
  const SESSION = 'kreft_session'
13
13
  const XSRF = 'XSRF-TOKEN'
14
+ // The XSRF-TOKEN cookie is not a random value anyone could plant: it is derived from this login's secret token, so a
15
+ // page on a sibling subdomain (or a script that can only set cookies) cannot make one that matches. Only a page that
16
+ // can read this browser's cookie, i.e. your own, can send it back in X-XSRF-TOKEN.
17
+ const xsrfFor = (sessionToken) => Hash.sha256('xsrf|' + sessionToken)
14
18
  const days = () => Number(process.env.AUTH_TOKEN_DAYS || 30)
15
19
 
16
20
  const resolveSecure = () => typeof cookieSecure === 'function' ? cookieSecure() : (process.env.SESSION_SECURE_COOKIE ? process.env.SESSION_SECURE_COOKIE === 'true' : (process.env.APP_ENV === 'production' || /^https:/i.test(process.env.APP_URL || '')))
@@ -57,7 +61,7 @@ async function signIn(req, user, { remember = false, status = 200 } = {}) {
57
61
  path: '/',
58
62
  maxAge: remember ? days() * 86400 : undefined,
59
63
  })
60
- res.cookie(XSRF, Str.random(40), {
64
+ res.cookie(XSRF, xsrfFor(plainTextToken), {
61
65
  httpOnly: false,
62
66
  secure: isSecure,
63
67
  sameSite,
@@ -99,7 +103,7 @@ async function regenerate(req, res, user = null) {
99
103
  path: '/',
100
104
  maxAge: remember ? days() * 86400 : undefined,
101
105
  })
102
- res.cookie(XSRF, Str.random(40), {
106
+ res.cookie(XSRF, xsrfFor(plainTextToken), {
103
107
  httpOnly: false,
104
108
  secure: isSecure,
105
109
  sameSite,
@@ -174,4 +178,4 @@ function browserName(ua) {
174
178
  return os ? `${b} on ${os}` : b
175
179
  }
176
180
 
177
- module.exports = { signIn, signOut, regenerate, pendingTwoFactor, readPendingTwoFactor, recordTwoFactorFailure, consumeTwoFactorTicket, browserName, SESSION, XSRF }
181
+ module.exports = { xsrfFor, signIn, signOut, regenerate, pendingTwoFactor, readPendingTwoFactor, recordTwoFactorFailure, consumeTwoFactorTicket, browserName, SESSION, XSRF }
@@ -5,7 +5,7 @@ const { abort, Hash, Str, Crypt, response, cookieSecure } = require('kreftjs/ser
5
5
  const crypto = require('crypto')
6
6
  const PersonalAccessToken = require('../../Models/PersonalAccessToken')
7
7
  const User = require('../../Models/User')
8
- const { SESSION, XSRF } = require('../../Auth/Session')
8
+ const { SESSION, XSRF, xsrfFor } = require('../../Auth/Session')
9
9
 
10
10
  const SAFE = ['GET', 'HEAD', 'OPTIONS']
11
11
  const NEEDS = { GET: 'read', HEAD: 'read', OPTIONS: 'read', POST: 'create', PUT: 'update', PATCH: 'update', DELETE: 'delete' }
@@ -41,8 +41,11 @@ class Authenticate {
41
41
  abort(401, 'Unauthenticated.', extra)
42
42
  }
43
43
 
44
- // a cookie login must prove the page is ours for every change: X-XSRF-TOKEN = the XSRF-TOKEN cookie
45
- if (fromCookie && !SAFE.includes(req.method) && !same(req.header('x-xsrf-token'), req.cookie(XSRF))) {
44
+ // a cookie login must prove the page is ours for every change: X-XSRF-TOKEN must be the value made from this login's
45
+ // secret (Session.xsrfFor), which the XSRF-TOKEN cookie carries. A planted cookie is not enough, as it would be if
46
+ // the header only had to equal the cookie.
47
+ const expectedXsrf = fromCookie ? xsrfFor(sessionPayload) : null
48
+ if (fromCookie && !SAFE.includes(req.method) && !same(req.header('x-xsrf-token'), expectedXsrf)) {
46
49
  abort(419, 'CSRF token mismatch. Reload the page and try again.')
47
50
  }
48
51
 
@@ -76,7 +79,7 @@ class Authenticate {
76
79
  req.accessToken = token
77
80
  const res = await next(req)
78
81
  // a cookie login without the XSRF cookie (cleared, or an old browser): give it a new one
79
- if (fromCookie && !req.cookie(XSRF)) res.cookie(XSRF, Str.random(40), { httpOnly: false, secure: cookieSecure(), sameSite: process.env.SESSION_SAME_SITE || 'Lax', path: '/', maxAge: token.remember ? days * 86400 : undefined })
82
+ if (fromCookie && req.cookie(XSRF) !== expectedXsrf) res.cookie(XSRF, expectedXsrf, { httpOnly: false, secure: cookieSecure(), sameSite: process.env.SESSION_SAME_SITE || 'Lax', path: '/', maxAge: token.remember ? days * 86400 : undefined })
80
83
  return res
81
84
  }
82
85
  }
@@ -5,6 +5,10 @@ const User = require('../../app/Models/User')
5
5
  // that user is an admin too, so /admin opens for it
6
6
  class DatabaseSeeder extends Seeder {
7
7
  async run() {
8
+ // an admin whose password is "password" must never exist on a live site: this seeder is for development only
9
+ if (process.env.APP_ENV === 'production' || process.env.NODE_ENV === 'production') {
10
+ throw new Error('DatabaseSeeder creates an admin with the password "password" and refuses to run in production. Seed real data instead.')
11
+ }
8
12
  const user = await User.create({ name: 'Test User', email: 'test@example.com', password: Hash.make('password') })
9
13
  user.email_verified_at = new Date()
10
14
  user.is_admin = true // never fillable: only set like this, by your own code
@@ -1,4 +1,4 @@
1
- // Type definitions for Kreft 0.0.20-beta
1
+ // Type definitions for Kreft 0.0.22-beta
2
2
  // Use from TypeScript or for editor hints in plain JavaScript (// @ts-check).
3
3
 
4
4
  export interface Signal<T> { value: T }