@proveanything/smartlinks 2.0.57 → 2.0.60
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/integrations.d.ts +17 -2
- package/dist/api/integrations.js +20 -1
- package/dist/docs/API_SUMMARY.md +76 -11
- package/dist/docs/app-manifest.md +1 -1
- package/dist/docs/connectors.md +72 -30
- package/dist/docs/integrations.md +8 -0
- package/dist/docs/site-seo.md +69 -68
- package/dist/docs/websites.md +91 -0
- package/dist/openapi.yaml +108 -12
- package/dist/site.d.ts +32 -4
- package/dist/site.js +48 -5
- package/dist/types/appManifest.d.ts +55 -10
- package/dist/types/integrations.d.ts +31 -3
- package/docs/API_SUMMARY.md +76 -11
- package/docs/app-manifest.md +1 -1
- package/docs/connectors.md +72 -30
- package/docs/integrations.md +8 -0
- package/docs/site-seo.md +69 -68
- package/docs/websites.md +91 -0
- package/openapi.yaml +108 -12
- package/package.json +1 -1
package/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.60 | Generated: 2026-10-13T16:25:55.817Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -65,7 +65,7 @@ For detailed guides on specific features:
|
|
|
65
65
|
- **[Analytics Metadata Conventions](analytics-metadata-conventions.md)** - Standard recommended keys and conventions for analytics metadata fields
|
|
66
66
|
- **[Loyalty: Points, Members & Earning Rules](loyalty.md)** - Loyalty schemes, automatic point earning via interaction rules, member balances, transaction history, and manual adjustments
|
|
67
67
|
- **[Integrations](integrations.md)** - Inbound/outbound integration flows + the sealed-secret store; triggers (manual/event/schedule), field mappings, and the Syndigo/Event Hub outbound path
|
|
68
|
-
- **[Connectors](connectors.md)** - Connector apps: a manifest `connector` block + server functions (testConnection/describeTarget/sample/
|
|
68
|
+
- **[Connectors](connectors.md)** - Connector apps (adapters for one external provider): a manifest `connector` block + server functions (testConnection/describeTarget/sample/fetch/shape/serialize/send); core does the mapping
|
|
69
69
|
- **[AI Tools & Skills](ai-tools-and-skills.md)** - The AI capability catalog: skills apps invoke by name (e.g. research.brand), the tools the AI reaches for (web fetch/screenshot/brand assets/image gen), the agent loop, and how apps discover them
|
|
70
70
|
- **[Deep Link Discovery](deep-link-discovery.md)** - Registering and discovering navigable app states for portal menus and AI orchestration
|
|
71
71
|
- **[AI Guide Template](ai-guide-template.md)** - A sample for an app on how to build an AI setup guide
|
|
@@ -2908,12 +2908,15 @@ interface AppConnectorDeclaration {
|
|
|
2908
2908
|
testConnection?: string
|
|
2909
2909
|
describeTarget?: string
|
|
2910
2910
|
sample?: string
|
|
2911
|
-
|
|
2911
|
+
shape?: string
|
|
2912
|
+
serialize?: string
|
|
2912
2913
|
send?: string
|
|
2913
2914
|
fetch?: string
|
|
2914
2915
|
}
|
|
2915
2916
|
panels?: ConnectorPanel[]
|
|
2916
2917
|
aiGuide?: string
|
|
2918
|
+
recordId?: string
|
|
2919
|
+
limits?: { requestsPerSecond?: number }
|
|
2917
2920
|
}
|
|
2918
2921
|
```
|
|
2919
2922
|
|
|
@@ -2963,18 +2966,34 @@ interface ConnectorRecords {
|
|
|
2963
2966
|
}
|
|
2964
2967
|
```
|
|
2965
2968
|
|
|
2966
|
-
**
|
|
2969
|
+
**ConnectorShapeInput** (interface)
|
|
2967
2970
|
```typescript
|
|
2968
|
-
interface
|
|
2971
|
+
interface ConnectorShapeInput {
|
|
2969
2972
|
installation: ConnectorInstallation
|
|
2970
2973
|
records: Array<Record<string, unknown>>
|
|
2971
|
-
mapping: FieldMapping[]
|
|
2972
2974
|
}
|
|
2973
2975
|
```
|
|
2974
2976
|
|
|
2975
|
-
**
|
|
2977
|
+
**ConnectorShapeResult** (interface)
|
|
2976
2978
|
```typescript
|
|
2977
|
-
interface
|
|
2979
|
+
interface ConnectorShapeResult {
|
|
2980
|
+
documents: Array<Record<string, unknown> | null>
|
|
2981
|
+
skipped?: Array<{ index: number; reason: string }>
|
|
2982
|
+
}
|
|
2983
|
+
```
|
|
2984
|
+
|
|
2985
|
+
**ConnectorSerializeInput** (interface)
|
|
2986
|
+
```typescript
|
|
2987
|
+
interface ConnectorSerializeInput {
|
|
2988
|
+
installation: ConnectorInstallation
|
|
2989
|
+
documents: Array<Record<string, unknown>>
|
|
2990
|
+
eventType?: string
|
|
2991
|
+
}
|
|
2992
|
+
```
|
|
2993
|
+
|
|
2994
|
+
**ConnectorSerializeResult** (interface)
|
|
2995
|
+
```typescript
|
|
2996
|
+
interface ConnectorSerializeResult {
|
|
2978
2997
|
payloads: Array<unknown | null>
|
|
2979
2998
|
skipped?: Array<{ index: number; reason: string }>
|
|
2980
2999
|
}
|
|
@@ -2991,7 +3010,15 @@ interface ConnectorSendInput {
|
|
|
2991
3010
|
**ConnectorSendResult** (interface)
|
|
2992
3011
|
```typescript
|
|
2993
3012
|
interface ConnectorSendResult {
|
|
2994
|
-
results: Array<{
|
|
3013
|
+
results: Array<{
|
|
3014
|
+
index: number
|
|
3015
|
+
ok: boolean
|
|
3016
|
+
status?: number
|
|
3017
|
+
error?: string
|
|
3018
|
+
externalId?: string
|
|
3019
|
+
retryable?: boolean
|
|
3020
|
+
retryAfterSeconds?: number
|
|
3021
|
+
}>
|
|
2995
3022
|
}
|
|
2996
3023
|
```
|
|
2997
3024
|
|
|
@@ -7091,6 +7118,32 @@ interface IntegrationFlow {
|
|
|
7091
7118
|
lastPollAt?: string | null
|
|
7092
7119
|
lastCursor?: string | null
|
|
7093
7120
|
totalSynced?: number | null
|
|
7121
|
+
circuitOpenedAt?: string | null
|
|
7122
|
+
circuitProbeAt?: string | null
|
|
7123
|
+
circuitReason?: string | null
|
|
7124
|
+
}
|
|
7125
|
+
```
|
|
7126
|
+
|
|
7127
|
+
**FailedRecord** (interface)
|
|
7128
|
+
```typescript
|
|
7129
|
+
interface FailedRecord {
|
|
7130
|
+
id: string
|
|
7131
|
+
entityId?: string | null
|
|
7132
|
+
externalId?: string
|
|
7133
|
+
status: 'failed' | 'dead'
|
|
7134
|
+
attempts: number
|
|
7135
|
+
nextAttemptAt?: string | null
|
|
7136
|
+
lastError?: string | null
|
|
7137
|
+
updatedAt?: string
|
|
7138
|
+
}
|
|
7139
|
+
```
|
|
7140
|
+
|
|
7141
|
+
**FailedRecordList** (interface)
|
|
7142
|
+
```typescript
|
|
7143
|
+
interface FailedRecordList {
|
|
7144
|
+
direction: FlowDirection
|
|
7145
|
+
items: FailedRecord[]
|
|
7146
|
+
nextCursor: string | null
|
|
7094
7147
|
}
|
|
7095
7148
|
```
|
|
7096
7149
|
|
|
@@ -7144,6 +7197,7 @@ interface RunFlowSummary {
|
|
|
7144
7197
|
created?: number
|
|
7145
7198
|
updated?: number
|
|
7146
7199
|
skipped?: number
|
|
7200
|
+
held?: number
|
|
7147
7201
|
status: RunStatus
|
|
7148
7202
|
runId?: string
|
|
7149
7203
|
}
|
|
@@ -7374,7 +7428,7 @@ interface RecordTypesResponse {
|
|
|
7374
7428
|
|
|
7375
7429
|
**UpdateFlowInput** = `Partial<Omit<CreateFlowInput, 'direction'>> & {`
|
|
7376
7430
|
|
|
7377
|
-
**ConnectorConsoleStep** = `'testConnection' | 'describeTarget' | 'sample' | '
|
|
7431
|
+
**ConnectorConsoleStep** = `'testConnection' | 'describeTarget' | 'sample' | 'shape' | 'serialize'`
|
|
7378
7432
|
|
|
7379
7433
|
**RunTrigger** = `'manual' | 'event' | 'schedule' | 'test'`
|
|
7380
7434
|
|
|
@@ -11687,6 +11741,17 @@ Is enhanced logging active for a connection? GET /integrations/logging/:connecti
|
|
|
11687
11741
|
**setLogging**(collectionId: string, connectionId: string, input: SetLoggingInput = {}) → `Promise<`
|
|
11688
11742
|
Enable enhanced logging. POST /integrations/logging/:connectionId (omit ttlMinutes = until cancelled)
|
|
11689
11743
|
|
|
11744
|
+
**listFailed**(collectionId: string,
|
|
11745
|
+
flowId: string,
|
|
11746
|
+
query: { status?: 'failed' | 'dead'; limit?: number; cursor?: string } = {}) → `Promise<FailedRecordList>`
|
|
11747
|
+
A flow's failed / dead records. GET /integrations/flows/:id/failed
|
|
11748
|
+
|
|
11749
|
+
**retryFailed**(collectionId: string, flowId: string, ids?: string[]) → `Promise<`
|
|
11750
|
+
Retry failed / dead records now (all of them when `ids` is omitted). POST /integrations/flows/:id/failed/retry
|
|
11751
|
+
|
|
11752
|
+
**resumeFlow**(collectionId: string, flowId: string) → `Promise<`
|
|
11753
|
+
Resume a flow that paused itself (circuit open): held records go out now. POST /integrations/flows/:id/resume
|
|
11754
|
+
|
|
11690
11755
|
**cancelLogging**(collectionId: string, connectionId: string) → `Promise<`
|
|
11691
11756
|
Cancel enhanced logging now. DELETE /integrations/logging/:connectionId
|
|
11692
11757
|
|
|
@@ -11702,7 +11767,7 @@ Save a connector's non-secret Connect-form values and/or settings. PUT /integrat
|
|
|
11702
11767
|
appId: string,
|
|
11703
11768
|
step: ConnectorConsoleStep,
|
|
11704
11769
|
input: Record<string, unknown> & { flowId?: string } = {}) → `Promise<T>`
|
|
11705
|
-
Run a connector step from the console: `testConnection` → ConnectorTestResult, `describeTarget` → ConnectorTargetSchema, `sample` → ConnectorRecords, `
|
|
11770
|
+
Run a connector step from the console: `testConnection` → ConnectorTestResult, `describeTarget` → ConnectorTargetSchema, `sample` → ConnectorRecords, and dry runs of `shape` ({ records }) → ConnectorShapeResult and `serialize` ({ documents }) → ConnectorSerializeResult. The server adds the installation; pass `flowId` to layer that flow's settings. POST /integrations/connectors/:appId/steps/:step
|
|
11706
11771
|
|
|
11707
11772
|
### interactions
|
|
11708
11773
|
|
package/docs/app-manifest.md
CHANGED
|
@@ -440,7 +440,7 @@ this is just the manifest shape.
|
|
|
440
440
|
|
|
441
441
|
Marks the app as an integration **connector** for one external provider: the integrations console
|
|
442
442
|
lists it and draws its connect form and settings, and the integration engine calls its server
|
|
443
|
-
functions (`testConnection`, `describeTarget`, `sample`, `
|
|
443
|
+
functions (`testConnection`, `describeTarget`, `sample`, `fetch`, `shape`, `serialize`, `send`). It adapts the provider's format; core does each customer's mapping. The full
|
|
444
444
|
contract is in the **[Connectors guide](connectors.md)**.
|
|
445
445
|
|
|
446
446
|
---
|
package/docs/connectors.md
CHANGED
|
@@ -9,16 +9,22 @@ Three layers, each with one job:
|
|
|
9
9
|
|
|
10
10
|
| Layer | Holds |
|
|
11
11
|
|---|---|
|
|
12
|
-
| **Engine** (the API) | [Integration flows](integrations.md), runs and their logs, triggers (events, schedule, Run now), the secret store, the
|
|
12
|
+
| **Engine** (the API) | [Integration flows](integrations.md), runs and their logs, triggers (events, schedule, Run now), the secret store, **the mapping** (each customer's field mappings and rules), generic transports (HTTP, Event Hub). No provider code. |
|
|
13
13
|
| **Integrations console** (a core app) | The one admin screen: installed connectors, connect forms, settings, the mapper with AI assist, flows, runs. |
|
|
14
|
-
| **Connector apps** | One per provider:
|
|
14
|
+
| **Connector apps** | One per provider: an **adapter**. Talks to the provider and converts between its format and a clean, provider-neutral document. |
|
|
15
|
+
|
|
16
|
+
**A connector adapts; core maps.** A connector never holds a customer's mapping. It turns the
|
|
17
|
+
provider's records into provider-neutral documents (`shape`), or provider-ready payloads out of
|
|
18
|
+
documents core has already mapped (`serialize`). Every customer's mapping, routing and rules live in
|
|
19
|
+
core, so they work the same for every provider and can be replayed and traced. This is the line
|
|
20
|
+
SnapLogic (endpoint Snaps vs the Mapper), MuleSoft and Airbyte all draw.
|
|
15
21
|
|
|
16
22
|
A flow that only posts JSON to a webhook or an Event Hub needs no connector; it uses the engine's
|
|
17
23
|
generic transports (see [integrations.md](integrations.md)).
|
|
18
24
|
|
|
19
25
|
**No AI at run time.** The AI helps an admin *author* a mapping in the console (from the connector's
|
|
20
26
|
target schema plus real sample records); a person approves it, and it is saved as data and runs
|
|
21
|
-
deterministically. A connector's `
|
|
27
|
+
deterministically. A connector's `shape` and `serialize` must be deterministic too.
|
|
22
28
|
|
|
23
29
|
---
|
|
24
30
|
|
|
@@ -33,8 +39,10 @@ deterministically. A connector's `transform` must be deterministic too.
|
|
|
33
39
|
"entities": ["product"],
|
|
34
40
|
"connection": {
|
|
35
41
|
"fields": [
|
|
36
|
-
|
|
37
|
-
{ "name": "
|
|
42
|
+
// Optional: SmartLinks' own credentials are the default; a customer may enter theirs.
|
|
43
|
+
{ "name": "appId", "label": "App ID", "secret": "oneworldsync-app-id" },
|
|
44
|
+
{ "name": "secretKey", "label": "Secret key", "secret": "oneworldsync-secret-key" },
|
|
45
|
+
{ "name": "gln", "label": "GLN", "required": true },
|
|
38
46
|
{ "name": "environment", "label": "Environment", "type": "select",
|
|
39
47
|
"options": [ { "value": "preprod", "label": "Pre-production" }, { "value": "production", "label": "Production" } ] }
|
|
40
48
|
]
|
|
@@ -44,12 +52,14 @@ deterministically. A connector's `transform` must be deterministic too.
|
|
|
44
52
|
"properties": { "targetMarket": { "type": "string", "default": "GB" } }
|
|
45
53
|
},
|
|
46
54
|
"presets": [
|
|
47
|
-
{ "id": "import-products", "title": "Import products", "direction": "inbound", "entity": "product"
|
|
55
|
+
{ "id": "import-products", "title": "Import products", "direction": "inbound", "entity": "product",
|
|
56
|
+
"flow": { "config": { "fieldMappings": [ /* a suggested mapping the customer can edit */ ] } } }
|
|
48
57
|
],
|
|
49
58
|
"functions": { // names of this app's server functions
|
|
50
59
|
"testConnection": "testConnection",
|
|
51
60
|
"describeTarget": "describeTarget",
|
|
52
61
|
"sample": "sample",
|
|
62
|
+
"shape": "shape",
|
|
53
63
|
"fetch": "fetch"
|
|
54
64
|
},
|
|
55
65
|
"panels": [ { "id": "explorer", "title": "Explorer", "component": "Explorer" } ],
|
|
@@ -74,12 +84,14 @@ Types: `AppConnectorDeclaration` and friends in `@proveanything/smartlinks` (`sr
|
|
|
74
84
|
| `title` | Provider display name; defaults to the app name. |
|
|
75
85
|
| `directions` | `inbound` (provider → SmartLinks), `outbound` (SmartLinks → provider), or both. |
|
|
76
86
|
| `entities` | SmartLinks entities it maps, e.g. `["product"]`. |
|
|
77
|
-
| `connection.fields[]` | The console's **Connect** form. A field with `secret` is stored as this app's secret of that name (write-only; never returned to the browser). Other fields are saved in the installation's connection settings. |
|
|
87
|
+
| `connection.fields[]` | The console's **Connect** form. A field with `secret` is stored as this app's secret of that name on the collection (write-only; never returned to the browser). Other fields are saved in the installation's connection settings. |
|
|
78
88
|
| `settings` | JSON Schema for installation settings; the console renders it as a form. |
|
|
79
|
-
| `presets[]` | Ready-made flows ("Products → provider")
|
|
89
|
+
| `presets[]` | Ready-made flows ("Products → provider"). A preset's `flow.config.fieldMappings` is a **suggested** mapping: the connector's knowledge of the provider, which the customer accepts or edits in core. |
|
|
80
90
|
| `functions` | Which of this app's server functions implement each step (below). Fill in only what applies. |
|
|
81
91
|
| `panels[]` | Provider-specific screens the console mounts (an API explorer, a status page). |
|
|
82
92
|
| `aiGuide` | A guide the mapping assistant reads: the provider's quirks, required fields, good defaults. |
|
|
93
|
+
| `recordId` | Path of the provider's own id in a fetched record (e.g. `gtin`). Inbound records are landed and matched by it, so a re-import updates the same product instead of creating another. Default `id`; a flow can override it (`config.upsert.externalIdPath`). |
|
|
94
|
+
| `limits` | `{ requestsPerSecond }` toward the provider, enforced across every SmartLinks worker. |
|
|
83
95
|
|
|
84
96
|
---
|
|
85
97
|
|
|
@@ -87,28 +99,40 @@ Types: `AppConnectorDeclaration` and friends in `@proveanything/smartlinks` (`sr
|
|
|
87
99
|
|
|
88
100
|
Each step names one of the app's server functions. Declare them as **http POST, `visibility:
|
|
89
101
|
"admin"`, `authority: "collection"`**: the engine calls them with no signed-in user (a product edit, a
|
|
90
|
-
schedule), and the console calls the same functions while an admin configures the flow
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
(`network:<host>`) and the secrets it reads (`secrets:<name>`).
|
|
102
|
+
schedule), and the console calls the same functions while an admin configures the flow. The input
|
|
103
|
+
arrives as `event.body`; return the result as JSON. Capabilities still cap what each one can do:
|
|
104
|
+
declare the provider's hosts (`network:<host>`) and the secrets it reads (`secrets:<name>`).
|
|
94
105
|
|
|
95
106
|
| Step | Called by | Input (`event.body`) | Returns |
|
|
96
107
|
|---|---|---|---|
|
|
97
108
|
| `testConnection` | Connect form, **Test** | `{ installation }` | `ConnectorTestResult` `{ ok, message }` |
|
|
98
|
-
| `describeTarget` | Mapper opens | `{ installation }` | `ConnectorTargetSchema` `{ fields: [{ path, label, type, required, enum }] }`: the provider's fields *for this customer
|
|
109
|
+
| `describeTarget` | Mapper opens | `{ installation }` | `ConnectorTargetSchema` `{ fields: [{ path, label, type, required, enum }] }`: the provider's fields *for this customer*. Outbound mappings target these paths. |
|
|
99
110
|
| `sample` | Mapper, AI assist, dry run | `{ installation, limit }` | `ConnectorRecords` `{ records }`: real provider records |
|
|
100
|
-
| `transform` | Every run | `ConnectorTransformInput` `{ installation, records, mapping }` | `ConnectorTransformResult` `{ payloads, skipped }`: one payload per record, same order; `null` skips one |
|
|
101
|
-
| `send` | Every outbound run | `ConnectorSendInput` `{ installation, payloads }` | `ConnectorSendResult` `{ results: [{ index, ok, status, error, externalId }] }` |
|
|
102
111
|
| `fetch` | Every inbound run | `ConnectorFetchInput` `{ installation, cursor, limit }` | `ConnectorRecords` `{ records, nextCursor }`: one page per call |
|
|
112
|
+
| `shape` | Every inbound run (and sample dry runs) | `ConnectorShapeInput` `{ installation, records }` | `ConnectorShapeResult` `{ documents, skipped }`: one provider-neutral document per record, same order; `null` skips one |
|
|
113
|
+
| `serialize` | Every outbound run | `ConnectorSerializeInput` `{ installation, documents, eventType? }`: documents core mapped to `describeTarget` paths; a deletion arrives as `{ id, _deleted: true }` | `ConnectorSerializeResult` `{ payloads, skipped }`: one provider payload per document, same order; `null` skips one |
|
|
114
|
+
| `send` | Every outbound run | `ConnectorSendInput` `{ installation, payloads }` | `ConnectorSendResult` `{ results: [{ index, ok, status, error, externalId }] }` |
|
|
103
115
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
- **
|
|
116
|
+
**How a run flows:**
|
|
117
|
+
- **Inbound:** `fetch` (paged; every record is landed as received) → `shape` → core maps the documents
|
|
118
|
+
with the flow's mapping → upsert, matched by `recordId`. Unchanged records aren't processed again; a
|
|
119
|
+
changed flow configuration replays the landed records without fetching.
|
|
120
|
+
- **Outbound:** gather the SmartLinks records → core maps them to the provider's fields → `serialize`
|
|
121
|
+
→ `send` (or a generic transport). A record identical to its last successful delivery isn't sent again.
|
|
122
|
+
- Large runs are processed in short slices that resume after an interruption; there's no record cap.
|
|
123
|
+
|
|
124
|
+
**What happens when a step is left out:**
|
|
125
|
+
- **No `shape`:** core maps the provider records as they come.
|
|
126
|
+
- **No `serialize`:** the mapped documents are sent as they are.
|
|
127
|
+
- **No `send`:** the flow's own transport is used: HTTP (`config.connection`), or Azure Event Hub with
|
|
109
128
|
`config.transport: 'eventhub'` and `config.eventHub`.
|
|
110
129
|
- **No `describeTarget`:** the mapper offers free-text target paths.
|
|
111
130
|
|
|
131
|
+
**What `shape` should produce:** plain, predictable JSON a person can map without reading the
|
|
132
|
+
provider's documentation. Flatten the provider's envelopes, name things plainly, keep codes alongside
|
|
133
|
+
their meanings, and put repeated sections (ingredients, allergens, nutrients, images) in arrays. The
|
|
134
|
+
same provider record always gives the same document.
|
|
135
|
+
|
|
112
136
|
### `installation`
|
|
113
137
|
|
|
114
138
|
Every step receives the installation it serves:
|
|
@@ -124,13 +148,15 @@ interface ConnectorInstallation {
|
|
|
124
148
|
```
|
|
125
149
|
|
|
126
150
|
Secrets are never in it. Read them in the function with `ctx.secrets.get('<name>')`, which needs the
|
|
127
|
-
matching `secrets:<name>` capability.
|
|
151
|
+
matching `secrets:<name>` capability. A secret the customer set on their collection wins; otherwise the
|
|
152
|
+
connector's own app-level secret of that name (set in Forge) is used, so a connector can ship with
|
|
153
|
+
SmartLinks' credentials as the default.
|
|
128
154
|
|
|
129
|
-
### Example: `fetch`
|
|
155
|
+
### Example: `fetch` and `shape`
|
|
130
156
|
|
|
131
157
|
```ts
|
|
132
158
|
// src/functions/index.ts (the app's functions bundle)
|
|
133
|
-
import type { ConnectorFetchInput, ConnectorRecords } from '@proveanything/smartlinks'
|
|
159
|
+
import type { ConnectorFetchInput, ConnectorRecords, ConnectorShapeInput, ConnectorShapeResult } from '@proveanything/smartlinks'
|
|
134
160
|
|
|
135
161
|
export async function fetch(ctx: any, event: any): Promise<ConnectorRecords> {
|
|
136
162
|
const { installation, cursor, limit = 50 } = event.body as ConnectorFetchInput
|
|
@@ -140,6 +166,18 @@ export async function fetch(ctx: any, event: any): Promise<ConnectorRecords> {
|
|
|
140
166
|
const page = await res.json()
|
|
141
167
|
return { records: page.items, nextCursor: page.next ?? null }
|
|
142
168
|
}
|
|
169
|
+
|
|
170
|
+
export async function shape(_ctx: any, event: any): Promise<ConnectorShapeResult> {
|
|
171
|
+
const { records } = event.body as ConnectorShapeInput
|
|
172
|
+
return {
|
|
173
|
+
documents: records.map((r: any) => ({
|
|
174
|
+
gtin: r.item?.gtin,
|
|
175
|
+
name: r.item?.tradeItemDescription?.[0]?.value,
|
|
176
|
+
brand: r.item?.brandName,
|
|
177
|
+
ingredients: (r.item?.ingredients ?? []).map((i: any) => ({ name: i.name, percent: i.percentage })),
|
|
178
|
+
})),
|
|
179
|
+
}
|
|
180
|
+
}
|
|
143
181
|
```
|
|
144
182
|
|
|
145
183
|
---
|
|
@@ -148,9 +186,9 @@ export async function fetch(ctx: any, event: any): Promise<ConnectorRecords> {
|
|
|
148
186
|
|
|
149
187
|
| What | Where |
|
|
150
188
|
|---|---|
|
|
151
|
-
| Connect-form values (non-secret), installation settings | The connector app's config on the collection
|
|
152
|
-
| Credentials | The connector app's secrets, by the names its fields declare. |
|
|
153
|
-
| One flow's use of the connector | The flow: `config.connector = { appId, preset?, settings? }` (per-flow settings override the installation's), plus `config.fieldMappings
|
|
189
|
+
| Connect-form values (non-secret), installation settings | The connector app's config on the collection, in its admin-only zone (`admin.connector.connection`, `admin.connector.settings`). |
|
|
190
|
+
| Credentials | The connector app's secrets, by the names its fields declare (the customer's on their collection, else the connector's default). |
|
|
191
|
+
| One flow's use of the connector | The flow: `config.connector = { appId, preset?, settings? }` (per-flow settings override the installation's), plus `config.fieldMappings`, which core runs. |
|
|
154
192
|
|
|
155
193
|
```ts
|
|
156
194
|
await integrations.createFlow(collectionId, {
|
|
@@ -162,7 +200,8 @@ await integrations.createFlow(collectionId, {
|
|
|
162
200
|
targetEntity: 'product',
|
|
163
201
|
config: {
|
|
164
202
|
connector: { appId: 'oneworldsync', preset: 'import-products' },
|
|
165
|
-
|
|
203
|
+
// Maps the connector's shaped documents (not the raw provider records) onto the product.
|
|
204
|
+
fieldMappings: [ { targetPath: 'name', sourcePath: 'name', transformType: 'direct' } ],
|
|
166
205
|
},
|
|
167
206
|
})
|
|
168
207
|
```
|
|
@@ -181,7 +220,9 @@ await secrets.put(collectionId, 'provider-key', value, { appId }) /
|
|
|
181
220
|
await integrations.saveConnectorInstallation(collectionId, appId, { connection: { environment: 'production' } })
|
|
182
221
|
const test = await integrations.runConnectorStep(collectionId, appId, 'testConnection')
|
|
183
222
|
const target = await integrations.runConnectorStep(collectionId, appId, 'describeTarget')
|
|
184
|
-
const
|
|
223
|
+
const { records } = await integrations.runConnectorStep(collectionId, appId, 'sample', { limit: 5 })
|
|
224
|
+
const shaped = await integrations.runConnectorStep(collectionId, appId, 'shape', { records }) // dry run
|
|
225
|
+
const wire = await integrations.runConnectorStep(collectionId, appId, 'serialize', { documents }) // dry run
|
|
185
226
|
```
|
|
186
227
|
|
|
187
228
|
`send` and `fetch` only run inside a flow. A flow run's summary carries `sent` (outbound) or
|
|
@@ -192,8 +233,9 @@ const dryRun = await integrations.runConnectorStep(collectionId, appId, 'transfo
|
|
|
192
233
|
## Panels
|
|
193
234
|
|
|
194
235
|
A connector can contribute screens to the integrations console (an API explorer, say). Each
|
|
195
|
-
`panels[]` entry names a component exported from the app's widgets bundle; the console mounts it
|
|
196
|
-
collection and the installation. Panels are admin-only and call the
|
|
236
|
+
`panels[]` entry names a component exported from the app's widgets bundle; the console mounts it
|
|
237
|
+
host-side and passes it the collection and the installation. Panels are admin-only and call the
|
|
238
|
+
connector's own functions.
|
|
197
239
|
|
|
198
240
|
---
|
|
199
241
|
|
package/docs/integrations.md
CHANGED
|
@@ -19,6 +19,14 @@ Provider-specific logic (a provider's payload format, its API, its connect form)
|
|
|
19
19
|
**connector app**, not in the engine: see **[Connectors](connectors.md)**. A flow uses one with
|
|
20
20
|
`config.connector = { appId }`.
|
|
21
21
|
|
|
22
|
+
**Reliability.** A record that fails is retried on its own with exponential backoff (honouring the
|
|
23
|
+
destination's `Retry-After`); a permanent failure (most 4xx) or one out of attempts is **dead** and
|
|
24
|
+
waits in `integrations.listFailed` for `integrations.retryFailed`. A destination that keeps failing
|
|
25
|
+
**pauses the flow automatically** (`flow.circuitOpenedAt`): edits are held, small probes check for
|
|
26
|
+
recovery, and it resumes on its own (or with `integrations.resumeFlow`). Every product and record
|
|
27
|
+
change is recorded durably before it's dispatched, so an edit can't be missed; scheduled sweeps are
|
|
28
|
+
the reconciliation net.
|
|
29
|
+
|
|
22
30
|
Credentials are **never** stored on the flow. The connection holds an opaque
|
|
23
31
|
`credentialRef` into the **sealed-secret store** (`secrets` namespace); the value is sealed
|
|
24
32
|
at rest and resolved server-side only, at execution.
|
package/docs/site-seo.md
CHANGED
|
@@ -1,68 +1,69 @@
|
|
|
1
|
-
# Websites: search engines and AI crawlers (SEO + GEO)
|
|
2
|
-
|
|
3
|
-
For apps served as websites (an app site at `<name>.smartlinks.host` or a custom domain). Hub sites
|
|
4
|
-
get all of this from Hub.
|
|
5
|
-
|
|
6
|
-
## What the platform does for you
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
the
|
|
15
|
-
- **
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- **
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
SL.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
|
48
|
-
|
|
49
|
-
| `schema.
|
|
50
|
-
| `schema.
|
|
51
|
-
| `schema.
|
|
52
|
-
| `schema.
|
|
53
|
-
|
|
54
|
-
`
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
`
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
- **
|
|
67
|
-
|
|
68
|
-
|
|
1
|
+
# Websites: search engines and AI crawlers (SEO + GEO)
|
|
2
|
+
|
|
3
|
+
For apps served as websites (an app site at `<name>.smartlinks.host` or a custom domain). Hub sites
|
|
4
|
+
get all of this from Hub.
|
|
5
|
+
|
|
6
|
+
## What the platform does for you
|
|
7
|
+
|
|
8
|
+
- **Every page is server-rendered.** A website runs on the SmartLinks site runtime (see
|
|
9
|
+
[websites.md](websites.md)): the first response for any URL is the finished HTML, with its title,
|
|
10
|
+
description, link-preview image, structured data and real status codes (404s are 404s). Search
|
|
11
|
+
engines, AI crawlers and link previews read it without running JavaScript.
|
|
12
|
+
- **`/robots.txt`, `/sitemap.xml`, `/llms.txt`** are generated on the site's own address. Don't ship
|
|
13
|
+
`robots.txt` or `sitemap.xml`: one build serves many sites, and sitemap URLs must be absolute on
|
|
14
|
+
the site's host, which the build can't know.
|
|
15
|
+
- **List every page** in `public/sitemap-paths.txt`, one `<path> [Title]` per line, in menu order
|
|
16
|
+
(`/ Home`, `/menu Menu`, `/book Book a table`). They go into the sitemap and `llms.txt` (titled)
|
|
17
|
+
on the right host, and Forge's preview uses the same list as its page menu. Keep it in step with
|
|
18
|
+
the router.
|
|
19
|
+
- **Canonical address.** A site can answer on its automatic address, a chosen name and a custom
|
|
20
|
+
domain. Every page gets `Link: <https://{canonical}{path}>; rel="canonical"`, so search engines
|
|
21
|
+
consolidate on one: the custom domain, else the chosen name, else the automatic address. Don't set
|
|
22
|
+
your own canonical unless a page has a different canonical page.
|
|
23
|
+
- **Previews stay out of search.** A sandbox collection's sites, and any test build (dev, alpha,
|
|
24
|
+
beta), are served with `X-Robots-Tag: noindex` and a `robots.txt` that disallows everything,
|
|
25
|
+
whatever the build ships.
|
|
26
|
+
|
|
27
|
+
## What you do: `SL.seo` and `SL.site`
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import * as SL from '@proveanything/smartlinks'
|
|
31
|
+
|
|
32
|
+
// Per route, from its data — on every route change:
|
|
33
|
+
SL.seo.head({
|
|
34
|
+
title: `${product.name} — ${brand}`,
|
|
35
|
+
description: product.description, // ~150 characters, says what the page is
|
|
36
|
+
image: product.heroImage?.url, // absolute; used for link previews
|
|
37
|
+
type: 'product', // 'website' | 'article' | 'product'
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// Structured data (schema.org JSON-LD). The id names the block, so calling again replaces it:
|
|
41
|
+
SL.seo.jsonLd('page', SL.seo.schema.product(product, { url: location.href, brand }))
|
|
42
|
+
SL.seo.jsonLd('faq', SL.seo.schema.faqPage(faqs.map((f) => ({ question: f.q, answer: f.a }))))
|
|
43
|
+
SL.seo.jsonLd('org', SL.seo.schema.organization(collection))
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
| Builder | For |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `schema.product(product, { url, brand, offer, rating })` | product pages (`offer` only when it's really sold) |
|
|
50
|
+
| `schema.faqPage([{ question, answer }])` | FAQs: rich results, and the answer-shaped content AI search cites |
|
|
51
|
+
| `schema.organization(collection)` | the brand behind the site (home page) |
|
|
52
|
+
| `schema.localBusiness(collection, { type, address, telephone, openingHours })` | a business with a place (`type: 'Florist'`, `'Restaurant'`…) |
|
|
53
|
+
| `schema.breadcrumbs([{ name, url }])` | nested pages |
|
|
54
|
+
| `schema.article({ headline, datePublished, … })` | posts, guides, news |
|
|
55
|
+
|
|
56
|
+
`seo.head` removes tags a previous route set and the next one doesn't. Both are no-ops without a
|
|
57
|
+
`document`. In a server-rendered site, prefer the router's own head support (TanStack Start's route
|
|
58
|
+
`head()`), so the tags are in the server's HTML; use `SL.seo.schema.*` to build the JSON-LD for it.
|
|
59
|
+
|
|
60
|
+
`SL.site.ready()` is no longer needed: pages arrive finished from the server.
|
|
61
|
+
|
|
62
|
+
## Content that gets found
|
|
63
|
+
|
|
64
|
+
- **Content in the page, not behind interaction.** FAQ answers in `<details>` are fine; answers
|
|
65
|
+
fetched only when a question is clicked aren't seen.
|
|
66
|
+
- **One `<h1>` per page**, a logical heading order, `alt` text on content images.
|
|
67
|
+
- **Answer-shaped writing.** A short summary near the top, question-style headings where they fit.
|
|
68
|
+
This is what AI assistants quote.
|
|
69
|
+
- **Real links** (`<a href>`) between pages, never hash routes.
|