@portel/photon-core 2.23.0 → 2.25.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 +114 -0
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +195 -2
- package/dist/base.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/generator.d.ts +102 -0
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +28 -0
- 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/photon-loader-lite.js +41 -0
- package/dist/photon-loader-lite.js.map +1 -1
- package/dist/schedule.d.ts +41 -2
- package/dist/schedule.d.ts.map +1 -1
- package/dist/schedule.js +72 -16
- package/dist/schedule.js.map +1 -1
- package/dist/schema-extractor.d.ts +2 -1
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +135 -14
- package/dist/schema-extractor.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 +224 -2
- package/src/description-sanitizer.ts +102 -0
- package/src/generator.ts +93 -0
- package/src/index.ts +12 -0
- package/src/memory.ts +28 -0
- package/src/middleware.ts +98 -0
- package/src/mixins.ts +14 -2
- package/src/photon-loader-lite.ts +38 -0
- package/src/schedule.ts +98 -14
- package/src/schema-extractor.ts +147 -14
- package/src/types.ts +9 -0
package/dist/schema-extractor.js
CHANGED
|
@@ -11,6 +11,9 @@ 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();
|
|
14
17
|
// Track which `handle*` method names have already emitted a deprecation
|
|
15
18
|
// warning this process so large photons don't spam the console.
|
|
16
19
|
const handlePrefixWarned = new Set();
|
|
@@ -321,6 +324,31 @@ export class SchemaExtractor {
|
|
|
321
324
|
propertyDocs.set(propMatch[1], propMatch[2].trim());
|
|
322
325
|
}
|
|
323
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
|
+
};
|
|
324
352
|
for (const prop of member.initializer.properties) {
|
|
325
353
|
if (!ts.isPropertyAssignment(prop))
|
|
326
354
|
continue;
|
|
@@ -386,7 +414,7 @@ export class SchemaExtractor {
|
|
|
386
414
|
properties.push({
|
|
387
415
|
name: propName,
|
|
388
416
|
type,
|
|
389
|
-
description: propertyDocs.get(propName),
|
|
417
|
+
description: propertyDocs.get(propName) ?? inlineDescriptionFor(prop),
|
|
390
418
|
default: defaultValue,
|
|
391
419
|
required,
|
|
392
420
|
});
|
|
@@ -849,10 +877,34 @@ export class SchemaExtractor {
|
|
|
849
877
|
const params = [];
|
|
850
878
|
try {
|
|
851
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
|
+
};
|
|
852
902
|
const visit = (node) => {
|
|
853
903
|
if (ts.isClassDeclaration(node)) {
|
|
854
904
|
node.members.forEach((member) => {
|
|
855
905
|
if (ts.isConstructorDeclaration(member)) {
|
|
906
|
+
const ctorJsdoc = this.getJSDocComment(member, sourceFile);
|
|
907
|
+
const ctorParamDocs = this.extractParamDocs(ctorJsdoc);
|
|
856
908
|
member.parameters.forEach((param) => {
|
|
857
909
|
if (param.name && ts.isIdentifier(param.name)) {
|
|
858
910
|
const name = param.name.getText(sourceFile);
|
|
@@ -870,6 +922,7 @@ export class SchemaExtractor {
|
|
|
870
922
|
hasDefault,
|
|
871
923
|
defaultValue,
|
|
872
924
|
isPrimitive: this.isPrimitiveType(type),
|
|
925
|
+
description: inlineDescriptionFor(param) ?? ctorParamDocs.get(name),
|
|
873
926
|
});
|
|
874
927
|
}
|
|
875
928
|
});
|
|
@@ -1086,6 +1139,27 @@ export class SchemaExtractor {
|
|
|
1086
1139
|
const def = builtinRegistry.get('locked');
|
|
1087
1140
|
declarations.push({ name: 'locked', config: { name: lockName }, phase: def?.phase ?? 60 });
|
|
1088
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
|
+
}
|
|
1089
1163
|
// 2. Extract @use declarations
|
|
1090
1164
|
const useDecls = this.extractUseDeclarations(jsdocContent);
|
|
1091
1165
|
for (const { name, rawConfig } of useDecls) {
|
|
@@ -1112,7 +1186,7 @@ export class SchemaExtractor {
|
|
|
1112
1186
|
extractDescription(jsdocContent) {
|
|
1113
1187
|
// Split by @tags that appear at start of a JSDoc line (after optional * prefix)
|
|
1114
1188
|
// This avoids matching @tag references inline in description text
|
|
1115
|
-
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];
|
|
1116
1190
|
// Remove leading * from each line and trim
|
|
1117
1191
|
const lines = beforeTags
|
|
1118
1192
|
.split('\n')
|
|
@@ -1147,8 +1221,25 @@ export class SchemaExtractor {
|
|
|
1147
1221
|
prevWasBlank = false;
|
|
1148
1222
|
}
|
|
1149
1223
|
const description = parts.join('');
|
|
1150
|
-
// Clean up multiple spaces
|
|
1151
|
-
|
|
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;
|
|
1152
1243
|
}
|
|
1153
1244
|
/**
|
|
1154
1245
|
* Extract parameter descriptions from JSDoc @param tags
|
|
@@ -2109,6 +2200,10 @@ export class SchemaExtractor {
|
|
|
2109
2200
|
if (['panels', 'tabs', 'accordion', 'stack', 'columns'].includes(format)) {
|
|
2110
2201
|
return format;
|
|
2111
2202
|
}
|
|
2203
|
+
// Match declarative UI formats (A2UI v0.9 rides on AG-UI)
|
|
2204
|
+
if (format === 'a2ui') {
|
|
2205
|
+
return 'a2ui';
|
|
2206
|
+
}
|
|
2112
2207
|
return undefined;
|
|
2113
2208
|
}
|
|
2114
2209
|
/**
|
|
@@ -2648,34 +2743,60 @@ export class SchemaExtractor {
|
|
|
2648
2743
|
return mimeTypes[ext] || 'application/octet-stream';
|
|
2649
2744
|
}
|
|
2650
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
|
+
// The `(this as <T>)` alternative allows one level of nested parens inside
|
|
2761
|
+
// the type annotation so function-type syntax like `(k: string) => void`
|
|
2762
|
+
// doesn't truncate the match. Without the `(?:[^()]|\([^()]*\))+` fallback,
|
|
2763
|
+
// `(this as unknown as { memory: { set: (k: string) => Promise<void> } })`
|
|
2764
|
+
// would terminate at the first inner `)` and the trailing `.memory` access
|
|
2765
|
+
// would never be seen — silently disabling this.memory injection for any
|
|
2766
|
+
// plain class that uses a complex TS type cast to reach memory.
|
|
2767
|
+
const THIS_BASE = String.raw `(?:\bthis\b|\(\s*<[^>]+>\s*this\s*\)|\(\s*this\s+as\s+(?:[^()]|\([^()]*\))+\))`;
|
|
2768
|
+
function memberAccess(name, trailing) {
|
|
2769
|
+
return new RegExp(`${THIS_BASE}\\s*\\.\\s*${name}\\s*${trailing}`);
|
|
2770
|
+
}
|
|
2651
2771
|
/**
|
|
2652
2772
|
* Detect capabilities used by a Photon from its source code.
|
|
2653
2773
|
*
|
|
2654
2774
|
* Scans for `this.emit(`, `this.memory`, `this.call(`, etc. patterns
|
|
2655
|
-
*
|
|
2775
|
+
* (including typed-access workarounds like `(this as any).call(`) and
|
|
2776
|
+
* returns the set of capabilities that the runtime should inject.
|
|
2656
2777
|
*
|
|
2657
2778
|
* This enables plain classes (no extends Photon) to use all framework
|
|
2658
2779
|
* features — the loader detects usage and injects automatically.
|
|
2659
2780
|
*/
|
|
2660
2781
|
export function detectCapabilities(source) {
|
|
2661
2782
|
const caps = new Set();
|
|
2662
|
-
if (
|
|
2783
|
+
if (memberAccess('emit', '\\(').test(source))
|
|
2663
2784
|
caps.add('emit');
|
|
2664
|
-
if (
|
|
2785
|
+
if (memberAccess('render', '\\(').test(source))
|
|
2665
2786
|
caps.add('emit'); // render() needs emit injection
|
|
2666
|
-
if (
|
|
2787
|
+
if (memberAccess('memory', '\\b').test(source))
|
|
2667
2788
|
caps.add('memory');
|
|
2668
|
-
if (
|
|
2789
|
+
if (memberAccess('call', '\\(').test(source))
|
|
2669
2790
|
caps.add('call');
|
|
2670
|
-
if (
|
|
2791
|
+
if (memberAccess('mcp', '\\(').test(source))
|
|
2671
2792
|
caps.add('mcp');
|
|
2672
|
-
if (
|
|
2793
|
+
if (memberAccess('withLock', '\\(').test(source))
|
|
2673
2794
|
caps.add('lock');
|
|
2674
|
-
if (
|
|
2795
|
+
if (memberAccess('instanceMeta', '\\b').test(source))
|
|
2675
2796
|
caps.add('instanceMeta');
|
|
2676
|
-
if (
|
|
2797
|
+
if (memberAccess('allInstances', '\\(').test(source))
|
|
2677
2798
|
caps.add('allInstances');
|
|
2678
|
-
if (
|
|
2799
|
+
if (memberAccess('caller', '\\b').test(source))
|
|
2679
2800
|
caps.add('caller');
|
|
2680
2801
|
return caps;
|
|
2681
2802
|
}
|