@telorun/templating 0.2.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/LICENSE +17 -0
- package/README.md +246 -0
- package/dist/builtins.d.ts +16 -0
- package/dist/builtins.d.ts.map +1 -0
- package/dist/builtins.js +28 -0
- package/dist/cel/analyze.d.ts +17 -0
- package/dist/cel/analyze.d.ts.map +1 -0
- package/dist/cel/analyze.js +116 -0
- package/dist/cel/compile.d.ts +15 -0
- package/dist/cel/compile.d.ts.map +1 -0
- package/dist/cel/compile.js +41 -0
- package/dist/cel/environment.d.ts +20 -0
- package/dist/cel/environment.d.ts.map +1 -0
- package/dist/cel/environment.js +40 -0
- package/dist/cel/walk.d.ts +13 -0
- package/dist/cel/walk.d.ts.map +1 -0
- package/dist/cel/walk.js +36 -0
- package/dist/engine.d.ts +47 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +1 -0
- package/dist/engines/cel.d.ts +7 -0
- package/dist/engines/cel.d.ts.map +1 -0
- package/dist/engines/cel.js +36 -0
- package/dist/engines/literal.d.ts +6 -0
- package/dist/engines/literal.d.ts.map +1 -0
- package/dist/engines/literal.js +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/registry.d.ts +10 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +19 -0
- package/dist/sentinel.d.ts +14 -0
- package/dist/sentinel.d.ts.map +1 -0
- package/dist/sentinel.js +10 -0
- package/dist/yaml-tags.d.ts +28 -0
- package/dist/yaml-tags.d.ts.map +1 -0
- package/dist/yaml-tags.js +61 -0
- package/package.json +52 -0
- package/src/builtins.ts +33 -0
- package/src/cel/analyze.ts +125 -0
- package/src/cel/compile.ts +47 -0
- package/src/cel/environment.ts +50 -0
- package/src/cel/walk.ts +43 -0
- package/src/engine.ts +53 -0
- package/src/engines/cel.ts +40 -0
- package/src/engines/literal.ts +16 -0
- package/src/index.ts +24 -0
- package/src/registry.ts +25 -0
- package/src/sentinel.ts +25 -0
- package/src/yaml-tags.ts +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# SUSTAINABLE USE LICENSE (Fair-code)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DiglyAI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, and distribute the Software for any purpose—including commercial purposes—subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
1. ANTI-COMPETITION RESTRICTION: The Software may not be provided to third parties as a managed service, commercial SaaS (Software-as-a-Service), PaaS (Platform-as-a-Service), BaaS (Backend-as-a-Service), or similar offering where the primary value provided to the user is the functionality of the Software itself, without a separate commercial license from the copyright holder.
|
|
8
|
+
|
|
9
|
+
2. PERMITTED COMMERCIAL USE: You are free to use the Software to build, host, and monetize your own commercial applications, products, and services, provided such use does not violate Clause 1.
|
|
10
|
+
|
|
11
|
+
3. ATTRIBUTION: This copyright notice and license must be included in all copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
4. CONTRIBUTIONS: Contributions to the Software are welcome and encouraged. By contributing, you agree that your contributions may be incorporated into the Software and distributed under this license.
|
|
14
|
+
|
|
15
|
+
5. DISCLAIMER: The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the Software or the use or other dealings in the Software.
|
|
16
|
+
|
|
17
|
+
For commercial licensing, managed hosting exemptions, or enterprise inquiries, please contact DiglyAI.
|
package/README.md
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./assets/telo.png" alt="Telo" width="200" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">Telo</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">Runtime for declarative backends.</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://github.com/telorun/telo/actions/workflows/test.yml"><img alt="Tests" src="https://github.com/telorun/telo/actions/workflows/test.yml/badge.svg" /></a>
|
|
11
|
+
<a href="https://www.npmjs.com/package/@telorun/cli"><img alt="node" src="https://img.shields.io/node/v/@telorun/cli" /></a>
|
|
12
|
+
<br />
|
|
13
|
+
<a href="https://github.com/telorun/telo/commits/main"><img alt="Last commit" src="https://img.shields.io/github/last-commit/telorun/telo" /></a>
|
|
14
|
+
<a href="https://github.com/telorun/telo/issues"><img alt="Issues" src="https://img.shields.io/github/issues/telorun/telo" /></a>
|
|
15
|
+
<a href="https://github.com/telorun/telo/pulls"><img alt="Pull requests" src="https://img.shields.io/github/issues-pr/telorun/telo" /></a>
|
|
16
|
+
<br />
|
|
17
|
+
<img alt="Changesets" src="https://img.shields.io/badge/maintained%20with-changesets-176de3" />
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
Telo is an execution engine (Micro-Kernel) that runs logic defined entirely in YAML manifests. Instead of writing imperative backend code, you define your routes, databases, schemas, and AI workflows as atomic, interconnected YAML documents. Telo takes those manifests and runs them.
|
|
21
|
+
|
|
22
|
+
Built to be language-agnostic and infinitely extensible.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Reconcile your manifest into a running backend
|
|
26
|
+
$ telo ./examples/hello-api.yaml
|
|
27
|
+
|
|
28
|
+
{"level":30,"time":1771610393008,"pid":1310178,"hostname":"dev","msg":"Server listening at http://127.0.0.1:8844"}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Why use Telo?
|
|
32
|
+
|
|
33
|
+
- **Open Standards:** Built on YAML, JSON Schema, and CEL — no proprietary DSL.
|
|
34
|
+
- **Static Analysis:** CEL type checking, reference validation, and IDE diagnostics catch errors before runtime.
|
|
35
|
+
- **Micro-Kernel Architecture:** Telo itself knows nothing about HTTP or SQL. Everything is a module you import, scope, and compose with typed variable and secret contracts.
|
|
36
|
+
- **Language Agnostic:** Available as a Node.js runtime today, with a shared YAML runtime contract that allows for future Rust or Go implementations without changing your manifests.
|
|
37
|
+
|
|
38
|
+
## What It Does
|
|
39
|
+
|
|
40
|
+
- **Loads** YAML resources and compiles CEL expressions (`${{ }}`) into an in-memory registry.
|
|
41
|
+
- **Resolves** resource dependencies via a multi-pass init loop, handling ordering automatically.
|
|
42
|
+
- **Indexes** resources by Kind and Name for constant-time lookup.
|
|
43
|
+
- **Dispatches** execution to the controller that owns each Kind.
|
|
44
|
+
|
|
45
|
+
## Example manifest
|
|
46
|
+
|
|
47
|
+
Here is an example Telo application that defines a simple HTTP API:
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
kind: Telo.Application
|
|
51
|
+
metadata:
|
|
52
|
+
name: feedback
|
|
53
|
+
version: 1.0.0
|
|
54
|
+
description: |
|
|
55
|
+
A complete feedback collection REST API — no code, pure YAML.
|
|
56
|
+
Persists entries to SQLite and serves them over HTTP.
|
|
57
|
+
targets:
|
|
58
|
+
- Migrations
|
|
59
|
+
- Server
|
|
60
|
+
---
|
|
61
|
+
kind: Telo.Import
|
|
62
|
+
metadata:
|
|
63
|
+
name: Http
|
|
64
|
+
source: ../modules/http-server
|
|
65
|
+
---
|
|
66
|
+
kind: Telo.Import
|
|
67
|
+
metadata:
|
|
68
|
+
name: Sql
|
|
69
|
+
source: ../modules/sql
|
|
70
|
+
---
|
|
71
|
+
# SQLite database — swap driver/host/database for PostgreSQL with zero YAML changes
|
|
72
|
+
kind: Sql.Connection
|
|
73
|
+
metadata:
|
|
74
|
+
name: Db
|
|
75
|
+
driver: sqlite
|
|
76
|
+
file: ./tmp/feedback.db
|
|
77
|
+
---
|
|
78
|
+
# Migrations: applied automatically before the server starts
|
|
79
|
+
kind: Sql.Migrations
|
|
80
|
+
metadata:
|
|
81
|
+
name: Migrations
|
|
82
|
+
connection:
|
|
83
|
+
kind: Sql.Connection
|
|
84
|
+
name: Db
|
|
85
|
+
---
|
|
86
|
+
kind: Sql.Migration
|
|
87
|
+
metadata:
|
|
88
|
+
name: Migration_20260413_182154_CreateFeedback
|
|
89
|
+
version: 20260413_182154_CreateFeedback
|
|
90
|
+
sql: |
|
|
91
|
+
CREATE TABLE IF NOT EXISTS feedback (
|
|
92
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
93
|
+
text TEXT NOT NULL,
|
|
94
|
+
source TEXT,
|
|
95
|
+
score INTEGER NOT NULL DEFAULT 0,
|
|
96
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
97
|
+
)
|
|
98
|
+
---
|
|
99
|
+
kind: Http.Server
|
|
100
|
+
metadata:
|
|
101
|
+
name: Server
|
|
102
|
+
baseUrl: http://localhost:8844
|
|
103
|
+
port: 8844
|
|
104
|
+
logger: true
|
|
105
|
+
openapi:
|
|
106
|
+
info:
|
|
107
|
+
title: Feedback API
|
|
108
|
+
version: 1.0.0
|
|
109
|
+
mounts:
|
|
110
|
+
- path: /v1
|
|
111
|
+
type: Http.Api.FeedbackRoutes
|
|
112
|
+
---
|
|
113
|
+
kind: Http.Api
|
|
114
|
+
metadata:
|
|
115
|
+
name: FeedbackRoutes
|
|
116
|
+
routes:
|
|
117
|
+
# POST /v1/feedback — insert a new entry, score derived from body length heuristic
|
|
118
|
+
- request:
|
|
119
|
+
path: /feedback
|
|
120
|
+
method: POST
|
|
121
|
+
schema:
|
|
122
|
+
body:
|
|
123
|
+
type: object
|
|
124
|
+
properties:
|
|
125
|
+
text:
|
|
126
|
+
type: string
|
|
127
|
+
minLength: 1
|
|
128
|
+
source:
|
|
129
|
+
type: string
|
|
130
|
+
required: [text]
|
|
131
|
+
handler:
|
|
132
|
+
kind: Sql.Exec
|
|
133
|
+
connection:
|
|
134
|
+
kind: Sql.Connection
|
|
135
|
+
name: Db
|
|
136
|
+
inputs:
|
|
137
|
+
sql: "INSERT INTO feedback (text, source, score) VALUES (?, ?, ?)"
|
|
138
|
+
bindings:
|
|
139
|
+
- "${{ request.body.text }}"
|
|
140
|
+
- "${{ request.body.source }}"
|
|
141
|
+
- "${{ size(request.body.text) }}"
|
|
142
|
+
response:
|
|
143
|
+
- status: 201
|
|
144
|
+
headers:
|
|
145
|
+
Content-Type: application/json
|
|
146
|
+
body:
|
|
147
|
+
ok: true
|
|
148
|
+
message: Feedback received
|
|
149
|
+
|
|
150
|
+
# GET /v1/feedback — list all entries, newest first
|
|
151
|
+
- request:
|
|
152
|
+
path: /feedback
|
|
153
|
+
method: GET
|
|
154
|
+
handler:
|
|
155
|
+
kind: Sql.Select
|
|
156
|
+
connection:
|
|
157
|
+
kind: Sql.Connection
|
|
158
|
+
name: Db
|
|
159
|
+
from: feedback
|
|
160
|
+
columns: [id, text, source, score, created_at]
|
|
161
|
+
orderBy:
|
|
162
|
+
- { column: created_at, direction: desc }
|
|
163
|
+
response:
|
|
164
|
+
- status: 200
|
|
165
|
+
headers:
|
|
166
|
+
Content-Type: application/json
|
|
167
|
+
body: "${{ result.rows }}"
|
|
168
|
+
|
|
169
|
+
# GET /v1/feedback/{id} — fetch a single entry
|
|
170
|
+
- request:
|
|
171
|
+
path: /feedback/{id}
|
|
172
|
+
method: GET
|
|
173
|
+
schema:
|
|
174
|
+
params:
|
|
175
|
+
type: object
|
|
176
|
+
properties:
|
|
177
|
+
id:
|
|
178
|
+
type: integer
|
|
179
|
+
required: [id]
|
|
180
|
+
handler:
|
|
181
|
+
kind: Sql.Select
|
|
182
|
+
connection:
|
|
183
|
+
kind: Sql.Connection
|
|
184
|
+
name: Db
|
|
185
|
+
from: feedback
|
|
186
|
+
columns: [id, text, source, score, created_at]
|
|
187
|
+
where:
|
|
188
|
+
- { column: id, op: "=", value: "${{ request.params.id }}" }
|
|
189
|
+
response:
|
|
190
|
+
- status: 200
|
|
191
|
+
when: "size(result.rows) > 0"
|
|
192
|
+
headers:
|
|
193
|
+
Content-Type: application/json
|
|
194
|
+
body: "${{ result.rows[0] }}"
|
|
195
|
+
- status: 404
|
|
196
|
+
headers:
|
|
197
|
+
Content-Type: application/json
|
|
198
|
+
body:
|
|
199
|
+
ok: false
|
|
200
|
+
message: Not found
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Status
|
|
204
|
+
|
|
205
|
+
Telo is under **active development**. The core runtime, module system, and standard library are functional, but the API surface — including YAML shapes — may change without notice. Not yet recommended for production use.
|
|
206
|
+
|
|
207
|
+
## The Meaning of Telo
|
|
208
|
+
|
|
209
|
+
The name Telo is derived from the Greek root Telos - meaning the "end goal", "purpose", or "final state". That is exactly the philosophy behind this runtime. In standard imperative programming, you have to write thousands of lines of code to tell a server exactly how to start. With Telo, you simply declare your desired final state.
|
|
210
|
+
|
|
211
|
+
You define the end state. Telo makes it real.
|
|
212
|
+
|
|
213
|
+
## Philosophy
|
|
214
|
+
|
|
215
|
+
Modern platforms often spend disproportionate effort on technical mechanics-wiring frameworks, managing infrastructure, and negotiating toolchains-while the original business problem gets delayed or diluted. Telo pushes in the opposite direction: it treats kernel execution as a stable, predictable host so teams can concentrate on the **business logic and outcomes** instead of the plumbing.
|
|
216
|
+
|
|
217
|
+
By separating "what the system should do" from "how it is hosted", the runtime reduces friction for domain‑level changes. Teams can move faster on product requirements, experiment more safely, and keep conversations centered on value delivered rather than implementation trivia.
|
|
218
|
+
|
|
219
|
+
Telo also aims to **join forces across all programming language communities**, so the best ideas, patterns, and implementations can converge into a shared kernel truth without forcing everyone into a single stack.
|
|
220
|
+
|
|
221
|
+
YAML also makes the system more **AI‑friendly** than traditional programming languages: it is explicit, structured, and easier for tools to generate, review, and transform without losing intent.
|
|
222
|
+
|
|
223
|
+
## Modularity
|
|
224
|
+
|
|
225
|
+
Telo is built around **modules** that own specific resource kinds. A module is loaded from a manifest, declares which kinds it implements, and then receives only the resources of those kinds. This keeps concerns isolated and lets teams compose systems from focused building blocks rather than monolithic services.
|
|
226
|
+
|
|
227
|
+
At kernel execution time, execution is always routed by **Kind.Name**. The kernel resolves the Kind to its owning module and hands off execution. Modules can call back into the kernel to execute other resources, enabling composition without tight coupling.
|
|
228
|
+
|
|
229
|
+
## Architecture
|
|
230
|
+
|
|
231
|
+
The architecture is inspired by Kubernetes-style manifests: declarative resources, explicit kinds, and a control plane that routes work based on those definitions.
|
|
232
|
+
Those manifests were taken to the next level by allowing them to run inside a standalone runtime host.
|
|
233
|
+
|
|
234
|
+
## See more at
|
|
235
|
+
|
|
236
|
+
- [Telo Kernel](./kernel/README.md)
|
|
237
|
+
- [Telo SDK for module authors](sdk/README.md)
|
|
238
|
+
- [Modules](modules/README.md)
|
|
239
|
+
|
|
240
|
+
## License
|
|
241
|
+
|
|
242
|
+
See [LICENSE](https://github.com/telorun/telo/blob/main/LICENSE).
|
|
243
|
+
|
|
244
|
+
## Contribution Note
|
|
245
|
+
|
|
246
|
+
By contributing, you agree that code and examples in this repository may be translated or re‑implemented in other programming languages (including by AI systems) to support the project’s polyglot goals.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { TemplatingEngineRegistry } from "./registry.js";
|
|
2
|
+
import type { TemplatingEngine } from "./engine.js";
|
|
3
|
+
/** Single source of truth for the built-in templating engines. Every host
|
|
4
|
+
* (kernel, analyzer, editor, vscode extension) calls `createDefaultRegistry`
|
|
5
|
+
* so the parse-side YAML tag set, the precompile dispatch, and the analyzer
|
|
6
|
+
* agree on which engines exist. Per-host à-la-carte registration would let
|
|
7
|
+
* a manifest validate clean in one host (e.g. `cel` only) and crash in
|
|
8
|
+
* another (e.g. `cel + literal`); always ship the same set. */
|
|
9
|
+
export declare const builtinEngines: readonly TemplatingEngine[];
|
|
10
|
+
export declare function createDefaultRegistry(): TemplatingEngineRegistry;
|
|
11
|
+
/** Memoized singleton: returns the default registry. Hosts that don't need
|
|
12
|
+
* per-instance isolation (precompile, the analyzer's tagged-value walker)
|
|
13
|
+
* should use this so they share the same registry instance the YAML tag
|
|
14
|
+
* factory uses. */
|
|
15
|
+
export declare function defaultRegistry(): TemplatingEngineRegistry;
|
|
16
|
+
//# sourceMappingURL=builtins.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD;;;;;gEAKgE;AAChE,eAAO,MAAM,cAAc,EAAE,SAAS,gBAAgB,EAA+B,CAAC;AAEtF,wBAAgB,qBAAqB,IAAI,wBAAwB,CAMhE;AAID;;;oBAGoB;AACpB,wBAAgB,eAAe,IAAI,wBAAwB,CAK1D"}
|
package/dist/builtins.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { celEngine } from "./engines/cel.js";
|
|
2
|
+
import { literalEngine } from "./engines/literal.js";
|
|
3
|
+
import { TemplatingEngineRegistry } from "./registry.js";
|
|
4
|
+
/** Single source of truth for the built-in templating engines. Every host
|
|
5
|
+
* (kernel, analyzer, editor, vscode extension) calls `createDefaultRegistry`
|
|
6
|
+
* so the parse-side YAML tag set, the precompile dispatch, and the analyzer
|
|
7
|
+
* agree on which engines exist. Per-host à-la-carte registration would let
|
|
8
|
+
* a manifest validate clean in one host (e.g. `cel` only) and crash in
|
|
9
|
+
* another (e.g. `cel + literal`); always ship the same set. */
|
|
10
|
+
export const builtinEngines = [celEngine, literalEngine];
|
|
11
|
+
export function createDefaultRegistry() {
|
|
12
|
+
const registry = new TemplatingEngineRegistry();
|
|
13
|
+
for (const engine of builtinEngines) {
|
|
14
|
+
registry.register(engine);
|
|
15
|
+
}
|
|
16
|
+
return registry;
|
|
17
|
+
}
|
|
18
|
+
let defaultRegistryCache;
|
|
19
|
+
/** Memoized singleton: returns the default registry. Hosts that don't need
|
|
20
|
+
* per-instance isolation (precompile, the analyzer's tagged-value walker)
|
|
21
|
+
* should use this so they share the same registry instance the YAML tag
|
|
22
|
+
* factory uses. */
|
|
23
|
+
export function defaultRegistry() {
|
|
24
|
+
if (!defaultRegistryCache) {
|
|
25
|
+
defaultRegistryCache = createDefaultRegistry();
|
|
26
|
+
}
|
|
27
|
+
return defaultRegistryCache;
|
|
28
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ASTNode } from "@marcbachmann/cel-js";
|
|
2
|
+
/**
|
|
3
|
+
* Extract all member-access chains from a CEL AST.
|
|
4
|
+
* Returns arrays like ["request", "query", "name"] for `request.query.name`.
|
|
5
|
+
* Chains that start with a call or non-identifier root are ignored.
|
|
6
|
+
* Bound variables in comprehension macros (filter, map, exists, all, exists_one) are excluded.
|
|
7
|
+
*/
|
|
8
|
+
export declare function extractAccessChains(node: ASTNode): string[][];
|
|
9
|
+
/**
|
|
10
|
+
* Check whether a member-access chain accesses only fields declared in a JSON Schema.
|
|
11
|
+
* Returns an error string if a field is unknown in a schema that declares explicit
|
|
12
|
+
* properties without `additionalProperties: true`, or if the chain attempts to
|
|
13
|
+
* reach inside an `x-telo-stream: true` property.
|
|
14
|
+
* Returns null when the chain is valid or the schema is too open to judge.
|
|
15
|
+
*/
|
|
16
|
+
export declare function validateChainAgainstSchema(chain: string[], schema: Record<string, any>): string | null;
|
|
17
|
+
//# sourceMappingURL=analyze.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../src/cel/analyze.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAEpD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,EAAE,EAAE,CAI7D;AA2ED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,MAAM,EAAE,EACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B,MAAM,GAAG,IAAI,CA2Bf"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract all member-access chains from a CEL AST.
|
|
3
|
+
* Returns arrays like ["request", "query", "name"] for `request.query.name`.
|
|
4
|
+
* Chains that start with a call or non-identifier root are ignored.
|
|
5
|
+
* Bound variables in comprehension macros (filter, map, exists, all, exists_one) are excluded.
|
|
6
|
+
*/
|
|
7
|
+
export function extractAccessChains(node) {
|
|
8
|
+
const chains = [];
|
|
9
|
+
visitNode(node, chains, new Set());
|
|
10
|
+
return chains;
|
|
11
|
+
}
|
|
12
|
+
const COMPREHENSION_METHODS = new Set(["filter", "map", "exists", "all", "exists_one"]);
|
|
13
|
+
function visitNode(node, chains, boundVars) {
|
|
14
|
+
const chain = extractChain(node, boundVars);
|
|
15
|
+
if (chain !== null) {
|
|
16
|
+
chains.push(chain);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (node.op === "rcall" &&
|
|
20
|
+
Array.isArray(node.args) &&
|
|
21
|
+
typeof node.args[0] === "string" &&
|
|
22
|
+
COMPREHENSION_METHODS.has(node.args[0])) {
|
|
23
|
+
const receiver = node.args[1];
|
|
24
|
+
const comprehensionArgs = node.args[2];
|
|
25
|
+
if (isASTNode(receiver))
|
|
26
|
+
visitNode(receiver, chains, boundVars);
|
|
27
|
+
if (Array.isArray(comprehensionArgs) &&
|
|
28
|
+
comprehensionArgs.length >= 2 &&
|
|
29
|
+
isASTNode(comprehensionArgs[0]) &&
|
|
30
|
+
comprehensionArgs[0].op === "id") {
|
|
31
|
+
const newBoundVars = new Set(boundVars);
|
|
32
|
+
newBoundVars.add(comprehensionArgs[0].args);
|
|
33
|
+
for (let i = 1; i < comprehensionArgs.length; i++) {
|
|
34
|
+
const arg = comprehensionArgs[i];
|
|
35
|
+
if (isASTNode(arg))
|
|
36
|
+
visitNode(arg, chains, newBoundVars);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const args = node.args;
|
|
42
|
+
if (Array.isArray(args)) {
|
|
43
|
+
for (const arg of args) {
|
|
44
|
+
if (isASTNode(arg)) {
|
|
45
|
+
visitNode(arg, chains, boundVars);
|
|
46
|
+
}
|
|
47
|
+
else if (Array.isArray(arg)) {
|
|
48
|
+
for (const item of arg) {
|
|
49
|
+
if (isASTNode(item))
|
|
50
|
+
visitNode(item, chains, boundVars);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function isASTNode(v) {
|
|
57
|
+
return v !== null && typeof v === "object" && "op" in v;
|
|
58
|
+
}
|
|
59
|
+
const INDEX_SEGMENT = "[*]";
|
|
60
|
+
function extractChain(node, boundVars) {
|
|
61
|
+
if (node.op === "id") {
|
|
62
|
+
const name = node.args;
|
|
63
|
+
if (boundVars.has(name))
|
|
64
|
+
return null;
|
|
65
|
+
return [name];
|
|
66
|
+
}
|
|
67
|
+
if (node.op === ".") {
|
|
68
|
+
const [obj, field] = node.args;
|
|
69
|
+
const parent = extractChain(obj, boundVars);
|
|
70
|
+
if (parent !== null)
|
|
71
|
+
return [...parent, field];
|
|
72
|
+
}
|
|
73
|
+
if (node.op === "[]") {
|
|
74
|
+
const [obj] = node.args;
|
|
75
|
+
const parent = extractChain(obj, boundVars);
|
|
76
|
+
if (parent !== null)
|
|
77
|
+
return [...parent, INDEX_SEGMENT];
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Check whether a member-access chain accesses only fields declared in a JSON Schema.
|
|
83
|
+
* Returns an error string if a field is unknown in a schema that declares explicit
|
|
84
|
+
* properties without `additionalProperties: true`, or if the chain attempts to
|
|
85
|
+
* reach inside an `x-telo-stream: true` property.
|
|
86
|
+
* Returns null when the chain is valid or the schema is too open to judge.
|
|
87
|
+
*/
|
|
88
|
+
export function validateChainAgainstSchema(chain, schema) {
|
|
89
|
+
let current = schema;
|
|
90
|
+
for (let i = 0; i < chain.length; i++) {
|
|
91
|
+
const key = chain[i];
|
|
92
|
+
if (!current || typeof current !== "object")
|
|
93
|
+
return null;
|
|
94
|
+
const props = current.properties;
|
|
95
|
+
if (!props)
|
|
96
|
+
return null;
|
|
97
|
+
if (key in props) {
|
|
98
|
+
const propSchema = props[key];
|
|
99
|
+
if (propSchema &&
|
|
100
|
+
typeof propSchema === "object" &&
|
|
101
|
+
propSchema["x-telo-stream"] === true &&
|
|
102
|
+
i < chain.length - 1) {
|
|
103
|
+
const path = chain.slice(0, i + 1).join(".");
|
|
104
|
+
return `'${path}' yields a stream — pipe it through an Encoder or iterate in a JS.Script step (no member access on stream-typed values)`;
|
|
105
|
+
}
|
|
106
|
+
current = propSchema;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (current.additionalProperties === true)
|
|
110
|
+
return null;
|
|
111
|
+
const path = chain.slice(0, i + 1).join(".");
|
|
112
|
+
const available = Object.keys(props).join(", ");
|
|
113
|
+
return `'${path}' is not defined (available: ${available})`;
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CompiledValue } from "@telorun/sdk";
|
|
2
|
+
import type { Environment } from "@marcbachmann/cel-js";
|
|
3
|
+
export declare const TEMPLATE_REGEX: RegExp;
|
|
4
|
+
export declare const EXACT_TEMPLATE_REGEX: RegExp;
|
|
5
|
+
/** Compile a single CEL expression (no `${{ }}` wrapping) into a CompiledValue.
|
|
6
|
+
* Throws on syntax errors. Used by the `!cel` engine where the entire tagged
|
|
7
|
+
* scalar is treated as one expression. */
|
|
8
|
+
export declare function compileExpression(expr: string, env: Environment): CompiledValue;
|
|
9
|
+
/** Compile a string that may contain `${{ }}`-delimited CEL segments. If the
|
|
10
|
+
* string is exactly one expression, returns a single CompiledValue. If it
|
|
11
|
+
* contains interpolations, returns a CompiledValue that joins literal parts
|
|
12
|
+
* with stringified expression results. If no expressions are present, returns
|
|
13
|
+
* the input string unchanged. Throws on CEL syntax errors. */
|
|
14
|
+
export declare function compileString(s: string, env: Environment): unknown;
|
|
15
|
+
//# sourceMappingURL=compile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../src/cel/compile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAExD,eAAO,MAAM,cAAc,QAA8B,CAAC;AAC1D,eAAO,MAAM,oBAAoB,QAAqC,CAAC;AAEvE;;2CAE2C;AAC3C,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,aAAa,CAO/E;AAED;;;;+DAI+D;AAC/D,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,CAuBlE"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const TEMPLATE_REGEX = /\$\{\{\s*([^}]+?)\s*\}\}/g;
|
|
2
|
+
export const EXACT_TEMPLATE_REGEX = /^\s*\$\{\{\s*([^}]+?)\s*\}\}\s*$/;
|
|
3
|
+
/** Compile a single CEL expression (no `${{ }}` wrapping) into a CompiledValue.
|
|
4
|
+
* Throws on syntax errors. Used by the `!cel` engine where the entire tagged
|
|
5
|
+
* scalar is treated as one expression. */
|
|
6
|
+
export function compileExpression(expr, env) {
|
|
7
|
+
const fn = env.parse(expr);
|
|
8
|
+
return {
|
|
9
|
+
__compiled: true,
|
|
10
|
+
source: expr,
|
|
11
|
+
call: (ctx) => fn(ctx),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** Compile a string that may contain `${{ }}`-delimited CEL segments. If the
|
|
15
|
+
* string is exactly one expression, returns a single CompiledValue. If it
|
|
16
|
+
* contains interpolations, returns a CompiledValue that joins literal parts
|
|
17
|
+
* with stringified expression results. If no expressions are present, returns
|
|
18
|
+
* the input string unchanged. Throws on CEL syntax errors. */
|
|
19
|
+
export function compileString(s, env) {
|
|
20
|
+
if (!s.includes("${{"))
|
|
21
|
+
return s;
|
|
22
|
+
const exact = s.match(EXACT_TEMPLATE_REGEX);
|
|
23
|
+
if (exact) {
|
|
24
|
+
return compileExpression(exact[1].trim(), env);
|
|
25
|
+
}
|
|
26
|
+
const parts = [];
|
|
27
|
+
let last = 0;
|
|
28
|
+
for (const m of s.matchAll(TEMPLATE_REGEX)) {
|
|
29
|
+
if (m.index > last)
|
|
30
|
+
parts.push(s.slice(last, m.index));
|
|
31
|
+
parts.push(compileExpression(m[1].trim(), env));
|
|
32
|
+
last = m.index + m[0].length;
|
|
33
|
+
}
|
|
34
|
+
if (last < s.length)
|
|
35
|
+
parts.push(s.slice(last));
|
|
36
|
+
return {
|
|
37
|
+
__compiled: true,
|
|
38
|
+
source: s,
|
|
39
|
+
call: (ctx) => parts.map((p) => (typeof p === "string" ? p : String(p.call(ctx) ?? ""))).join(""),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Environment } from "@marcbachmann/cel-js";
|
|
2
|
+
export interface CelHandlers {
|
|
3
|
+
sha256: (s: string) => string;
|
|
4
|
+
json: (value: unknown) => string;
|
|
5
|
+
}
|
|
6
|
+
/** Build a CEL `Environment` with Telo's stdlib of functions. Always registers the
|
|
7
|
+
* same function signatures (so `env.check()` succeeds for type-inference) — the
|
|
8
|
+
* handlers govern what the function does when called at runtime. Analyzer-only
|
|
9
|
+
* callers can omit handlers; runtime callers (kernel) must supply real ones.
|
|
10
|
+
*
|
|
11
|
+
* Also registers the `Stream` object type, backed by the `Stream` class from
|
|
12
|
+
* `@telorun/sdk`. CEL's type-checker rejects values whose constructor isn't
|
|
13
|
+
* Object/Map/Array/Set/registered; producers that need to expose an
|
|
14
|
+
* `AsyncIterable` through a stream-typed property must wrap the iterable in
|
|
15
|
+
* `new Stream(...)` so its constructor is the registered class. The type has
|
|
16
|
+
* no fields, so terminal access (passing the value through CEL) succeeds but
|
|
17
|
+
* member access raises a CEL error at runtime — matching the analyzer's
|
|
18
|
+
* static check on `x-telo-stream`-marked properties. */
|
|
19
|
+
export declare function buildCelEnvironment(handlers?: CelHandlers): Environment;
|
|
20
|
+
//# sourceMappingURL=environment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"environment.d.ts","sourceRoot":"","sources":["../../src/cel/environment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9B,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;CAClC;AAcD;;;;;;;;;;;;yDAYyD;AACzD,wBAAgB,mBAAmB,CAAC,QAAQ,GAAE,WAA2B,GAAG,WAAW,CAgBtF"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Environment } from "@marcbachmann/cel-js";
|
|
2
|
+
import { Stream } from "@telorun/sdk";
|
|
3
|
+
const stub = (name) => () => {
|
|
4
|
+
throw new Error(`${name}() is not available in this environment. ` +
|
|
5
|
+
`Construct StaticAnalyzer or Loader with celHandlers to enable it.`);
|
|
6
|
+
};
|
|
7
|
+
const STUB_HANDLERS = {
|
|
8
|
+
sha256: stub("sha256"),
|
|
9
|
+
json: stub("json"),
|
|
10
|
+
};
|
|
11
|
+
/** Build a CEL `Environment` with Telo's stdlib of functions. Always registers the
|
|
12
|
+
* same function signatures (so `env.check()` succeeds for type-inference) — the
|
|
13
|
+
* handlers govern what the function does when called at runtime. Analyzer-only
|
|
14
|
+
* callers can omit handlers; runtime callers (kernel) must supply real ones.
|
|
15
|
+
*
|
|
16
|
+
* Also registers the `Stream` object type, backed by the `Stream` class from
|
|
17
|
+
* `@telorun/sdk`. CEL's type-checker rejects values whose constructor isn't
|
|
18
|
+
* Object/Map/Array/Set/registered; producers that need to expose an
|
|
19
|
+
* `AsyncIterable` through a stream-typed property must wrap the iterable in
|
|
20
|
+
* `new Stream(...)` so its constructor is the registered class. The type has
|
|
21
|
+
* no fields, so terminal access (passing the value through CEL) succeeds but
|
|
22
|
+
* member access raises a CEL error at runtime — matching the analyzer's
|
|
23
|
+
* static check on `x-telo-stream`-marked properties. */
|
|
24
|
+
export function buildCelEnvironment(handlers = STUB_HANDLERS) {
|
|
25
|
+
return new Environment({ unlistedVariablesAreDyn: true, enableOptionalTypes: true })
|
|
26
|
+
.registerFunction("join(list, string): string", (list, sep) => list.map(String).join(sep))
|
|
27
|
+
.registerFunction("keys(map): list", (map) => {
|
|
28
|
+
if (map instanceof Map)
|
|
29
|
+
return [...map.keys()];
|
|
30
|
+
return Object.keys(map);
|
|
31
|
+
})
|
|
32
|
+
.registerFunction("values(map): list", (map) => {
|
|
33
|
+
if (map instanceof Map)
|
|
34
|
+
return [...map.values()];
|
|
35
|
+
return Object.values(map);
|
|
36
|
+
})
|
|
37
|
+
.registerFunction("sha256(string): string", (s) => handlers.sha256(s))
|
|
38
|
+
.registerFunction("json(dyn): string", (value) => handlers.json(value))
|
|
39
|
+
.registerType("Stream", Stream);
|
|
40
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Walks `value` and emits each templated source segment with its dotted
|
|
2
|
+
* path (e.g. `routes[0].handler.body`) and the engine that owns it.
|
|
3
|
+
*
|
|
4
|
+
* - Untagged strings: every `${{ ... }}` segment is emitted with
|
|
5
|
+
* `engineName = "cel"` (the implicit engine for the legacy interpolation
|
|
6
|
+
* syntax).
|
|
7
|
+
* - Tagged sentinels: emitted once with the sentinel's declared engine.
|
|
8
|
+
* This includes engines that may produce no diagnostics (`literal`) —
|
|
9
|
+
* routing through the registry stays generic so adding a third engine
|
|
10
|
+
* that wants real analysis doesn't require touching the walker.
|
|
11
|
+
* - Compiled values are skipped so a precompiled tree won't be re-walked. */
|
|
12
|
+
export declare function walkCelExpressions(value: unknown, path: string, cb: (source: string, path: string, engineName: string) => void): void;
|
|
13
|
+
//# sourceMappingURL=walk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"walk.d.ts","sourceRoot":"","sources":["../../src/cel/walk.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;8EAU8E;AAC9E,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,OAAO,EACd,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,GAC7D,IAAI,CAwBN"}
|
package/dist/cel/walk.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { isTaggedSentinel } from "../sentinel.js";
|
|
2
|
+
import { TEMPLATE_REGEX } from "./compile.js";
|
|
3
|
+
/** Walks `value` and emits each templated source segment with its dotted
|
|
4
|
+
* path (e.g. `routes[0].handler.body`) and the engine that owns it.
|
|
5
|
+
*
|
|
6
|
+
* - Untagged strings: every `${{ ... }}` segment is emitted with
|
|
7
|
+
* `engineName = "cel"` (the implicit engine for the legacy interpolation
|
|
8
|
+
* syntax).
|
|
9
|
+
* - Tagged sentinels: emitted once with the sentinel's declared engine.
|
|
10
|
+
* This includes engines that may produce no diagnostics (`literal`) —
|
|
11
|
+
* routing through the registry stays generic so adding a third engine
|
|
12
|
+
* that wants real analysis doesn't require touching the walker.
|
|
13
|
+
* - Compiled values are skipped so a precompiled tree won't be re-walked. */
|
|
14
|
+
export function walkCelExpressions(value, path, cb) {
|
|
15
|
+
if (isTaggedSentinel(value)) {
|
|
16
|
+
cb(value.source, path, value.engine);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (typeof value === "string") {
|
|
20
|
+
for (const m of value.matchAll(TEMPLATE_REGEX)) {
|
|
21
|
+
cb(m[1].trim(), path, "cel");
|
|
22
|
+
}
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
value.forEach((v, i) => walkCelExpressions(v, `${path}[${i}]`, cb));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (value !== null &&
|
|
30
|
+
typeof value === "object" &&
|
|
31
|
+
!value.__compiled) {
|
|
32
|
+
for (const [k, v] of Object.entries(value)) {
|
|
33
|
+
walkCelExpressions(v, path ? `${path}.${k}` : k, cb);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|