@portel/photon-core 2.22.0 → 2.24.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/dist/base.d.ts +56 -0
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +100 -2
- package/dist/base.js.map +1 -1
- package/dist/bases-registry.d.ts +57 -0
- package/dist/bases-registry.d.ts.map +1 -0
- package/dist/bases-registry.js +127 -0
- package/dist/bases-registry.js.map +1 -0
- package/dist/data-paths.d.ts +31 -18
- package/dist/data-paths.d.ts.map +1 -1
- package/dist/data-paths.js +42 -43
- package/dist/data-paths.js.map +1 -1
- package/dist/description-sanitizer.d.ts +34 -0
- package/dist/description-sanitizer.d.ts.map +1 -0
- package/dist/description-sanitizer.js +80 -0
- package/dist/description-sanitizer.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +109 -1
- package/dist/memory.js.map +1 -1
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +96 -0
- package/dist/middleware.js.map +1 -1
- package/dist/mixins.d.ts.map +1 -1
- package/dist/mixins.js +9 -2
- package/dist/mixins.js.map +1 -1
- package/dist/path-resolver.d.ts +13 -1
- package/dist/path-resolver.d.ts.map +1 -1
- package/dist/path-resolver.js +23 -1
- package/dist/path-resolver.js.map +1 -1
- package/dist/photon-loader-lite.d.ts +17 -2
- package/dist/photon-loader-lite.d.ts.map +1 -1
- package/dist/photon-loader-lite.js +203 -26
- package/dist/photon-loader-lite.js.map +1 -1
- package/dist/schedule.d.ts +10 -1
- package/dist/schedule.d.ts.map +1 -1
- package/dist/schedule.js +20 -10
- package/dist/schedule.js.map +1 -1
- package/dist/schema-extractor.d.ts +9 -3
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +149 -17
- package/dist/schema-extractor.js.map +1 -1
- package/dist/stateful.d.ts +3 -2
- package/dist/stateful.d.ts.map +1 -1
- package/dist/stateful.js +18 -6
- package/dist/stateful.js.map +1 -1
- package/dist/types.d.ts +9 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/src/base.ts +123 -2
- package/src/bases-registry.ts +141 -0
- package/src/data-paths.ts +43 -49
- package/src/description-sanitizer.ts +102 -0
- package/src/index.ts +20 -1
- package/src/memory.ts +109 -0
- package/src/middleware.ts +98 -0
- package/src/mixins.ts +14 -2
- package/src/path-resolver.ts +26 -1
- package/src/photon-loader-lite.ts +214 -33
- package/src/schedule.ts +26 -10
- package/src/schema-extractor.ts +164 -17
- package/src/stateful.ts +19 -6
- package/src/types.ts +9 -0
package/dist/schema-extractor.js
CHANGED
|
@@ -11,6 +11,21 @@ import * as fs from 'fs/promises';
|
|
|
11
11
|
import * as ts from 'typescript';
|
|
12
12
|
import { parseDuration, parseRate } from './utils/duration.js';
|
|
13
13
|
import { builtinRegistry } from './middleware.js';
|
|
14
|
+
import { sanitizeDescription } from './description-sanitizer.js';
|
|
15
|
+
// Warn once per unique (method, rule) pair so poisoned photons don't spam.
|
|
16
|
+
const sanitizeWarnings = new Set();
|
|
17
|
+
// Track which `handle*` method names have already emitted a deprecation
|
|
18
|
+
// warning this process so large photons don't spam the console.
|
|
19
|
+
const handlePrefixWarned = new Set();
|
|
20
|
+
function warnHandlePrefixOnce(methodName) {
|
|
21
|
+
if (handlePrefixWarned.has(methodName))
|
|
22
|
+
return;
|
|
23
|
+
handlePrefixWarned.add(methodName);
|
|
24
|
+
// eslint-disable-next-line no-console
|
|
25
|
+
console.error(`[photon] deprecation: method "${methodName}" is being auto-registered as a ` +
|
|
26
|
+
`webhook via the legacy handle* prefix. Add an explicit "@webhook" JSDoc ` +
|
|
27
|
+
`tag; the prefix convention will be removed in the next minor release.`);
|
|
28
|
+
}
|
|
14
29
|
/**
|
|
15
30
|
* Extract schemas from a Photon MCP class file
|
|
16
31
|
*/
|
|
@@ -309,6 +324,31 @@ export class SchemaExtractor {
|
|
|
309
324
|
propertyDocs.set(propMatch[1], propMatch[2].trim());
|
|
310
325
|
}
|
|
311
326
|
const properties = [];
|
|
327
|
+
// Inline-JSDoc helper: pull the description from a /** ... */ comment
|
|
328
|
+
// that immediately precedes a property assignment inside the object
|
|
329
|
+
// literal. Class-level @property tags still win when both are
|
|
330
|
+
// present (so existing photons aren't disrupted).
|
|
331
|
+
const sourceText = sourceFile.text;
|
|
332
|
+
const inlineDescriptionFor = (prop) => {
|
|
333
|
+
const ranges = ts.getLeadingCommentRanges(sourceText, prop.pos);
|
|
334
|
+
if (!ranges || ranges.length === 0)
|
|
335
|
+
return undefined;
|
|
336
|
+
// Use the last comment block before the property (closest to it).
|
|
337
|
+
const block = ranges[ranges.length - 1];
|
|
338
|
+
if (sourceText[block.pos] !== '/' ||
|
|
339
|
+
sourceText[block.pos + 1] !== '*' ||
|
|
340
|
+
sourceText[block.pos + 2] !== '*') {
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
const raw = sourceText.slice(block.pos + 3, block.end - 2);
|
|
344
|
+
// Strip leading "*" markers, trim, collapse whitespace.
|
|
345
|
+
return raw
|
|
346
|
+
.split('\n')
|
|
347
|
+
.map((line) => line.replace(/^\s*\*\s?/, '').trim())
|
|
348
|
+
.filter(Boolean)
|
|
349
|
+
.join(' ')
|
|
350
|
+
.trim();
|
|
351
|
+
};
|
|
312
352
|
for (const prop of member.initializer.properties) {
|
|
313
353
|
if (!ts.isPropertyAssignment(prop))
|
|
314
354
|
continue;
|
|
@@ -374,7 +414,7 @@ export class SchemaExtractor {
|
|
|
374
414
|
properties.push({
|
|
375
415
|
name: propName,
|
|
376
416
|
type,
|
|
377
|
-
description: propertyDocs.get(propName),
|
|
417
|
+
description: propertyDocs.get(propName) ?? inlineDescriptionFor(prop),
|
|
378
418
|
default: defaultValue,
|
|
379
419
|
required,
|
|
380
420
|
});
|
|
@@ -837,10 +877,34 @@ export class SchemaExtractor {
|
|
|
837
877
|
const params = [];
|
|
838
878
|
try {
|
|
839
879
|
const sourceFile = ts.createSourceFile('temp.ts', source, ts.ScriptTarget.Latest, true);
|
|
880
|
+
// Per-parameter JSDoc: prefer an inline /** ... */ block immediately
|
|
881
|
+
// before the parameter (the natural way authors write it), fall back
|
|
882
|
+
// to a constructor-level @param tag.
|
|
883
|
+
const sourceText = sourceFile.text;
|
|
884
|
+
const inlineDescriptionFor = (param) => {
|
|
885
|
+
const ranges = ts.getLeadingCommentRanges(sourceText, param.pos);
|
|
886
|
+
if (!ranges || ranges.length === 0)
|
|
887
|
+
return undefined;
|
|
888
|
+
const block = ranges[ranges.length - 1];
|
|
889
|
+
if (sourceText[block.pos] !== '/' ||
|
|
890
|
+
sourceText[block.pos + 1] !== '*' ||
|
|
891
|
+
sourceText[block.pos + 2] !== '*') {
|
|
892
|
+
return undefined;
|
|
893
|
+
}
|
|
894
|
+
const raw = sourceText.slice(block.pos + 3, block.end - 2);
|
|
895
|
+
return raw
|
|
896
|
+
.split('\n')
|
|
897
|
+
.map((line) => line.replace(/^\s*\*\s?/, '').trim())
|
|
898
|
+
.filter(Boolean)
|
|
899
|
+
.join(' ')
|
|
900
|
+
.trim();
|
|
901
|
+
};
|
|
840
902
|
const visit = (node) => {
|
|
841
903
|
if (ts.isClassDeclaration(node)) {
|
|
842
904
|
node.members.forEach((member) => {
|
|
843
905
|
if (ts.isConstructorDeclaration(member)) {
|
|
906
|
+
const ctorJsdoc = this.getJSDocComment(member, sourceFile);
|
|
907
|
+
const ctorParamDocs = this.extractParamDocs(ctorJsdoc);
|
|
844
908
|
member.parameters.forEach((param) => {
|
|
845
909
|
if (param.name && ts.isIdentifier(param.name)) {
|
|
846
910
|
const name = param.name.getText(sourceFile);
|
|
@@ -858,6 +922,7 @@ export class SchemaExtractor {
|
|
|
858
922
|
hasDefault,
|
|
859
923
|
defaultValue,
|
|
860
924
|
isPrimitive: this.isPrimitiveType(type),
|
|
925
|
+
description: inlineDescriptionFor(param) ?? ctorParamDocs.get(name),
|
|
861
926
|
});
|
|
862
927
|
}
|
|
863
928
|
});
|
|
@@ -1074,6 +1139,27 @@ export class SchemaExtractor {
|
|
|
1074
1139
|
const def = builtinRegistry.get('locked');
|
|
1075
1140
|
declarations.push({ name: 'locked', config: { name: lockName }, phase: def?.phase ?? 60 });
|
|
1076
1141
|
}
|
|
1142
|
+
// @mask <field1,field2,...> — redact named fields from the response.
|
|
1143
|
+
// Accepts comma- or whitespace-separated field names.
|
|
1144
|
+
const maskMatch = jsdocContent.match(/@mask\s+([^\n@]+)/i);
|
|
1145
|
+
if (maskMatch) {
|
|
1146
|
+
const def = builtinRegistry.get('mask');
|
|
1147
|
+
const rawValue = maskMatch[1].trim();
|
|
1148
|
+
const config = def?.parseShorthand
|
|
1149
|
+
? def.parseShorthand(rawValue)
|
|
1150
|
+
: { fields: rawValue.split(/[,\s]+/).filter(Boolean), placeholder: '[REDACTED]' };
|
|
1151
|
+
declarations.push({ name: 'mask', config, phase: def?.phase ?? 85 });
|
|
1152
|
+
}
|
|
1153
|
+
// @maxResponseBytes <N> — cap serialized response size.
|
|
1154
|
+
const maxBytesMatch = jsdocContent.match(/@maxResponseBytes\s+(\d+)/i);
|
|
1155
|
+
if (maxBytesMatch) {
|
|
1156
|
+
const def = builtinRegistry.get('maxResponseBytes');
|
|
1157
|
+
const rawValue = maxBytesMatch[1];
|
|
1158
|
+
const config = def?.parseShorthand
|
|
1159
|
+
? def.parseShorthand(rawValue)
|
|
1160
|
+
: { limit: parseInt(rawValue, 10) || 0 };
|
|
1161
|
+
declarations.push({ name: 'maxResponseBytes', config, phase: def?.phase ?? 88 });
|
|
1162
|
+
}
|
|
1077
1163
|
// 2. Extract @use declarations
|
|
1078
1164
|
const useDecls = this.extractUseDeclarations(jsdocContent);
|
|
1079
1165
|
for (const { name, rawConfig } of useDecls) {
|
|
@@ -1100,7 +1186,7 @@ export class SchemaExtractor {
|
|
|
1100
1186
|
extractDescription(jsdocContent) {
|
|
1101
1187
|
// Split by @tags that appear at start of a JSDoc line (after optional * prefix)
|
|
1102
1188
|
// This avoids matching @tag references inline in description text
|
|
1103
|
-
const beforeTags = jsdocContent.split(/(?:^|\n)\s*\*?\s*@(?:param|example|returns?|throws?|see|since|deprecated|version|author|license|ui|icon|format|stateful|autorun|async|webhook|cron|scheduled|locked|fallback|logged|circuitBreaker|cached|timeout|retryable|throttled|debounced|queued|validate|use|Template|Static|mcp|photon|cli|tags|dependencies|csp|visibility|auth)\b/)[0];
|
|
1189
|
+
const beforeTags = jsdocContent.split(/(?:^|\n)\s*\*?\s*@(?:param|example|returns?|throws?|see|since|deprecated|version|author|license|ui|icon|format|stateful|autorun|async|webhook|cron|scheduled|locked|fallback|logged|circuitBreaker|cached|timeout|retryable|throttled|debounced|queued|validate|use|Template|Static|mcp|photon|cli|tags|dependencies|csp|visibility|auth|mask|maxResponseBytes)\b/)[0];
|
|
1104
1190
|
// Remove leading * from each line and trim
|
|
1105
1191
|
const lines = beforeTags
|
|
1106
1192
|
.split('\n')
|
|
@@ -1135,8 +1221,25 @@ export class SchemaExtractor {
|
|
|
1135
1221
|
prevWasBlank = false;
|
|
1136
1222
|
}
|
|
1137
1223
|
const description = parts.join('');
|
|
1138
|
-
// Clean up multiple spaces
|
|
1139
|
-
|
|
1224
|
+
// Clean up multiple spaces, then defend against tool-description poisoning.
|
|
1225
|
+
const collapsed = description.replace(/\s+/g, ' ').trim() || 'No description';
|
|
1226
|
+
const { cleaned, warnings, truncated } = sanitizeDescription(collapsed);
|
|
1227
|
+
if (warnings.length > 0 || truncated) {
|
|
1228
|
+
for (const w of warnings) {
|
|
1229
|
+
const key = `${w.rule}|${w.sample}`;
|
|
1230
|
+
if (sanitizeWarnings.has(key))
|
|
1231
|
+
continue;
|
|
1232
|
+
sanitizeWarnings.add(key);
|
|
1233
|
+
// eslint-disable-next-line no-console
|
|
1234
|
+
console.warn(`[photon] description sanitizer: redacted ${w.rule} (sample: "${w.sample}")`);
|
|
1235
|
+
}
|
|
1236
|
+
if (truncated && !sanitizeWarnings.has('truncated')) {
|
|
1237
|
+
sanitizeWarnings.add('truncated');
|
|
1238
|
+
// eslint-disable-next-line no-console
|
|
1239
|
+
console.warn('[photon] description sanitizer: truncated oversized description');
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return cleaned;
|
|
1140
1243
|
}
|
|
1141
1244
|
/**
|
|
1142
1245
|
* Extract parameter descriptions from JSDoc @param tags
|
|
@@ -1790,10 +1893,15 @@ export class SchemaExtractor {
|
|
|
1790
1893
|
// DAEMON FEATURE EXTRACTION
|
|
1791
1894
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1792
1895
|
/**
|
|
1793
|
-
* Extract webhook configuration from @webhook tag or handle* prefix
|
|
1896
|
+
* Extract webhook configuration from @webhook tag or handle* prefix.
|
|
1794
1897
|
* - @webhook → use method name as path
|
|
1795
1898
|
* - @webhook stripe → custom path "stripe"
|
|
1796
|
-
* - handle* prefix → auto-detected as webhook
|
|
1899
|
+
* - handle* prefix → auto-detected as webhook (DEPRECATED)
|
|
1900
|
+
*
|
|
1901
|
+
* The handle* prefix is a legacy convention being removed. It still
|
|
1902
|
+
* works for one release but emits a one-time stderr warning per
|
|
1903
|
+
* method so users can migrate to the explicit @webhook tag before
|
|
1904
|
+
* the convention is dropped.
|
|
1797
1905
|
*/
|
|
1798
1906
|
extractWebhook(jsdocContent, methodName) {
|
|
1799
1907
|
// Check for @webhook tag with optional path
|
|
@@ -1804,8 +1912,9 @@ export class SchemaExtractor {
|
|
|
1804
1912
|
// Return custom path if specified, otherwise true for bare @webhook
|
|
1805
1913
|
return path || true;
|
|
1806
1914
|
}
|
|
1807
|
-
// Check for handle* prefix (convention)
|
|
1915
|
+
// Check for handle* prefix (legacy convention — deprecated).
|
|
1808
1916
|
if (methodName.startsWith('handle')) {
|
|
1917
|
+
warnHandlePrefixOnce(methodName);
|
|
1809
1918
|
return true;
|
|
1810
1919
|
}
|
|
1811
1920
|
return undefined;
|
|
@@ -2091,6 +2200,10 @@ export class SchemaExtractor {
|
|
|
2091
2200
|
if (['panels', 'tabs', 'accordion', 'stack', 'columns'].includes(format)) {
|
|
2092
2201
|
return format;
|
|
2093
2202
|
}
|
|
2203
|
+
// Match declarative UI formats (A2UI v0.9 rides on AG-UI)
|
|
2204
|
+
if (format === 'a2ui') {
|
|
2205
|
+
return 'a2ui';
|
|
2206
|
+
}
|
|
2094
2207
|
return undefined;
|
|
2095
2208
|
}
|
|
2096
2209
|
/**
|
|
@@ -2630,34 +2743,53 @@ export class SchemaExtractor {
|
|
|
2630
2743
|
return mimeTypes[ext] || 'application/octet-stream';
|
|
2631
2744
|
}
|
|
2632
2745
|
}
|
|
2746
|
+
/**
|
|
2747
|
+
* Match a `this`-like base in source code. Covers:
|
|
2748
|
+
* this — literal
|
|
2749
|
+
* (this as any) — TS `as` cast (the most common workaround when
|
|
2750
|
+
* TypeScript can't see a runtime-injected method)
|
|
2751
|
+
* (this as SomeClass), (this as unknown as T)
|
|
2752
|
+
* (<any>this), (<T>this) — older angle-bracket cast syntax
|
|
2753
|
+
*
|
|
2754
|
+
* Not covered: aliasing (`const self = this`), destructuring
|
|
2755
|
+
* (`const { call } = this`), or bracket access (`this['call']`).
|
|
2756
|
+
* Those require dataflow analysis which a regex can't do; the loader
|
|
2757
|
+
* compensates by always-injecting the cheap convenience methods whose
|
|
2758
|
+
* gating would otherwise silently fail for those patterns.
|
|
2759
|
+
*/
|
|
2760
|
+
const THIS_BASE = String.raw `(?:\bthis\b|\(\s*<[^>]+>\s*this\s*\)|\(\s*this\s+as\s+[^)]+\))`;
|
|
2761
|
+
function memberAccess(name, trailing) {
|
|
2762
|
+
return new RegExp(`${THIS_BASE}\\s*\\.\\s*${name}\\s*${trailing}`);
|
|
2763
|
+
}
|
|
2633
2764
|
/**
|
|
2634
2765
|
* Detect capabilities used by a Photon from its source code.
|
|
2635
2766
|
*
|
|
2636
2767
|
* Scans for `this.emit(`, `this.memory`, `this.call(`, etc. patterns
|
|
2637
|
-
*
|
|
2768
|
+
* (including typed-access workarounds like `(this as any).call(`) and
|
|
2769
|
+
* returns the set of capabilities that the runtime should inject.
|
|
2638
2770
|
*
|
|
2639
2771
|
* This enables plain classes (no extends Photon) to use all framework
|
|
2640
2772
|
* features — the loader detects usage and injects automatically.
|
|
2641
2773
|
*/
|
|
2642
2774
|
export function detectCapabilities(source) {
|
|
2643
2775
|
const caps = new Set();
|
|
2644
|
-
if (
|
|
2776
|
+
if (memberAccess('emit', '\\(').test(source))
|
|
2645
2777
|
caps.add('emit');
|
|
2646
|
-
if (
|
|
2778
|
+
if (memberAccess('render', '\\(').test(source))
|
|
2647
2779
|
caps.add('emit'); // render() needs emit injection
|
|
2648
|
-
if (
|
|
2780
|
+
if (memberAccess('memory', '\\b').test(source))
|
|
2649
2781
|
caps.add('memory');
|
|
2650
|
-
if (
|
|
2782
|
+
if (memberAccess('call', '\\(').test(source))
|
|
2651
2783
|
caps.add('call');
|
|
2652
|
-
if (
|
|
2784
|
+
if (memberAccess('mcp', '\\(').test(source))
|
|
2653
2785
|
caps.add('mcp');
|
|
2654
|
-
if (
|
|
2786
|
+
if (memberAccess('withLock', '\\(').test(source))
|
|
2655
2787
|
caps.add('lock');
|
|
2656
|
-
if (
|
|
2788
|
+
if (memberAccess('instanceMeta', '\\b').test(source))
|
|
2657
2789
|
caps.add('instanceMeta');
|
|
2658
|
-
if (
|
|
2790
|
+
if (memberAccess('allInstances', '\\(').test(source))
|
|
2659
2791
|
caps.add('allInstances');
|
|
2660
|
-
if (
|
|
2792
|
+
if (memberAccess('caller', '\\b').test(source))
|
|
2661
2793
|
caps.add('caller');
|
|
2662
2794
|
return caps;
|
|
2663
2795
|
}
|