@portel/photon-core 2.23.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 +37 -0
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +71 -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/index.d.ts +1 -0
- 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 +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 +2 -1
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +128 -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 +80 -2
- package/src/description-sanitizer.ts +102 -0
- package/src/index.ts +6 -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 +26 -10
- package/src/schema-extractor.ts +140 -14
- package/src/types.ts +9 -0
package/src/schema-extractor.ts
CHANGED
|
@@ -13,6 +13,10 @@ import * as ts from 'typescript';
|
|
|
13
13
|
import { ExtractedSchema, ConstructorParam, TemplateInfo, StaticInfo, OutputFormat, YieldInfo, MCPDependency, PhotonDependency, CLIDependency, ResolvedInjection, PhotonAssets, UIAsset, PromptAsset, ResourceAsset, ConfigSchema, ConfigParam, SettingsSchema, SettingsProperty, NotificationSubscription } from './types.js';
|
|
14
14
|
import { parseDuration, parseRate } from './utils/duration.js';
|
|
15
15
|
import { builtinRegistry, type MiddlewareDeclaration } from './middleware.js';
|
|
16
|
+
import { sanitizeDescription } from './description-sanitizer.js';
|
|
17
|
+
|
|
18
|
+
// Warn once per unique (method, rule) pair so poisoned photons don't spam.
|
|
19
|
+
const sanitizeWarnings = new Set<string>();
|
|
16
20
|
|
|
17
21
|
// Track which `handle*` method names have already emitted a deprecation
|
|
18
22
|
// warning this process so large photons don't spam the console.
|
|
@@ -396,6 +400,33 @@ export class SchemaExtractor {
|
|
|
396
400
|
|
|
397
401
|
const properties: SettingsProperty[] = [];
|
|
398
402
|
|
|
403
|
+
// Inline-JSDoc helper: pull the description from a /** ... */ comment
|
|
404
|
+
// that immediately precedes a property assignment inside the object
|
|
405
|
+
// literal. Class-level @property tags still win when both are
|
|
406
|
+
// present (so existing photons aren't disrupted).
|
|
407
|
+
const sourceText = sourceFile.text;
|
|
408
|
+
const inlineDescriptionFor = (prop: ts.Node): string | undefined => {
|
|
409
|
+
const ranges = ts.getLeadingCommentRanges(sourceText, prop.pos);
|
|
410
|
+
if (!ranges || ranges.length === 0) return undefined;
|
|
411
|
+
// Use the last comment block before the property (closest to it).
|
|
412
|
+
const block = ranges[ranges.length - 1];
|
|
413
|
+
if (
|
|
414
|
+
sourceText[block.pos] !== '/' ||
|
|
415
|
+
sourceText[block.pos + 1] !== '*' ||
|
|
416
|
+
sourceText[block.pos + 2] !== '*'
|
|
417
|
+
) {
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
420
|
+
const raw = sourceText.slice(block.pos + 3, block.end - 2);
|
|
421
|
+
// Strip leading "*" markers, trim, collapse whitespace.
|
|
422
|
+
return raw
|
|
423
|
+
.split('\n')
|
|
424
|
+
.map((line) => line.replace(/^\s*\*\s?/, '').trim())
|
|
425
|
+
.filter(Boolean)
|
|
426
|
+
.join(' ')
|
|
427
|
+
.trim();
|
|
428
|
+
};
|
|
429
|
+
|
|
399
430
|
for (const prop of member.initializer.properties) {
|
|
400
431
|
if (!ts.isPropertyAssignment(prop)) continue;
|
|
401
432
|
const propName = prop.name.getText(sourceFile);
|
|
@@ -452,7 +483,7 @@ export class SchemaExtractor {
|
|
|
452
483
|
properties.push({
|
|
453
484
|
name: propName,
|
|
454
485
|
type,
|
|
455
|
-
description: propertyDocs.get(propName),
|
|
486
|
+
description: propertyDocs.get(propName) ?? inlineDescriptionFor(prop),
|
|
456
487
|
default: defaultValue,
|
|
457
488
|
required,
|
|
458
489
|
});
|
|
@@ -996,10 +1027,36 @@ export class SchemaExtractor {
|
|
|
996
1027
|
true
|
|
997
1028
|
);
|
|
998
1029
|
|
|
1030
|
+
// Per-parameter JSDoc: prefer an inline /** ... */ block immediately
|
|
1031
|
+
// before the parameter (the natural way authors write it), fall back
|
|
1032
|
+
// to a constructor-level @param tag.
|
|
1033
|
+
const sourceText = sourceFile.text;
|
|
1034
|
+
const inlineDescriptionFor = (param: ts.Node): string | undefined => {
|
|
1035
|
+
const ranges = ts.getLeadingCommentRanges(sourceText, param.pos);
|
|
1036
|
+
if (!ranges || ranges.length === 0) return undefined;
|
|
1037
|
+
const block = ranges[ranges.length - 1];
|
|
1038
|
+
if (
|
|
1039
|
+
sourceText[block.pos] !== '/' ||
|
|
1040
|
+
sourceText[block.pos + 1] !== '*' ||
|
|
1041
|
+
sourceText[block.pos + 2] !== '*'
|
|
1042
|
+
) {
|
|
1043
|
+
return undefined;
|
|
1044
|
+
}
|
|
1045
|
+
const raw = sourceText.slice(block.pos + 3, block.end - 2);
|
|
1046
|
+
return raw
|
|
1047
|
+
.split('\n')
|
|
1048
|
+
.map((line) => line.replace(/^\s*\*\s?/, '').trim())
|
|
1049
|
+
.filter(Boolean)
|
|
1050
|
+
.join(' ')
|
|
1051
|
+
.trim();
|
|
1052
|
+
};
|
|
1053
|
+
|
|
999
1054
|
const visit = (node: ts.Node) => {
|
|
1000
1055
|
if (ts.isClassDeclaration(node)) {
|
|
1001
1056
|
node.members.forEach((member) => {
|
|
1002
1057
|
if (ts.isConstructorDeclaration(member)) {
|
|
1058
|
+
const ctorJsdoc = this.getJSDocComment(member as any, sourceFile);
|
|
1059
|
+
const ctorParamDocs = this.extractParamDocs(ctorJsdoc);
|
|
1003
1060
|
member.parameters.forEach((param) => {
|
|
1004
1061
|
if (param.name && ts.isIdentifier(param.name)) {
|
|
1005
1062
|
const name = param.name.getText(sourceFile);
|
|
@@ -1019,6 +1076,7 @@ export class SchemaExtractor {
|
|
|
1019
1076
|
hasDefault,
|
|
1020
1077
|
defaultValue,
|
|
1021
1078
|
isPrimitive: this.isPrimitiveType(type),
|
|
1079
|
+
description: inlineDescriptionFor(param) ?? ctorParamDocs.get(name),
|
|
1022
1080
|
});
|
|
1023
1081
|
}
|
|
1024
1082
|
});
|
|
@@ -1264,6 +1322,29 @@ export class SchemaExtractor {
|
|
|
1264
1322
|
declarations.push({ name: 'locked', config: { name: lockName }, phase: def?.phase ?? 60 });
|
|
1265
1323
|
}
|
|
1266
1324
|
|
|
1325
|
+
// @mask <field1,field2,...> — redact named fields from the response.
|
|
1326
|
+
// Accepts comma- or whitespace-separated field names.
|
|
1327
|
+
const maskMatch = jsdocContent.match(/@mask\s+([^\n@]+)/i);
|
|
1328
|
+
if (maskMatch) {
|
|
1329
|
+
const def = builtinRegistry.get('mask');
|
|
1330
|
+
const rawValue = maskMatch[1].trim();
|
|
1331
|
+
const config = def?.parseShorthand
|
|
1332
|
+
? def.parseShorthand(rawValue)
|
|
1333
|
+
: { fields: rawValue.split(/[,\s]+/).filter(Boolean), placeholder: '[REDACTED]' };
|
|
1334
|
+
declarations.push({ name: 'mask', config, phase: def?.phase ?? 85 });
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// @maxResponseBytes <N> — cap serialized response size.
|
|
1338
|
+
const maxBytesMatch = jsdocContent.match(/@maxResponseBytes\s+(\d+)/i);
|
|
1339
|
+
if (maxBytesMatch) {
|
|
1340
|
+
const def = builtinRegistry.get('maxResponseBytes');
|
|
1341
|
+
const rawValue = maxBytesMatch[1];
|
|
1342
|
+
const config = def?.parseShorthand
|
|
1343
|
+
? def.parseShorthand(rawValue)
|
|
1344
|
+
: { limit: parseInt(rawValue, 10) || 0 };
|
|
1345
|
+
declarations.push({ name: 'maxResponseBytes', config, phase: def?.phase ?? 88 });
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1267
1348
|
// 2. Extract @use declarations
|
|
1268
1349
|
const useDecls = this.extractUseDeclarations(jsdocContent);
|
|
1269
1350
|
for (const { name, rawConfig } of useDecls) {
|
|
@@ -1291,7 +1372,7 @@ export class SchemaExtractor {
|
|
|
1291
1372
|
private extractDescription(jsdocContent: string): string {
|
|
1292
1373
|
// Split by @tags that appear at start of a JSDoc line (after optional * prefix)
|
|
1293
1374
|
// This avoids matching @tag references inline in description text
|
|
1294
|
-
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];
|
|
1375
|
+
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];
|
|
1295
1376
|
|
|
1296
1377
|
// Remove leading * from each line and trim
|
|
1297
1378
|
const lines = beforeTags
|
|
@@ -1328,8 +1409,26 @@ export class SchemaExtractor {
|
|
|
1328
1409
|
|
|
1329
1410
|
const description = parts.join('');
|
|
1330
1411
|
|
|
1331
|
-
// Clean up multiple spaces
|
|
1332
|
-
|
|
1412
|
+
// Clean up multiple spaces, then defend against tool-description poisoning.
|
|
1413
|
+
const collapsed = description.replace(/\s+/g, ' ').trim() || 'No description';
|
|
1414
|
+
const { cleaned, warnings, truncated } = sanitizeDescription(collapsed);
|
|
1415
|
+
if (warnings.length > 0 || truncated) {
|
|
1416
|
+
for (const w of warnings) {
|
|
1417
|
+
const key = `${w.rule}|${w.sample}`;
|
|
1418
|
+
if (sanitizeWarnings.has(key)) continue;
|
|
1419
|
+
sanitizeWarnings.add(key);
|
|
1420
|
+
// eslint-disable-next-line no-console
|
|
1421
|
+
console.warn(
|
|
1422
|
+
`[photon] description sanitizer: redacted ${w.rule} (sample: "${w.sample}")`
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
if (truncated && !sanitizeWarnings.has('truncated')) {
|
|
1426
|
+
sanitizeWarnings.add('truncated');
|
|
1427
|
+
// eslint-disable-next-line no-console
|
|
1428
|
+
console.warn('[photon] description sanitizer: truncated oversized description');
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
return cleaned;
|
|
1333
1432
|
}
|
|
1334
1433
|
|
|
1335
1434
|
/**
|
|
@@ -2412,6 +2511,11 @@ export class SchemaExtractor {
|
|
|
2412
2511
|
return format as OutputFormat;
|
|
2413
2512
|
}
|
|
2414
2513
|
|
|
2514
|
+
// Match declarative UI formats (A2UI v0.9 rides on AG-UI)
|
|
2515
|
+
if (format === 'a2ui') {
|
|
2516
|
+
return 'a2ui' as OutputFormat;
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2415
2519
|
return undefined;
|
|
2416
2520
|
}
|
|
2417
2521
|
|
|
@@ -3025,25 +3129,47 @@ export class SchemaExtractor {
|
|
|
3025
3129
|
*/
|
|
3026
3130
|
export type PhotonCapability = 'emit' | 'memory' | 'call' | 'mcp' | 'lock' | 'instanceMeta' | 'allInstances' | 'caller';
|
|
3027
3131
|
|
|
3132
|
+
/**
|
|
3133
|
+
* Match a `this`-like base in source code. Covers:
|
|
3134
|
+
* this — literal
|
|
3135
|
+
* (this as any) — TS `as` cast (the most common workaround when
|
|
3136
|
+
* TypeScript can't see a runtime-injected method)
|
|
3137
|
+
* (this as SomeClass), (this as unknown as T)
|
|
3138
|
+
* (<any>this), (<T>this) — older angle-bracket cast syntax
|
|
3139
|
+
*
|
|
3140
|
+
* Not covered: aliasing (`const self = this`), destructuring
|
|
3141
|
+
* (`const { call } = this`), or bracket access (`this['call']`).
|
|
3142
|
+
* Those require dataflow analysis which a regex can't do; the loader
|
|
3143
|
+
* compensates by always-injecting the cheap convenience methods whose
|
|
3144
|
+
* gating would otherwise silently fail for those patterns.
|
|
3145
|
+
*/
|
|
3146
|
+
const THIS_BASE =
|
|
3147
|
+
String.raw`(?:\bthis\b|\(\s*<[^>]+>\s*this\s*\)|\(\s*this\s+as\s+[^)]+\))`;
|
|
3148
|
+
|
|
3149
|
+
function memberAccess(name: string, trailing: '\\(' | '\\b'): RegExp {
|
|
3150
|
+
return new RegExp(`${THIS_BASE}\\s*\\.\\s*${name}\\s*${trailing}`);
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3028
3153
|
/**
|
|
3029
3154
|
* Detect capabilities used by a Photon from its source code.
|
|
3030
3155
|
*
|
|
3031
3156
|
* Scans for `this.emit(`, `this.memory`, `this.call(`, etc. patterns
|
|
3032
|
-
*
|
|
3157
|
+
* (including typed-access workarounds like `(this as any).call(`) and
|
|
3158
|
+
* returns the set of capabilities that the runtime should inject.
|
|
3033
3159
|
*
|
|
3034
3160
|
* This enables plain classes (no extends Photon) to use all framework
|
|
3035
3161
|
* features — the loader detects usage and injects automatically.
|
|
3036
3162
|
*/
|
|
3037
3163
|
export function detectCapabilities(source: string): Set<PhotonCapability> {
|
|
3038
3164
|
const caps = new Set<PhotonCapability>();
|
|
3039
|
-
if (
|
|
3040
|
-
if (
|
|
3041
|
-
if (
|
|
3042
|
-
if (
|
|
3043
|
-
if (
|
|
3044
|
-
if (
|
|
3045
|
-
if (
|
|
3046
|
-
if (
|
|
3047
|
-
if (
|
|
3165
|
+
if (memberAccess('emit', '\\(').test(source)) caps.add('emit');
|
|
3166
|
+
if (memberAccess('render', '\\(').test(source)) caps.add('emit'); // render() needs emit injection
|
|
3167
|
+
if (memberAccess('memory', '\\b').test(source)) caps.add('memory');
|
|
3168
|
+
if (memberAccess('call', '\\(').test(source)) caps.add('call');
|
|
3169
|
+
if (memberAccess('mcp', '\\(').test(source)) caps.add('mcp');
|
|
3170
|
+
if (memberAccess('withLock', '\\(').test(source)) caps.add('lock');
|
|
3171
|
+
if (memberAccess('instanceMeta', '\\b').test(source)) caps.add('instanceMeta');
|
|
3172
|
+
if (memberAccess('allInstances', '\\(').test(source)) caps.add('allInstances');
|
|
3173
|
+
if (memberAccess('caller', '\\b').test(source)) caps.add('caller');
|
|
3048
3174
|
return caps;
|
|
3049
3175
|
}
|
package/src/types.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* - Visualization: chart, chart:<type>, metric, gauge, timeline, dashboard, cart
|
|
10
10
|
* - Content: json, markdown, yaml, xml, html, mermaid, code, code:<lang>, slides
|
|
11
11
|
* - Container: panels, tabs, accordion, stack, columns
|
|
12
|
+
* - Declarative: a2ui (A2UI v0.9 JSONL — emits createSurface/updateComponents/updateDataModel)
|
|
12
13
|
*/
|
|
13
14
|
export type OutputFormat =
|
|
14
15
|
| 'primitive' | 'table' | 'tree' | 'list' | 'none'
|
|
@@ -16,6 +17,7 @@ export type OutputFormat =
|
|
|
16
17
|
| 'card' | 'grid' | 'chips' | 'kv' | 'qr'
|
|
17
18
|
| 'chart' | `chart:${string}` | 'metric' | 'gauge' | 'timeline' | 'dashboard' | 'cart'
|
|
18
19
|
| 'panels' | 'tabs' | 'accordion' | 'stack' | 'columns'
|
|
20
|
+
| 'a2ui'
|
|
19
21
|
| `code` | `code:${string}`;
|
|
20
22
|
|
|
21
23
|
export interface PhotonTool {
|
|
@@ -299,6 +301,13 @@ export interface ConstructorParam {
|
|
|
299
301
|
defaultValue?: any;
|
|
300
302
|
/** True if type is string, number, or boolean (inject from env var) */
|
|
301
303
|
isPrimitive: boolean;
|
|
304
|
+
/**
|
|
305
|
+
* Per-parameter JSDoc description, taken from an inline `/** ... *\/`
|
|
306
|
+
* comment immediately before the parameter, or from a constructor-level
|
|
307
|
+
* `@param <name>` tag when the inline form is absent. Used by the Beam
|
|
308
|
+
* Setup form for field help text.
|
|
309
|
+
*/
|
|
310
|
+
description?: string;
|
|
302
311
|
}
|
|
303
312
|
|
|
304
313
|
/**
|