@proveanything/smartlinks 1.6.7 → 1.7.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/api/attestation.d.ts +22 -0
- package/dist/api/attestation.js +22 -0
- package/dist/api/attestations.d.ts +292 -0
- package/dist/api/attestations.js +405 -0
- package/dist/api/containers.d.ts +236 -0
- package/dist/api/containers.js +316 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +2 -0
- package/dist/api/tags.d.ts +20 -1
- package/dist/api/tags.js +30 -0
- package/dist/docs/API_SUMMARY.md +701 -7
- package/dist/docs/app-manifest.md +430 -0
- package/dist/docs/attestations.md +498 -0
- package/dist/docs/container-tracking.md +437 -0
- package/dist/docs/deep-link-discovery.md +6 -6
- package/dist/docs/executor.md +554 -0
- package/dist/index.d.ts +3 -0
- package/dist/openapi.yaml +3110 -1323
- package/dist/types/appManifest.d.ts +152 -0
- package/dist/types/attestation.d.ts +12 -0
- package/dist/types/attestations.d.ts +237 -0
- package/dist/types/attestations.js +11 -0
- package/dist/types/containers.d.ts +186 -0
- package/dist/types/containers.js +10 -0
- package/dist/types/tags.d.ts +47 -3
- package/docs/API_SUMMARY.md +701 -7
- package/docs/app-manifest.md +430 -0
- package/docs/attestations.md +498 -0
- package/docs/container-tracking.md +437 -0
- package/docs/deep-link-discovery.md +6 -6
- package/docs/executor.md +554 -0
- package/openapi.yaml +3110 -1323
- package/package.json +1 -1
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
# SmartLinks Executor Model
|
|
2
|
+
|
|
3
|
+
> **SDK minimum:** `@proveanything/smartlinks@1.4.1`
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What Is an Executor?
|
|
8
|
+
|
|
9
|
+
An executor is a **standalone JavaScript library** (`executor.umd.js` / `executor.es.js`) that a SmartLinks app ships alongside its widget and container bundles. It exposes programmatic functions that external systems — AI orchestrators, the Hub server, setup wizards — can call **without rendering the app's UI**.
|
|
10
|
+
|
|
11
|
+
Every app can optionally ship an executor. The executor pattern solves three problems that iframe-based apps can't address on their own:
|
|
12
|
+
|
|
13
|
+
| Problem | Executor Solution |
|
|
14
|
+
|---------|-------------------|
|
|
15
|
+
| AI orchestrators need to configure apps programmatically | Executor exposes typed mutation functions |
|
|
16
|
+
| Search engines can't execute JavaScript in iframes | `getSEO()` returns metadata the server injects into `<head>` |
|
|
17
|
+
| AI crawlers need readable content, not interactive widgets | `getLLMContent()` returns structured markdown sections |
|
|
18
|
+
|
|
19
|
+
### What an Executor Is Not
|
|
20
|
+
|
|
21
|
+
- **Not a REST API** — it's a JS bundle loaded and called in-process (Node.js or browser)
|
|
22
|
+
- **Not required** — apps work fine without one; the executor adds programmatic capabilities
|
|
23
|
+
- **Not the app itself** — it shares types and logic with the app but produces a separate bundle
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Manifest Declaration
|
|
28
|
+
|
|
29
|
+
Every executor is declared in `app.manifest.json` so the platform can discover and load it:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"meta": {
|
|
34
|
+
"name": "My App",
|
|
35
|
+
"appId": "my-app",
|
|
36
|
+
"version": "1.0.0",
|
|
37
|
+
"seo": { "priority": 10 }
|
|
38
|
+
},
|
|
39
|
+
"executor": {
|
|
40
|
+
"files": {
|
|
41
|
+
"js": { "umd": "dist/executor.umd.js", "esm": "dist/executor.es.js" }
|
|
42
|
+
},
|
|
43
|
+
"factory": "createMyAppExecutor",
|
|
44
|
+
"exports": ["createMyAppExecutor", "getSEO", "getLLMContent"],
|
|
45
|
+
"description": "Programmatic configuration and SEO API for My App."
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
| Field | Purpose |
|
|
51
|
+
|-------|---------|
|
|
52
|
+
| `files.js.umd` | UMD bundle path (loaded via `<script>` tag — available as `window.MyAppExecutor`) |
|
|
53
|
+
| `files.js.esm` | ESM bundle path (for `import()` in modern environments) |
|
|
54
|
+
| `factory` | Name of the factory function that creates an executor instance |
|
|
55
|
+
| `exports` | All named exports — tells consumers what's available without loading the bundle |
|
|
56
|
+
| `description` | Human-readable summary for AI orchestrators and admin UIs |
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Loading an Executor
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// ESM (modern bundlers, Deno, Node 18+)
|
|
64
|
+
const { createMyAppExecutor } = await import('https://my-app.smartlinks.app/dist/executor.es.js');
|
|
65
|
+
|
|
66
|
+
// UMD (script tag, legacy environments)
|
|
67
|
+
// After loading executor.umd.js:
|
|
68
|
+
const { createMyAppExecutor } = window.MyAppExecutor;
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Initialising
|
|
72
|
+
|
|
73
|
+
Every executor factory receives a standard `ExecutorContext`:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import * as SL from '@proveanything/smartlinks';
|
|
77
|
+
|
|
78
|
+
const executor = createMyAppExecutor({
|
|
79
|
+
collectionId: 'col_abc123',
|
|
80
|
+
appId: 'my-app',
|
|
81
|
+
SL, // Pre-initialised SmartLinks SDK
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The SDK reference is passed in rather than imported because executors run in contexts where the SDK may already be loaded (the Hub server, an AI orchestrator's runtime, a parent app). This avoids duplicate SDK instances and ensures the caller controls authentication.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Core Pattern: The Mutate Pipeline
|
|
90
|
+
|
|
91
|
+
The recommended internal architecture for executor mutations is a `mutate()` helper:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
async function mutate(fn: (config: MyConfig) => MyConfig): Promise<MyConfig> {
|
|
95
|
+
const current = await getConfig(); // Read current state + merge with defaults
|
|
96
|
+
const updated = fn(current); // Pure transformation (no side effects)
|
|
97
|
+
await saveConfig(updated); // Validate + write via SL API
|
|
98
|
+
return updated;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
This gives you:
|
|
103
|
+
- **Read-before-write** — always operates on the latest config
|
|
104
|
+
- **Pure transformations** — mutation functions are testable without API calls
|
|
105
|
+
- **Validation gate** — invalid configs are rejected before saving
|
|
106
|
+
- **`admin: true` encapsulated** — callers don't need to remember the flag
|
|
107
|
+
|
|
108
|
+
All transformation functions should be pure: `(Config) → Config`. Side effects (SL API calls) are isolated in the factory's `getConfig`/`saveConfig` wrappers.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## SEO Contract
|
|
113
|
+
|
|
114
|
+
The SEO contract allows the Hub server (or any SSR layer) to generate metadata for search engines and social previews **without rendering the app's UI**.
|
|
115
|
+
|
|
116
|
+
### Why This Exists
|
|
117
|
+
|
|
118
|
+
SmartLinks apps run in iframes — search engines and social crawlers can't see their content. The server needs a way to ask each app: "What title, description, and structured data should this page have?"
|
|
119
|
+
|
|
120
|
+
Rather than HTTP endpoints (which add latency and infrastructure), the server loads the app's executor bundle in Node.js and calls `getSEO()` directly. This is fast (~1ms), requires no network round-trips, and uses pre-fetched entity data.
|
|
121
|
+
|
|
122
|
+
### How It Works
|
|
123
|
+
|
|
124
|
+
```text
|
|
125
|
+
1. Server receives a page request
|
|
126
|
+
2. Loads executor.umd.js for each app on the page (Node.js)
|
|
127
|
+
3. Calls getSEO() with pre-fetched collection / product / proof data
|
|
128
|
+
4. Merges results from all apps using priority-based resolution
|
|
129
|
+
5. Injects into <head>: <title>, OG tags, JSON-LD, canonical URL
|
|
130
|
+
6. Falls back to defaults if any app doesn't respond within 200ms
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Input
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
interface SEOInput {
|
|
137
|
+
collectionId: string;
|
|
138
|
+
appId: string;
|
|
139
|
+
productId?: string;
|
|
140
|
+
proofId?: string;
|
|
141
|
+
/** Pre-authenticated SmartLinks SDK for any extra lookups */
|
|
142
|
+
SL: typeof import('@proveanything/smartlinks');
|
|
143
|
+
/** Pre-fetched objects — use these directly, avoid additional SL calls */
|
|
144
|
+
collection?: Record<string, any>;
|
|
145
|
+
product?: Record<string, any>;
|
|
146
|
+
proof?: Record<string, any>;
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Key principle:** The server pre-fetches collection, product, and proof data and passes them in. Your `getSEO()` should use these directly — **avoid making additional SL API calls** unless you need app-specific data (e.g., your app's config). This keeps execution well within the 200ms budget.
|
|
151
|
+
|
|
152
|
+
### Output
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
interface SEOResult {
|
|
156
|
+
/** Page title — singular field, highest-priority app wins */
|
|
157
|
+
title?: string;
|
|
158
|
+
/** Meta description — singular */
|
|
159
|
+
description?: string;
|
|
160
|
+
/** Open Graph image URL — singular */
|
|
161
|
+
ogImage?: string;
|
|
162
|
+
/** JSON-LD structured data — additive, merged from all apps */
|
|
163
|
+
jsonLd?: Record<string, any> | Record<string, any>[];
|
|
164
|
+
/** Plain text summary for AI crawlers — additive, concatenated */
|
|
165
|
+
contentSummary?: string;
|
|
166
|
+
/** Topic tags — additive, merged and deduplicated */
|
|
167
|
+
topics?: string[];
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Multi-App Priority Merging
|
|
172
|
+
|
|
173
|
+
When multiple apps appear on the same page, the server merges their SEO output:
|
|
174
|
+
|
|
175
|
+
| Field | Strategy | Rule |
|
|
176
|
+
|-------|----------|------|
|
|
177
|
+
| `title` | **Singular** | Highest `meta.seo.priority` wins. Equal priority: first app in page order. |
|
|
178
|
+
| `description` | **Singular** | Same as title |
|
|
179
|
+
| `ogImage` | **Singular** | Same as title |
|
|
180
|
+
| `jsonLd` | **Additive** | All apps' JSON-LD arrays are concatenated |
|
|
181
|
+
| `contentSummary` | **Additive** | Concatenated from all apps |
|
|
182
|
+
| `topics` | **Additive** | Merged and deduplicated from all apps |
|
|
183
|
+
|
|
184
|
+
Set your app's priority in `app.manifest.json` (default is `0`, higher wins):
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
"meta": {
|
|
188
|
+
"seo": { "priority": 10 }
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Implementation Example
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
// src/executor/seo.ts — Warranty app example
|
|
196
|
+
export function getSEO(input: SEOInput): SEOResult {
|
|
197
|
+
const { product, collection } = input;
|
|
198
|
+
const brandName = collection?.name ?? 'Product';
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
title: product
|
|
202
|
+
? `Warranty — ${product.name}`
|
|
203
|
+
: 'Warranty Registration',
|
|
204
|
+
description: `Register your ${product?.name ?? 'product'} warranty with ${brandName}.`,
|
|
205
|
+
jsonLd: {
|
|
206
|
+
'@context': 'https://schema.org',
|
|
207
|
+
'@type': 'WebPage',
|
|
208
|
+
name: `Warranty — ${product?.name ?? brandName}`,
|
|
209
|
+
description: `Warranty registration for ${product?.name ?? 'products'}.`,
|
|
210
|
+
},
|
|
211
|
+
topics: ['Warranty', 'Product Registration'],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Declaring SEO in the Manifest
|
|
217
|
+
|
|
218
|
+
```json
|
|
219
|
+
"meta": {
|
|
220
|
+
"seo": {
|
|
221
|
+
"strategy": "executor",
|
|
222
|
+
"priority": 10,
|
|
223
|
+
"contract": {
|
|
224
|
+
"function": "getSEO",
|
|
225
|
+
"input": ["collectionId", "appId", "productId", "proofId", "collection", "product", "proof", "SL"],
|
|
226
|
+
"timeout": 200,
|
|
227
|
+
"responseShape": {
|
|
228
|
+
"title": "string",
|
|
229
|
+
"description": "string",
|
|
230
|
+
"ogImage": "string (URL)",
|
|
231
|
+
"jsonLd": "object | object[]",
|
|
232
|
+
"contentSummary": "string",
|
|
233
|
+
"topics": "string[]"
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## LLM Content Contract
|
|
243
|
+
|
|
244
|
+
The `getLLMContent()` function generates structured markdown sections for AI crawler consumption. Unlike `getSEO()` (which produces HTML meta tags), this produces **readable content** — rich text that an LLM can ingest to understand what the page is about.
|
|
245
|
+
|
|
246
|
+
### Why This Exists
|
|
247
|
+
|
|
248
|
+
AI crawlers (GPTBot, ClaudeBot, PerplexityBot, etc.) want to understand page content, not just metadata. Since microapp content lives inside iframes that crawlers can't execute, each app provides a way to say: "Here's my content as readable markdown."
|
|
249
|
+
|
|
250
|
+
### Tiered AI Crawler Strategy
|
|
251
|
+
|
|
252
|
+
| Tier | Mechanism | Purpose |
|
|
253
|
+
|------|-----------|---------|
|
|
254
|
+
| **Discovery** | `/llms.txt` at site root | Table of contents — lists pages, products, links |
|
|
255
|
+
| **Per-route rendering** | Bot User-Agent detection | Rich semantic HTML injected before React root |
|
|
256
|
+
| **App-level sections** | `getLLMContent()` on each app's executor | Structured markdown sections merged by server |
|
|
257
|
+
|
|
258
|
+
AI crawlers naturally follow links from `/llms.txt` to individual pages. When they hit a page, the server detects the bot User-Agent, calls each embedded app's `getLLMContent()`, and injects the combined content as HTML that the crawler can read.
|
|
259
|
+
|
|
260
|
+
**Detected bots:**
|
|
261
|
+
```
|
|
262
|
+
GPTBot, ChatGPT-User, PerplexityBot, ClaudeBot, Claude-Web,
|
|
263
|
+
Google-Extended, CCBot, Amazonbot, anthropic-ai, Bytespider, cohere-ai
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Input
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
interface LLMContentInput {
|
|
270
|
+
collectionId: string;
|
|
271
|
+
appId: string;
|
|
272
|
+
productId?: string;
|
|
273
|
+
proofId?: string;
|
|
274
|
+
SL: typeof import('@proveanything/smartlinks');
|
|
275
|
+
collection?: Record<string, any>;
|
|
276
|
+
product?: Record<string, any>;
|
|
277
|
+
proof?: Record<string, any>;
|
|
278
|
+
/** Which page slug is being rendered */
|
|
279
|
+
pageSlug?: string;
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Output
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
interface LLMContentResult {
|
|
287
|
+
sections: LLMContentSection[];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
interface LLMContentSection {
|
|
291
|
+
/** Section heading (e.g., "FAQ", "Warranty", "Product Manual") */
|
|
292
|
+
heading: string;
|
|
293
|
+
/** Markdown content */
|
|
294
|
+
content: string;
|
|
295
|
+
/** Sort order — lower numbers appear first (default: 100) */
|
|
296
|
+
order?: number;
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Implementation Example
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// src/executor/llm-content.ts — FAQ app example
|
|
304
|
+
export async function getLLMContent(input: LLMContentInput): Promise<LLMContentResult> {
|
|
305
|
+
const { collectionId, appId, product, SL } = input;
|
|
306
|
+
|
|
307
|
+
const config = await SL.appConfiguration.getConfig({ collectionId, appId });
|
|
308
|
+
const faqs = config?.faqs ?? [];
|
|
309
|
+
|
|
310
|
+
if (faqs.length === 0) return { sections: [] };
|
|
311
|
+
|
|
312
|
+
const content = faqs
|
|
313
|
+
.map((faq: any) => `### ${faq.question}\n\n${faq.answer}`)
|
|
314
|
+
.join('\n\n');
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
sections: [
|
|
318
|
+
{
|
|
319
|
+
heading: product
|
|
320
|
+
? `${product.name} — Frequently Asked Questions`
|
|
321
|
+
: 'Frequently Asked Questions',
|
|
322
|
+
content,
|
|
323
|
+
order: 50,
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Declaring LLM Content in the Manifest
|
|
331
|
+
|
|
332
|
+
```json
|
|
333
|
+
"executor": {
|
|
334
|
+
"exports": ["createExecutor", "getSEO", "getLLMContent"],
|
|
335
|
+
"llmContent": {
|
|
336
|
+
"function": "getLLMContent",
|
|
337
|
+
"timeout": 500,
|
|
338
|
+
"responseShape": {
|
|
339
|
+
"sections": "Array<{ heading: string, content: string, order?: number }>"
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Note the longer timeout (500ms vs 200ms for SEO) — LLM content can involve fetching app-specific data and is only served to bots, so slightly higher latency is acceptable.
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Building an Executor
|
|
350
|
+
|
|
351
|
+
### File Structure
|
|
352
|
+
|
|
353
|
+
```
|
|
354
|
+
src/executor/
|
|
355
|
+
├── index.ts # Factory function + re-exports
|
|
356
|
+
├── types.ts # Public TypeScript types
|
|
357
|
+
├── seo.ts # getSEO implementation
|
|
358
|
+
├── llm-content.ts # getLLMContent implementation
|
|
359
|
+
└── ... # App-specific mutation modules
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Vite Config
|
|
363
|
+
|
|
364
|
+
Create `vite.config.executor.ts`:
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
import { defineConfig } from 'vite';
|
|
368
|
+
import path from 'path';
|
|
369
|
+
|
|
370
|
+
const ENABLE = process.env.VITE_ENABLE_EXECUTOR !== 'false';
|
|
371
|
+
|
|
372
|
+
export default defineConfig({
|
|
373
|
+
resolve: {
|
|
374
|
+
alias: { '@': path.resolve(__dirname, './src') },
|
|
375
|
+
},
|
|
376
|
+
build: {
|
|
377
|
+
outDir: 'dist',
|
|
378
|
+
emptyOutDir: false, // Append to existing dist/ — run after main builds
|
|
379
|
+
lib: {
|
|
380
|
+
entry: ENABLE
|
|
381
|
+
? path.resolve(__dirname, 'src/executor/index.ts')
|
|
382
|
+
: path.resolve(__dirname, 'src/executor-stub.ts'),
|
|
383
|
+
name: 'MyAppExecutor', // window.MyAppExecutor for UMD
|
|
384
|
+
formats: ['umd', 'es'],
|
|
385
|
+
fileName: (format) => `executor.${format}.js`,
|
|
386
|
+
},
|
|
387
|
+
rollupOptions: {
|
|
388
|
+
external: ['@proveanything/smartlinks'],
|
|
389
|
+
output: {
|
|
390
|
+
globals: { '@proveanything/smartlinks': 'SL' },
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**Key points:**
|
|
398
|
+
- `emptyOutDir: false` — the executor build appends to `dist/` after the main app builds
|
|
399
|
+
- Only `@proveanything/smartlinks` is externalised — all other dependencies are bundled
|
|
400
|
+
- `VITE_ENABLE_EXECUTOR=false` produces a stub, allowing CI to skip executor builds when not yet implemented
|
|
401
|
+
|
|
402
|
+
### Build Script
|
|
403
|
+
|
|
404
|
+
```json
|
|
405
|
+
{
|
|
406
|
+
"scripts": {
|
|
407
|
+
"build": "vite build && vite build --config vite.config.widget.ts && vite build --config vite.config.container.ts && vite build --config vite.config.executor.ts && node scripts/hash-bundles.mjs",
|
|
408
|
+
"build:executor": "vite build --config vite.config.executor.ts"
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Stub File
|
|
414
|
+
|
|
415
|
+
When the executor build is disabled, export an empty object:
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
// src/executor-stub.ts
|
|
419
|
+
export {};
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## Minimal Executor (SEO + LLM only)
|
|
425
|
+
|
|
426
|
+
Not every app needs full programmatic configuration. If you just want SEO and LLM support, the executor can be very small:
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
// src/executor/index.ts
|
|
430
|
+
import type { SEOInput, SEOResult, LLMContentInput, LLMContentResult } from './types';
|
|
431
|
+
|
|
432
|
+
export function getSEO(input: SEOInput): SEOResult {
|
|
433
|
+
const { product } = input;
|
|
434
|
+
return {
|
|
435
|
+
title: product ? `My Feature — ${product.name}` : 'My Feature',
|
|
436
|
+
description: 'A useful feature for your products.',
|
|
437
|
+
topics: ['My Feature'],
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export async function getLLMContent(input: LLMContentInput): Promise<LLMContentResult> {
|
|
442
|
+
return { sections: [] };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export type { SEOInput, SEOResult, LLMContentInput, LLMContentResult };
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
The manifest declares what's exported, and the server only calls what's available.
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## Full Executor (With Configuration API)
|
|
453
|
+
|
|
454
|
+
For apps that want AI-driven setup and programmatic configuration, add a factory function with mutation methods:
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
// src/executor/index.ts
|
|
458
|
+
import type { ExecutorContext, SEOInput, SEOResult, LLMContentInput, LLMContentResult } from './types';
|
|
459
|
+
|
|
460
|
+
export function createMyAppExecutor(ctx: ExecutorContext) {
|
|
461
|
+
const { collectionId, appId, SL } = ctx;
|
|
462
|
+
|
|
463
|
+
async function getConfig() {
|
|
464
|
+
try {
|
|
465
|
+
return await SL.appConfiguration.getConfig({ collectionId, appId, admin: true });
|
|
466
|
+
} catch {
|
|
467
|
+
return DEFAULT_CONFIG;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function saveConfig(config: MyConfig) {
|
|
472
|
+
// Validate with Zod or similar, then save
|
|
473
|
+
await SL.appConfiguration.setConfig({ collectionId, appId, config, admin: true });
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return {
|
|
477
|
+
getConfig,
|
|
478
|
+
saveConfig,
|
|
479
|
+
|
|
480
|
+
/** Update the welcome message shown to first-time visitors */
|
|
481
|
+
async setWelcomeMessage(message: string) {
|
|
482
|
+
const config = await getConfig();
|
|
483
|
+
config.welcomeMessage = message;
|
|
484
|
+
await saveConfig(config);
|
|
485
|
+
return config;
|
|
486
|
+
},
|
|
487
|
+
|
|
488
|
+
async getSEO(input: SEOInput): Promise<SEOResult> { /* ... */ },
|
|
489
|
+
async getLLMContent(input: LLMContentInput): Promise<LLMContentResult> { /* ... */ },
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Returns a human-readable summary of current config.
|
|
493
|
+
* Recommended for any app with configuration — enables conversational AI setup flows.
|
|
494
|
+
*/
|
|
495
|
+
async describeCurrentConfig(): Promise<string> {
|
|
496
|
+
const config = await getConfig();
|
|
497
|
+
return `Welcome message: "${config.welcomeMessage}"`;
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
The `describeCurrentConfig()` method is strongly recommended — it gives AI orchestrators situational awareness of the app's current state, enabling natural-language setup flows ("What is the app currently configured to show?").
|
|
504
|
+
|
|
505
|
+
---
|
|
506
|
+
|
|
507
|
+
## TypeScript Types
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
import type {
|
|
511
|
+
ExecutorContext, // { collectionId, appId, SL }
|
|
512
|
+
SEOInput, // getSEO() argument
|
|
513
|
+
SEOResult, // getSEO() return value
|
|
514
|
+
LLMContentInput, // getLLMContent() argument
|
|
515
|
+
LLMContentResult, // getLLMContent() return value
|
|
516
|
+
LLMContentSection, // { heading, content, order? }
|
|
517
|
+
AppManifestExecutor, // executor block in app.manifest.json
|
|
518
|
+
} from '@proveanything/smartlinks';
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
All types live in `src/types/appManifest.ts`.
|
|
522
|
+
|
|
523
|
+
---
|
|
524
|
+
|
|
525
|
+
## Design Principles
|
|
526
|
+
|
|
527
|
+
| Principle | Detail |
|
|
528
|
+
|-----------|--------|
|
|
529
|
+
| **Pure transformations** | Mutation functions are `(Config) → Config` with no side effects |
|
|
530
|
+
| **Pre-fetched entities** | `getSEO()` and `getLLMContent()` receive data — they don't fetch it themselves |
|
|
531
|
+
| **200ms SEO budget** | The server enforces a hard timeout. Keep `getSEO()` synchronous if possible |
|
|
532
|
+
| **500ms LLM budget** | `getLLMContent()` may fetch app config, but should remain fast |
|
|
533
|
+
| **`admin: true` encapsulated** | Write operations always include the flag — callers never need to remember |
|
|
534
|
+
| **Validate before save** | Use Zod or similar to reject invalid configs before writing |
|
|
535
|
+
| **Idempotent mutations** | Repeated calls with the same input produce the same result |
|
|
536
|
+
| **Externalise only the SDK** | Bundle everything else — keep the dependency contract minimal |
|
|
537
|
+
|
|
538
|
+
---
|
|
539
|
+
|
|
540
|
+
## Reference: Hub Executor
|
|
541
|
+
|
|
542
|
+
The Product Hub app provides the most comprehensive executor implementation as a reference. Its executor includes:
|
|
543
|
+
|
|
544
|
+
- **Theme & branding** — `applyBranding()`, `setFonts()`, `setColors()` with hex→HSL conversion
|
|
545
|
+
- **Pages & content** — `addPage()` (idempotent by slug), `removePage()`, `addBlockToPage()`, `generateHomepage()`
|
|
546
|
+
- **Navigation** — `addNavItem()`, `setNavItems()`, `syncNavToPages()`
|
|
547
|
+
- **Header & footer** — `setHeader()`, `setFooter()`, `setFooterFromCrawl()`
|
|
548
|
+
- **Compliance** — `setCompliance()` (cookie consent, analytics, legal)
|
|
549
|
+
- **SEO** — `getSEO()` with layered metadata (brand → page → product)
|
|
550
|
+
- **LLM content** — `getLLMContent()` producing overview, pages, product, and contact sections
|
|
551
|
+
- **Bulk setup** — `setupFromWebsite()` orchestrating all of the above from crawled data
|
|
552
|
+
- **Describe** — `describeCurrentConfig()` for AI situational awareness
|
|
553
|
+
|
|
554
|
+
See `public/ai-guide.md` in the Hub app for complete API documentation.
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export type { LoginResponse, VerifyTokenResponse, AccountInfoResponse, } from ".
|
|
|
10
10
|
export type { UserAccountRegistrationRequest, } from "./types/auth";
|
|
11
11
|
export type { CommunicationEvent, CommsQueryByUser, CommsRecipientIdsQuery, CommsRecipientsWithoutActionQuery, CommsRecipientsWithActionQuery, RecipientId, RecipientWithOutcome, LogCommunicationEventBody, LogBulkCommunicationEventsBody, AppendResult, AppendBulkResult, CommsSettings, TopicConfig, CommsSettingsGetResponse, CommsSettingsPatchBody, CommsPublicTopicsResponse, UnsubscribeQuery, UnsubscribeResponse, CommsConsentUpsertRequest, CommsPreferencesUpsertRequest, CommsSubscribeRequest, CommsSubscribeResponse, CommsSubscriptionCheckQuery, CommsSubscriptionCheckResponse, CommsListMethodsQuery, CommsListMethodsResponse, RegisterEmailMethodRequest, RegisterSmsMethodRequest, RegisterMethodResponse, SubscriptionsResolveRequest, SubscriptionsResolveResponse, } from "./types/comms";
|
|
12
12
|
export type { AttestationResponse, AttestationCreateRequest, AttestationUpdateRequest, } from "./types/attestation";
|
|
13
|
+
export type { Attestation, LatestAttestation, AttestationSummaryBucket, ChainVerifyResult, CreateAttestationInput, AttestationSubjectType, AttestationVisibility, AttestationAudience, AttestationGroupBy, ListAttestationsParams, AttestationSummaryParams, AttestationLatestParams, AttestationVerifyParams, AttestationTreeSummaryParams, AttestationTreeLatestParams, ListAttestationsResponse, PublicListAttestationsResponse, AttestationSummaryResponse, PublicAttestationSummaryResponse, AttestationLatestResponse, PublicAttestationLatestResponse, AttestationTreeSummaryResponse, PublicAttestationTreeSummaryResponse, AttestationTreeLatestResponse, PublicAttestationTreeLatestResponse, } from "./types/attestations";
|
|
14
|
+
export type { Container, ContainerItem, ContainerStatus, ContainerItemType, CreateContainerInput, UpdateContainerInput, AddContainerItemsInput, RemoveContainerItemsInput, ListContainersParams, GetContainerParams, ListContainerItemsParams, FindContainersForItemParams, ListContainersResponse, PublicListContainersResponse, FindContainersForItemResponse, ContainerItemsResponse, AddContainerItemsResponse, RemoveContainerItemsResponse, } from "./types/containers";
|
|
13
15
|
export type { BatchResponse, BatchCreateRequest, BatchUpdateRequest, } from "./types/batch";
|
|
14
16
|
export type { VariantResponse, VariantCreateRequest, VariantUpdateRequest, } from "./types/variant";
|
|
15
17
|
export type { BroadcastSendRequest } from "./types/broadcasts";
|
|
@@ -18,3 +20,4 @@ export type { ProductCreateRequest, ProductUpdateRequest, Product, } from "./typ
|
|
|
18
20
|
export type { Collection, CollectionResponse, CollectionCreateRequest, CollectionUpdateRequest, } from "./types/collection";
|
|
19
21
|
export type { Proof, ProofResponse, ProofCreateRequest, ProofUpdateRequest, ProofClaimRequest, } from "./types/proof";
|
|
20
22
|
export type { QrShortCodeLookupResponse, } from "./types/qr";
|
|
23
|
+
export type { ReverseTagLookupParams, ReverseTagLookupResponse, } from "./types/tags";
|