@proveanything/smartlinks 2.0.56 → 2.0.58

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.
@@ -1,4 +1,4 @@
1
- import type { IntegrationFlow, CreateFlowInput, UpdateFlowInput, ListFlowsQuery, FlowList, RunFlowInput, RunFlowResult, RunFlowSummary, RunFlowEnqueued, RecordTypesResponse, FlowRun, ListRunsQuery, RunList, ListRunItemsQuery, RunItemList, LoggingWindow, SetLoggingInput } from "../types/integrations.js";
1
+ import type { IntegrationFlow, CreateFlowInput, UpdateFlowInput, ListFlowsQuery, FlowList, RunFlowInput, RunFlowResult, RunFlowSummary, RunFlowEnqueued, RecordTypesResponse, FlowRun, ListRunsQuery, RunList, ListRunItemsQuery, RunItemList, LoggingWindow, SetLoggingInput, InstalledConnector, SaveConnectorInstallationInput, ConnectorConsoleStep } from "../types/integrations.js";
2
2
  export declare namespace integrations {
3
3
  /** List flows in a collection. GET /integrations/flows */
4
4
  function listFlows(collectionId: string, query?: ListFlowsQuery): Promise<FlowList>;
@@ -57,4 +57,25 @@ export declare namespace integrations {
57
57
  function cancelLogging(collectionId: string, connectionId: string): Promise<{
58
58
  canceled: boolean;
59
59
  }>;
60
+ /** The collection's installed connector apps. GET /integrations/connectors */
61
+ function listConnectors(collectionId: string): Promise<{
62
+ connectors: InstalledConnector[];
63
+ }>;
64
+ /** Save a connector's non-secret Connect-form values and/or settings. PUT /integrations/connectors/:appId/installation */
65
+ function saveConnectorInstallation(collectionId: string, appId: string, input: SaveConnectorInstallationInput): Promise<{
66
+ installation: {
67
+ connection: Record<string, unknown>;
68
+ settings: Record<string, unknown>;
69
+ };
70
+ }>;
71
+ /**
72
+ * Run a connector step from the console: `testConnection` → ConnectorTestResult, `describeTarget` →
73
+ * ConnectorTargetSchema, `sample` → ConnectorRecords, and dry runs of `shape` ({ records }) →
74
+ * ConnectorShapeResult and `serialize` ({ documents }) → ConnectorSerializeResult.
75
+ * The server adds the installation; pass `flowId` to layer that flow's settings.
76
+ * POST /integrations/connectors/:appId/steps/:step
77
+ */
78
+ function runConnectorStep<T = unknown>(collectionId: string, appId: string, step: ConnectorConsoleStep, input?: Record<string, unknown> & {
79
+ flowId?: string;
80
+ }): Promise<T>;
60
81
  }
@@ -135,4 +135,30 @@ export var integrations;
135
135
  return del(`/admin/collection/${enc(collectionId)}/integrations/logging/${enc(connectionId)}`);
136
136
  }
137
137
  integrations.cancelLogging = cancelLogging;
138
+ // --- Connectors (docs/connectors.md) ------------------------------------
139
+ // Connector apps installed on the collection: their declared contract, the installation's
140
+ // non-secret settings, and which secrets are set. Secrets themselves go through
141
+ // `secrets.put(collectionId, name, value, { appId })` for the connector's appId.
142
+ const connectorsBase = (collectionId) => `/admin/collection/${enc(collectionId)}/integrations/connectors`;
143
+ /** The collection's installed connector apps. GET /integrations/connectors */
144
+ async function listConnectors(collectionId) {
145
+ return request(connectorsBase(collectionId));
146
+ }
147
+ integrations.listConnectors = listConnectors;
148
+ /** Save a connector's non-secret Connect-form values and/or settings. PUT /integrations/connectors/:appId/installation */
149
+ async function saveConnectorInstallation(collectionId, appId, input) {
150
+ return put(`${connectorsBase(collectionId)}/${enc(appId)}/installation`, input);
151
+ }
152
+ integrations.saveConnectorInstallation = saveConnectorInstallation;
153
+ /**
154
+ * Run a connector step from the console: `testConnection` → ConnectorTestResult, `describeTarget` →
155
+ * ConnectorTargetSchema, `sample` → ConnectorRecords, and dry runs of `shape` ({ records }) →
156
+ * ConnectorShapeResult and `serialize` ({ documents }) → ConnectorSerializeResult.
157
+ * The server adds the installation; pass `flowId` to layer that flow's settings.
158
+ * POST /integrations/connectors/:appId/steps/:step
159
+ */
160
+ async function runConnectorStep(collectionId, appId, step, input = {}) {
161
+ return post(`${connectorsBase(collectionId)}/${enc(appId)}/steps/${enc(step)}`, input);
162
+ }
163
+ integrations.runConnectorStep = runConnectorStep;
138
164
  })(integrations || (integrations = {}));
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.56 | Generated: 2026-10-12T20:58:59.335Z
3
+ Version: 2.0.58 | Generated: 2026-10-13T14:08:01.791Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -65,6 +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 (adapters for one external provider): a manifest `connector` block + server functions (testConnection/describeTarget/sample/fetch/shape/serialize/send); core does the mapping
68
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
69
70
  - **[Deep Link Discovery](deep-link-discovery.md)** - Registering and discovering navigable app states for portal menus and AI orchestration
70
71
  - **[AI Guide Template](ai-guide-template.md)** - A sample for an app on how to build an AI setup guide
@@ -2853,10 +2854,173 @@ interface AppManifest {
2853
2854
  realtime?: { channels: RealtimeChannelDeclaration[] };
2854
2855
  data?: AppDataDeclaration;
2855
2856
  headless?: AppHeadlessDeclaration;
2857
+ connector?: AppConnectorDeclaration;
2856
2858
  [key: string]: any;
2857
2859
  }
2858
2860
  ```
2859
2861
 
2862
+ **ConnectorConnectionField** (interface)
2863
+ ```typescript
2864
+ interface ConnectorConnectionField {
2865
+ name: string
2866
+ label: string
2867
+ description?: string
2868
+ secret?: string
2869
+ required?: boolean
2870
+ type?: 'text' | 'url' | 'select' | 'number' | 'boolean'
2871
+ options?: Array<{ value: string; label: string }>
2872
+ placeholder?: string
2873
+ }
2874
+ ```
2875
+
2876
+ **ConnectorPreset** (interface)
2877
+ ```typescript
2878
+ interface ConnectorPreset {
2879
+ id: string
2880
+ title: string
2881
+ description?: string
2882
+ direction: 'inbound' | 'outbound'
2883
+ entity: string
2884
+ flow?: Record<string, unknown>
2885
+ }
2886
+ ```
2887
+
2888
+ **ConnectorPanel** (interface)
2889
+ ```typescript
2890
+ interface ConnectorPanel {
2891
+ id: string
2892
+ title: string
2893
+ component: string
2894
+ }
2895
+ ```
2896
+
2897
+ **AppConnectorDeclaration** (interface)
2898
+ ```typescript
2899
+ interface AppConnectorDeclaration {
2900
+ provider: string
2901
+ title?: string
2902
+ directions: Array<'inbound' | 'outbound'>
2903
+ entities: string[]
2904
+ connection?: { fields: ConnectorConnectionField[] }
2905
+ settings?: Record<string, unknown>
2906
+ presets?: ConnectorPreset[]
2907
+ functions: {
2908
+ testConnection?: string
2909
+ describeTarget?: string
2910
+ sample?: string
2911
+ shape?: string
2912
+ serialize?: string
2913
+ send?: string
2914
+ fetch?: string
2915
+ }
2916
+ panels?: ConnectorPanel[]
2917
+ aiGuide?: string
2918
+ }
2919
+ ```
2920
+
2921
+ **ConnectorInstallation** (interface)
2922
+ ```typescript
2923
+ interface ConnectorInstallation {
2924
+ collectionId: string
2925
+ appId: string
2926
+ flowId?: string | null
2927
+ connection: Record<string, unknown>
2928
+ settings: Record<string, unknown>
2929
+ }
2930
+ ```
2931
+
2932
+ **ConnectorTestResult** (interface)
2933
+ ```typescript
2934
+ interface ConnectorTestResult {
2935
+ ok: boolean
2936
+ message: string
2937
+ }
2938
+ ```
2939
+
2940
+ **ConnectorTargetField** (interface)
2941
+ ```typescript
2942
+ interface ConnectorTargetField {
2943
+ path: string
2944
+ label?: string
2945
+ type?: 'string' | 'number' | 'boolean' | 'date' | 'object' | 'array'
2946
+ required?: boolean
2947
+ description?: string
2948
+ enum?: string[]
2949
+ }
2950
+ ```
2951
+
2952
+ **ConnectorTargetSchema** (interface)
2953
+ ```typescript
2954
+ interface ConnectorTargetSchema {
2955
+ fields: ConnectorTargetField[]
2956
+ }
2957
+ ```
2958
+
2959
+ **ConnectorRecords** (interface)
2960
+ ```typescript
2961
+ interface ConnectorRecords {
2962
+ records: Array<Record<string, unknown>>
2963
+ nextCursor?: string | null
2964
+ }
2965
+ ```
2966
+
2967
+ **ConnectorShapeInput** (interface)
2968
+ ```typescript
2969
+ interface ConnectorShapeInput {
2970
+ installation: ConnectorInstallation
2971
+ records: Array<Record<string, unknown>>
2972
+ }
2973
+ ```
2974
+
2975
+ **ConnectorShapeResult** (interface)
2976
+ ```typescript
2977
+ interface ConnectorShapeResult {
2978
+ documents: Array<Record<string, unknown> | null>
2979
+ skipped?: Array<{ index: number; reason: string }>
2980
+ }
2981
+ ```
2982
+
2983
+ **ConnectorSerializeInput** (interface)
2984
+ ```typescript
2985
+ interface ConnectorSerializeInput {
2986
+ installation: ConnectorInstallation
2987
+ documents: Array<Record<string, unknown>>
2988
+ eventType?: string
2989
+ }
2990
+ ```
2991
+
2992
+ **ConnectorSerializeResult** (interface)
2993
+ ```typescript
2994
+ interface ConnectorSerializeResult {
2995
+ payloads: Array<unknown | null>
2996
+ skipped?: Array<{ index: number; reason: string }>
2997
+ }
2998
+ ```
2999
+
3000
+ **ConnectorSendInput** (interface)
3001
+ ```typescript
3002
+ interface ConnectorSendInput {
3003
+ installation: ConnectorInstallation
3004
+ payloads: unknown[]
3005
+ }
3006
+ ```
3007
+
3008
+ **ConnectorSendResult** (interface)
3009
+ ```typescript
3010
+ interface ConnectorSendResult {
3011
+ results: Array<{ index: number; ok: boolean; status?: number; error?: string; externalId?: string }>
3012
+ }
3013
+ ```
3014
+
3015
+ **ConnectorFetchInput** (interface)
3016
+ ```typescript
3017
+ interface ConnectorFetchInput {
3018
+ installation: ConnectorInstallation
3019
+ cursor?: string | null
3020
+ limit?: number
3021
+ }
3022
+ ```
3023
+
2860
3024
  **CollectionAppWidget** (interface)
2861
3025
  ```typescript
2862
3026
  interface CollectionAppWidget {
@@ -6899,11 +7063,21 @@ interface FlowConnection {
6899
7063
  }
6900
7064
  ```
6901
7065
 
7066
+ **FlowConnectorRef** (interface)
7067
+ ```typescript
7068
+ interface FlowConnectorRef {
7069
+ appId: string
7070
+ preset?: string
7071
+ settings?: Record<string, unknown>
7072
+ }
7073
+ ```
7074
+
6902
7075
  **IntegrationFlowConfig** (interface)
6903
7076
  ```typescript
6904
7077
  interface IntegrationFlowConfig {
6905
7078
  connection?: FlowConnection
6906
7079
  fieldMappings?: FieldMapping[]
7080
+ connector?: FlowConnectorRef
6907
7081
  [key: string]: any
6908
7082
  }
6909
7083
  ```
@@ -6982,13 +7156,37 @@ interface RunFlowSummary {
6982
7156
  flowId: string
6983
7157
  direction: FlowDirection
6984
7158
  records: number
6985
- sent: number
7159
+ sent?: number
6986
7160
  failed: number
7161
+ created?: number
7162
+ updated?: number
7163
+ skipped?: number
6987
7164
  status: RunStatus
6988
7165
  runId?: string
6989
7166
  }
6990
7167
  ```
6991
7168
 
7169
+ **InstalledConnector** (interface)
7170
+ ```typescript
7171
+ interface InstalledConnector {
7172
+ appId: string
7173
+ name: string
7174
+ channel: string
7175
+ version: string | null
7176
+ connector: import('./appManifest').AppConnectorDeclaration
7177
+ installation: { connection: Record<string, unknown>; settings: Record<string, unknown> }
7178
+ secrets: Record<string, { set: boolean; hint?: string | null }>
7179
+ }
7180
+ ```
7181
+
7182
+ **SaveConnectorInstallationInput** (interface)
7183
+ ```typescript
7184
+ interface SaveConnectorInstallationInput {
7185
+ connection?: Record<string, unknown>
7186
+ settings?: Record<string, unknown>
7187
+ }
7188
+ ```
7189
+
6992
7190
  **CapturedRequest** (interface)
6993
7191
  ```typescript
6994
7192
  interface CapturedRequest {
@@ -7193,6 +7391,8 @@ interface RecordTypesResponse {
7193
7391
 
7194
7392
  **UpdateFlowInput** = `Partial<Omit<CreateFlowInput, 'direction'>> & {`
7195
7393
 
7394
+ **ConnectorConsoleStep** = `'testConnection' | 'describeTarget' | 'sample' | 'shape' | 'serialize'`
7395
+
7196
7396
  **RunTrigger** = `'manual' | 'event' | 'schedule' | 'test'`
7197
7397
 
7198
7398
  **FlowRunStatus** = `'running' | 'success' | 'partial' | 'failed' | 'skipped'`
@@ -11507,6 +11707,20 @@ Enable enhanced logging. POST /integrations/logging/:connectionId (omit ttlMinut
11507
11707
  **cancelLogging**(collectionId: string, connectionId: string) → `Promise<`
11508
11708
  Cancel enhanced logging now. DELETE /integrations/logging/:connectionId
11509
11709
 
11710
+ **listConnectors**(collectionId: string) → `Promise<`
11711
+ The collection's installed connector apps. GET /integrations/connectors
11712
+
11713
+ **saveConnectorInstallation**(collectionId: string,
11714
+ appId: string,
11715
+ input: SaveConnectorInstallationInput) → `Promise<`
11716
+ Save a connector's non-secret Connect-form values and/or settings. PUT /integrations/connectors/:appId/installation
11717
+
11718
+ **runConnectorStep**(collectionId: string,
11719
+ appId: string,
11720
+ step: ConnectorConsoleStep,
11721
+ input: Record<string, unknown> & { flowId?: string } = {}) → `Promise<T>`
11722
+ 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
11723
+
11510
11724
  ### interactions
11511
11725
 
11512
11726
  **query**(collectionId: string,
@@ -436,6 +436,13 @@ this is just the manifest shape.
436
436
  | `definitions[].authority` | string | `caller` \| `collection` — whose authority it runs as |
437
437
  | `definitions[].capabilities` | string[] | Least-privilege grants (`sl:<res>:<read\|write>`, `network[:<host>]`, `secrets:<ref>`) |
438
438
 
439
+ #### `connector`
440
+
441
+ Marks the app as an integration **connector** for one external provider: the integrations console
442
+ lists it and draws its connect form and settings, and the integration engine calls its server
443
+ functions (`testConnection`, `describeTarget`, `sample`, `fetch`, `shape`, `serialize`, `send`). It adapts the provider's format; core does each customer's mapping. The full
444
+ contract is in the **[Connectors guide](connectors.md)**.
445
+
439
446
  ---
440
447
 
441
448
  ## `app.admin.json`
@@ -0,0 +1,241 @@
1
+ # Integration connectors
2
+
3
+ A **connector** is an app that knows one external provider (a PIM, a content network, a shop
4
+ platform…). It is installed on a collection like any app, versioned and released like any app, and
5
+ it adds a manifest `connector` block plus a handful of [server functions](server-functions.md) the
6
+ integration engine and the integrations console call by convention.
7
+
8
+ Three layers, each with one job:
9
+
10
+ | Layer | Holds |
11
+ |---|---|
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
+ | **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: 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.
21
+
22
+ A flow that only posts JSON to a webhook or an Event Hub needs no connector; it uses the engine's
23
+ generic transports (see [integrations.md](integrations.md)).
24
+
25
+ **No AI at run time.** The AI helps an admin *author* a mapping in the console (from the connector's
26
+ target schema plus real sample records); a person approves it, and it is saved as data and runs
27
+ deterministically. A connector's `shape` and `serialize` must be deterministic too.
28
+
29
+ ---
30
+
31
+ ## The manifest block
32
+
33
+ ```jsonc
34
+ // app.manifest.json
35
+ "connector": {
36
+ "provider": "oneworldsync",
37
+ "title": "1WorldSync",
38
+ "directions": ["inbound"],
39
+ "entities": ["product"],
40
+ "connection": {
41
+ "fields": [
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 },
46
+ { "name": "environment", "label": "Environment", "type": "select",
47
+ "options": [ { "value": "preprod", "label": "Pre-production" }, { "value": "production", "label": "Production" } ] }
48
+ ]
49
+ },
50
+ "settings": { // JSON Schema → a form in the console
51
+ "type": "object",
52
+ "properties": { "targetMarket": { "type": "string", "default": "GB" } }
53
+ },
54
+ "presets": [
55
+ { "id": "import-products", "title": "Import products", "direction": "inbound", "entity": "product",
56
+ "flow": { "config": { "fieldMappings": [ /* a suggested mapping the customer can edit */ ] } } }
57
+ ],
58
+ "functions": { // names of this app's server functions
59
+ "testConnection": "testConnection",
60
+ "describeTarget": "describeTarget",
61
+ "sample": "sample",
62
+ "shape": "shape",
63
+ "fetch": "fetch"
64
+ },
65
+ "panels": [ { "id": "explorer", "title": "Explorer", "component": "Explorer" } ],
66
+ "aiGuide": "connector-ai-guide.md"
67
+ },
68
+ "functions": {
69
+ "files": { "js": { "umd": "dist/functions.umd.js" } },
70
+ "definitions": [
71
+ { "name": "testConnection", "trigger": { "type": "http", "method": "POST" },
72
+ "visibility": "admin", "authority": "collection",
73
+ "capabilities": ["network:content1-api.1worldsync.com", "secrets:oneworldsync-app-id", "secrets:oneworldsync-secret-key"] }
74
+ // …one definition per function named above
75
+ ]
76
+ }
77
+ ```
78
+
79
+ Types: `AppConnectorDeclaration` and friends in `@proveanything/smartlinks` (`src/types/appManifest.ts`).
80
+
81
+ | Field | Meaning |
82
+ |---|---|
83
+ | `provider` | Stable provider key (`syndigo`, `oneworldsync`). |
84
+ | `title` | Provider display name; defaults to the app name. |
85
+ | `directions` | `inbound` (provider → SmartLinks), `outbound` (SmartLinks → provider), or both. |
86
+ | `entities` | SmartLinks entities it maps, e.g. `["product"]`. |
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. |
88
+ | `settings` | JSON Schema for installation settings; the console renders it as a form. |
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. |
90
+ | `functions` | Which of this app's server functions implement each step (below). Fill in only what applies. |
91
+ | `panels[]` | Provider-specific screens the console mounts (an API explorer, a status page). |
92
+ | `aiGuide` | A guide the mapping assistant reads: the provider's quirks, required fields, good defaults. |
93
+
94
+ ---
95
+
96
+ ## The functions
97
+
98
+ Each step names one of the app's server functions. Declare them as **http POST, `visibility:
99
+ "admin"`, `authority: "collection"`**: the engine calls them with no signed-in user (a product edit, a
100
+ schedule), and the console calls the same functions while an admin configures the flow. The input
101
+ arrives as `event.body`; return the result as JSON. Capabilities still cap what each one can do:
102
+ declare the provider's hosts (`network:<host>`) and the secrets it reads (`secrets:<name>`).
103
+
104
+ | Step | Called by | Input (`event.body`) | Returns |
105
+ |---|---|---|---|
106
+ | `testConnection` | Connect form, **Test** | `{ installation }` | `ConnectorTestResult` `{ ok, message }` |
107
+ | `describeTarget` | Mapper opens | `{ installation }` | `ConnectorTargetSchema` `{ fields: [{ path, label, type, required, enum }] }`: the provider's fields *for this customer*. Outbound mappings target these paths. |
108
+ | `sample` | Mapper, AI assist, dry run | `{ installation, limit }` | `ConnectorRecords` `{ records }`: real provider records |
109
+ | `fetch` | Every inbound run | `ConnectorFetchInput` `{ installation, cursor, limit }` | `ConnectorRecords` `{ records, nextCursor }`: one page per call |
110
+ | `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 |
111
+ | `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 |
112
+ | `send` | Every outbound run | `ConnectorSendInput` `{ installation, payloads }` | `ConnectorSendResult` `{ results: [{ index, ok, status, error, externalId }] }` |
113
+
114
+ **How a run flows:**
115
+ - **Inbound:** `fetch` (paged) → `shape` → core maps the documents with the flow's mapping → upsert.
116
+ - **Outbound:** gather the SmartLinks records → core maps them to the provider's fields → `serialize`
117
+ → `send` (or a generic transport).
118
+
119
+ **What happens when a step is left out:**
120
+ - **No `shape`:** core maps the provider records as they come.
121
+ - **No `serialize`:** the mapped documents are sent as they are.
122
+ - **No `send`:** the flow's own transport is used: HTTP (`config.connection`), or Azure Event Hub with
123
+ `config.transport: 'eventhub'` and `config.eventHub`.
124
+ - **No `describeTarget`:** the mapper offers free-text target paths.
125
+
126
+ **What `shape` should produce:** plain, predictable JSON a person can map without reading the
127
+ provider's documentation. Flatten the provider's envelopes, name things plainly, keep codes alongside
128
+ their meanings, and put repeated sections (ingredients, allergens, nutrients, images) in arrays. The
129
+ same provider record always gives the same document.
130
+
131
+ ### `installation`
132
+
133
+ Every step receives the installation it serves:
134
+
135
+ ```ts
136
+ interface ConnectorInstallation {
137
+ collectionId: string
138
+ appId: string // the connector app
139
+ flowId?: string | null // when called for a flow
140
+ connection: Record<string, unknown> // non-secret Connect-form fields
141
+ settings: Record<string, unknown> // installation settings, with the flow's overrides on top
142
+ }
143
+ ```
144
+
145
+ Secrets are never in it. Read them in the function with `ctx.secrets.get('<name>')`, which needs the
146
+ matching `secrets:<name>` capability. A secret the customer set on their collection wins; otherwise the
147
+ connector's own app-level secret of that name (set in Forge) is used, so a connector can ship with
148
+ SmartLinks' credentials as the default.
149
+
150
+ ### Example: `fetch` and `shape`
151
+
152
+ ```ts
153
+ // src/functions/index.ts (the app's functions bundle)
154
+ import type { ConnectorFetchInput, ConnectorRecords, ConnectorShapeInput, ConnectorShapeResult } from '@proveanything/smartlinks'
155
+
156
+ export async function fetch(ctx: any, event: any): Promise<ConnectorRecords> {
157
+ const { installation, cursor, limit = 50 } = event.body as ConnectorFetchInput
158
+ const appId = await ctx.secrets.get('provider-app-id')
159
+ const key = await ctx.secrets.get('provider-secret-key')
160
+ const res = await ctx.fetch(buildSignedUrl(installation, { cursor, limit }, appId, key))
161
+ const page = await res.json()
162
+ return { records: page.items, nextCursor: page.next ?? null }
163
+ }
164
+
165
+ export async function shape(_ctx: any, event: any): Promise<ConnectorShapeResult> {
166
+ const { records } = event.body as ConnectorShapeInput
167
+ return {
168
+ documents: records.map((r: any) => ({
169
+ gtin: r.item?.gtin,
170
+ name: r.item?.tradeItemDescription?.[0]?.value,
171
+ brand: r.item?.brandName,
172
+ ingredients: (r.item?.ingredients ?? []).map((i: any) => ({ name: i.name, percent: i.percentage })),
173
+ })),
174
+ }
175
+ }
176
+ ```
177
+
178
+ ---
179
+
180
+ ## Where configuration lives
181
+
182
+ | What | Where |
183
+ |---|---|
184
+ | 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`). |
185
+ | Credentials | The connector app's secrets, by the names its fields declare (the customer's on their collection, else the connector's default). |
186
+ | 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. |
187
+
188
+ ```ts
189
+ await integrations.createFlow(collectionId, {
190
+ appId: 'core-integrations',
191
+ direction: 'inbound',
192
+ name: 'Import products from 1WorldSync',
193
+ status: 'active',
194
+ schedule: '0 * * * *',
195
+ targetEntity: 'product',
196
+ config: {
197
+ connector: { appId: 'oneworldsync', preset: 'import-products' },
198
+ // Maps the connector's shaped documents (not the raw provider records) onto the product.
199
+ fieldMappings: [ { targetPath: 'name', sourcePath: 'name', transformType: 'direct' } ],
200
+ },
201
+ })
202
+ ```
203
+
204
+ ---
205
+
206
+ ## From the console
207
+
208
+ ```ts
209
+ import { integrations, secrets } from '@proveanything/smartlinks'
210
+
211
+ const { connectors } = await integrations.listConnectors(collectionId)
212
+ // → [{ appId, name, version, connector, installation: { connection, settings }, secrets: { 'provider-key': { set, hint } } }]
213
+
214
+ await secrets.put(collectionId, 'provider-key', value, { appId }) // a secret field
215
+ await integrations.saveConnectorInstallation(collectionId, appId, { connection: { environment: 'production' } })
216
+ const test = await integrations.runConnectorStep(collectionId, appId, 'testConnection')
217
+ const target = await integrations.runConnectorStep(collectionId, appId, 'describeTarget')
218
+ const { records } = await integrations.runConnectorStep(collectionId, appId, 'sample', { limit: 5 })
219
+ const shaped = await integrations.runConnectorStep(collectionId, appId, 'shape', { records }) // dry run
220
+ const wire = await integrations.runConnectorStep(collectionId, appId, 'serialize', { documents }) // dry run
221
+ ```
222
+
223
+ `send` and `fetch` only run inside a flow. A flow run's summary carries `sent` (outbound) or
224
+ `created` / `updated` / `skipped` (inbound), and every run, inbound or outbound, is in the run history.
225
+
226
+ ---
227
+
228
+ ## Panels
229
+
230
+ A connector can contribute screens to the integrations console (an API explorer, say). Each
231
+ `panels[]` entry names a component exported from the app's widgets bundle; the console mounts it
232
+ host-side and passes it the collection and the installation. Panels are admin-only and call the
233
+ connector's own functions.
234
+
235
+ ---
236
+
237
+ ## Who can build connectors
238
+
239
+ First-party connectors run in-process, like any app server function. Third-party connectors will run
240
+ in the isolated runner (see [server-functions.md](server-functions.md)) once that opens up; the contract
241
+ does not change.
@@ -15,6 +15,10 @@ Flows are triggered three ways, all converging on the same executor:
15
15
  automatically when that entity changes.
16
16
  - **schedule** — a flow carrying a cron/interval `schedule` is run by the scan job. *(next)*
17
17
 
18
+ Provider-specific logic (a provider's payload format, its API, its connect form) lives in a
19
+ **connector app**, not in the engine: see **[Connectors](connectors.md)**. A flow uses one with
20
+ `config.connector = { appId }`.
21
+
18
22
  Credentials are **never** stored on the flow. The connection holds an opaque
19
23
  `credentialRef` into the **sealed-secret store** (`secrets` namespace); the value is sealed
20
24
  at rest and resolved server-side only, at execution.
@@ -94,6 +94,7 @@ The SmartLinks SDK (`@proveanything/smartlinks`) includes comprehensive document
94
94
  | **Lots** | `docs/lots.md` | Cross-SKU production "Lot" entity |
95
95
  | **Loyalty** | `docs/loyalty.md` | Points, members, earning rules |
96
96
  | **Integrations** | `docs/integrations.md` | Inbound/outbound external-system integration flows |
97
+ | **Connectors** | `docs/connectors.md` | Connector apps: the manifest `connector` block + the functions the integration engine calls |
97
98
  | **Item Context** | `docs/item-context.md` | The `itemContext` container prop (serial / NFC authenticity context) |
98
99
  | **Native Facade** | `docs/native-facade.md` | `host.native` / `SL.native` device-capability facade |
99
100
  | **Product Facets** | `docs/PRODUCT_FACETS_SDK.md` | Facet API reference (admin + public endpoints, types) |