@proveanything/smartlinks 2.1.14 → 2.1.16

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.
@@ -92,6 +92,7 @@ Types: `AppConnectorDeclaration` and friends in `@proveanything/smartlinks` (`sr
92
92
  | `aiGuide` | A guide the mapping assistant reads: the provider's quirks, required fields, good defaults. |
93
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
94
  | `limits` | `{ requestsPerSecond }` toward the provider, enforced across every SmartLinks worker; `{ maxEvents, maxAttributes }` when the provider caps registered events — a flow's `eventMapping` is checked against them when saved. |
95
+ | `actions[]` | Provider operations invoked on demand (redeem, enrol, issue…): name, title, function, `visibility`, input schema. See [Actions](#actions-provider-operations-on-demand). |
95
96
 
96
97
  ---
97
98
 
@@ -219,6 +220,61 @@ A connector should ship an `eventMapping` **preset** with the provider's standar
219
220
 
220
221
  ---
221
222
 
223
+ ## Actions: provider operations on demand
224
+
225
+ Steps are called by the engine. **Actions** are called when someone asks: redeem a reward, enrol a
226
+ member, issue a coupon, raise a ticket, send a receipt, check a balance. A connector declares them in
227
+ the manifest and implements each as a server function; the platform decides who may call them and
228
+ runs them with the connector's credentials, telling the function who asked.
229
+
230
+ ```jsonc
231
+ "connector": {
232
+ "actions": [
233
+ { "name": "redeem", "title": "Redeem a reward", "function": "redeemReward", "visibility": "member",
234
+ "input": { "type": "object", "required": ["rewardId"], "properties": { "rewardId": { "type": "string" } } } },
235
+ { "name": "enrol", "title": "Join the programme", "function": "enrolMember", "visibility": "member" },
236
+ { "name": "coupon-status", "title": "Coupon status", "function": "couponStatus", "visibility": "public" },
237
+ { "name": "adjust-points", "title": "Adjust points", "function": "adjustPoints" } // admin only (the default)
238
+ ]
239
+ }
240
+ ```
241
+
242
+ | `visibility` | Who may invoke it |
243
+ |---|---|
244
+ | `admin` (default) | the console and admin API; any app function |
245
+ | `member` | also a signed-in person through the Hub / a site |
246
+ | `public` | also anyone |
247
+
248
+ The function receives `event.body = { input, installation, caller: { userId, contactId, role, surface } }`
249
+ and returns JSON (or a `Response`). `input.required` fields from the declared schema are checked before
250
+ the function runs (400 with `missing[]`); fuller validation is the connector's job.
251
+
252
+ ```ts
253
+ export async function redeemReward(ctx: any, event: any) {
254
+ const { input, caller } = event.body
255
+ if (!caller.contactId) return new Response(JSON.stringify({ error: 'no_contact' }), { status: 400 })
256
+ const memberId = (await ctx.sl.contacts.forCaller())?.customFields?.loyaltyMemberId // written by Profile
257
+ const res = await ctx.fetch(`${api}/members/${memberId}/redemptions`, { method: 'POST', headers: auth(await ctx.secrets.get('apiKey')), body: JSON.stringify({ reward: input.rewardId }) })
258
+ return res.json()
259
+ }
260
+ ```
261
+
262
+ Calling an action:
263
+
264
+ ```ts
265
+ // From the Hub or a site (visibility public / member):
266
+ const { actions } = await integrations.listConnectorActions(collectionId, 'acme-loyalty')
267
+ const out = await integrations.callConnectorAction(collectionId, 'acme-loyalty', 'redeem', { rewardId: 'R-5' })
268
+
269
+ // From another app's server function (any visibility; needs sl:connectors:write):
270
+ const out = await ctx.sl.connectors.call('acme-loyalty', 'redeem', { rewardId: 'R-5' })
271
+
272
+ // From the console (any visibility):
273
+ await integrations.runConnectorAction(collectionId, 'acme-loyalty', 'adjust-points', { memberId, delta: 100 })
274
+ ```
275
+
276
+ ---
277
+
222
278
  ## Where configuration lives
223
279
 
224
280
  | What | Where |
@@ -118,6 +118,7 @@ mapped with the flow's field mappings (or handed to a connector's `serialize`):
118
118
 
119
119
  ```jsonc
120
120
  {
121
+ "specVersion": 1, // bumped only when a consumer could break
121
122
  "id": "0b2b…", // the event's id — stable across retries; use it as the destination's idempotency / transaction id
122
123
  "type": "interaction.submitted:quiz-1",
123
124
  "entityType": "interaction", "entityId": "0b2b…",
@@ -126,12 +127,14 @@ mapped with the flow's field mappings (or handed to a connector's `serialize`):
126
127
  "identity": {
127
128
  "contactId": "…", "userId": "…",
128
129
  "externalIds": { "acme-loyalty": "M-9", "crm": "C-1" }, // ids other systems know this person by
130
+ "anonymousId": "v-77", "sessionId": "s-1", // the visitor / device and session, before anyone signed in (every policy)
129
131
  "emailHash": "…", "phoneHash": "…", "hashAlgorithm": "hmac-sha256" // policy 'hashed' (default)
130
132
  // "email", "phone": only under policy 'raw'
131
133
  },
132
134
  "identityPolicy": "hashed",
133
135
  "product": { "id": "P1", "name": "Widget", "sku": "W-1", "gtin": "0500…", "variantId": null, "batchId": null },
134
136
  "proof": { "id": "…" },
137
+ "consent": { "channels": { "email": "opted_in", "sms": "suppressed", "push": "unknown", "whatsapp": "unknown" }, "topics": { "offers": true }, "updatedAt": "…" }, // null when about nobody we hold
135
138
  "subject": { "interactionId": "quiz-1", "outcome": "completed", "metadata": { "score": 9 }, "source": "hub" },
136
139
  "event": { "name": "engagement_completed", "attributes": { "kind": "quiz", "score": 9 }, "rule": "quiz" }, // from eventMapping
137
140
  "attributes": {}
@@ -142,6 +145,11 @@ Field mappings read envelope paths (`identity.externalIds.acme-loyalty`, `subjec
142
145
  `event.name`, `product.sku`). With no mappings the envelope itself is sent. A failed event delivery
143
146
  keeps its envelope and is retried from it; a product is re-read instead.
144
147
 
148
+ **CloudEvents.** `config.envelopeFormat: 'cloudevents'` wraps each delivered body (mapped or not) as a
149
+ CloudEvents 1.0 structured event — `{ specversion: '1.0', id, type, source: 'urn:smartlinks:collection:<id>',
150
+ subject, time, datacontenttype, dataschema: 'urn:smartlinks:envelope:v1', data }` — the shape every event
151
+ bus, lake and serverless tool already consumes.
152
+
145
153
  ```ts
146
154
  const flow = await integrations.createFlow(collectionId, {
147
155
  appId: 'core-integrations', direction: 'outbound', name: 'Engagement → loyalty', status: 'active',
@@ -518,6 +518,7 @@ Prefer the platform primitives above over a dependency; when you do need one, pi
518
518
  | Put a visitor's details on a contact | `ctx.sl.contacts.upsert({ email, firstName, … })` → `{ contactId, created }` (fills empty fields only) | `sl:contacts:write` |
519
519
  | Record "they did X" | `ctx.sl.interactions.record({ interactionId, contactId, outcome, metadata })` | `sl:interactions:write` |
520
520
  | Land a loyalty / CRM provider's view of a person (member id, tier, points, tags, external ids) | `ctx.sl.members.apply({ identity, contact?, loyalty?, tags?, externalIds? }, { provider })` → `{ contactId, op }` — matches by id, external id, email or phone; profile fields fill-empty only; see [connectors.md](connectors.md#loyalty-and-crm-connectors) | `sl:members:write` |
521
+ | Invoke a connector's action — redeem, enrol, issue a coupon, raise a ticket — with the connector's credentials | `ctx.sl.connectors.call('acme-loyalty', 'redeem', { rewardId })` → the action's result; `ctx.sl.connectors.actions(appId)` lists them; see [connectors.md](connectors.md#actions-provider-operations-on-demand) | `sl:connectors:write` |
521
522
  | Ask "is this person gold or above?" without holding their member record | `ctx.sl.entitlements.check('tier>=gold')` → `{ allowed, basis, asOf, attributes }` for the signed-in caller (or `{ identity, token, fallback }`); see [entitlements.md](entitlements.md) | `sl:entitlements:read` |
522
523
  | Take a booking with your own checks (hours, queue token, payment) | `ctx.sl.bookings.create({ slotId \| resource + time, contactId, status, idempotencyKey })` — never oversold, never double-booked; also `confirm` / `cancel` / `move` / `mine` / `freebusy` / `message` (everyone booked, hydrated) / `slots.*` / `blocks.*` (see [bookings.md](bookings.md)) | `sl:bookings:write` (`sl:bookings:read` to read) |
523
524
 
@@ -55,6 +55,27 @@ tag without a chip, with its URL and key; write it, then `ranges.recordTag(scope
55
55
 
56
56
  To find where a chip or code went, `partner.lookupTags(partner.id, { uid })` or `{ code }` searches every set the partner owns and returns each hit's set, tag and range, never a key.
57
57
 
58
+ ## Open ranges and groups
59
+
60
+ A range's count is fixed when it is minted. Two phone-side jobs don't know their count in advance, so a range
61
+ has a `kind`:
62
+
63
+ - **`open`**: blank tags programmed on Sidekick one at a time. `ranges.create(scope, set.id, { kind: 'open', name })`
64
+ reserves only a start index; each `ranges.next()` hands back a free minted code or mints one more, so
65
+ `quantity`/`endIndex` grow as tags are written (`remaining` is null). One open range per set, and nothing can
66
+ be minted behind it (`409 RANGE_OPEN`) until `ranges.close(scope, set.id, range.id)` turns it into an ordinary
67
+ range (an open range that minted nothing is removed instead).
68
+ - **`group`**: tags that already exist, added by tapping, assigned by hand, and moved together later. Assigners may
69
+ make one: `ranges.create(scope, set.id, { kind: 'group', name, intendedCollectionId? })`. Each tap goes through
70
+ `ranges.addMember(scope, set.id, group.id, { code | uid, collectionId, productId? })`, which stamps the tag with
71
+ the group and assigns it with the single-tag rules; `assigned.skipped` says when a tag was left alone (claimed,
72
+ or in a collection you don't administer). `ranges.assign(scope, set.id, group.id, { collectionId })` moves every
73
+ member in one go; `ranges.removeMember` takes one out. Groups have no printer file, instructions, import or
74
+ `next` (`400 NOT_A_RANGE`). Its tags must be codes of the group's set; a chip that is a code elsewhere is
75
+ `409 OTHER_SET`.
76
+
77
+ Status reads `open` or `group` for these (and `assigned` once every member of a group is assigned).
78
+
58
79
  ## The printer file
59
80
 
60
81
  One row per tag, in index order. The manufacturer fills in `UID` and sends the same file back; rows are
package/openapi.yaml CHANGED
@@ -14952,6 +14952,38 @@ paths:
14952
14952
  description: Unauthorized
14953
14953
  404:
14954
14954
  description: Not found
14955
+ /{groupId}/members:
14956
+ post:
14957
+ tags:
14958
+ - ranges
14959
+ summary: Put an existing tag of the set into a group (by code or chip UID), assigning it if a collection is given.
14960
+ operationId: ranges_addMember
14961
+ security: []
14962
+ parameters:
14963
+ - name: groupId
14964
+ in: path
14965
+ required: true
14966
+ schema:
14967
+ type: string
14968
+ responses:
14969
+ 200:
14970
+ description: Success
14971
+ content:
14972
+ application/json:
14973
+ schema:
14974
+ $ref: "#/components/schemas/AddClaimSetRangeMemberResponse"
14975
+ 400:
14976
+ description: Bad request
14977
+ 401:
14978
+ description: Unauthorized
14979
+ 404:
14980
+ description: Not found
14981
+ requestBody:
14982
+ required: true
14983
+ content:
14984
+ application/json:
14985
+ schema:
14986
+ $ref: "#/components/schemas/AddClaimSetRangeMemberRequest"
14955
14987
  /{rangeId}:
14956
14988
  get:
14957
14989
  tags:
@@ -15041,6 +15073,38 @@ paths:
15041
15073
  application/json:
15042
15074
  schema:
15043
15075
  $ref: "#/components/schemas/AssignClaimSetRangeRequest"
15076
+ /{rangeId}/close:
15077
+ post:
15078
+ tags:
15079
+ - ranges
15080
+ summary: "Close an open range: it becomes a range with the quantity it reached (removed if nothing was minted)."
15081
+ operationId: ranges_close
15082
+ security: []
15083
+ parameters:
15084
+ - name: rangeId
15085
+ in: path
15086
+ required: true
15087
+ schema:
15088
+ type: string
15089
+ responses:
15090
+ 200:
15091
+ description: Success
15092
+ content:
15093
+ application/json:
15094
+ schema:
15095
+ $ref: "#/components/schemas/CloseClaimSetRangeResponse"
15096
+ 400:
15097
+ description: Bad request
15098
+ 401:
15099
+ description: Unauthorized
15100
+ 404:
15101
+ description: Not found
15102
+ requestBody:
15103
+ required: true
15104
+ content:
15105
+ application/json:
15106
+ schema:
15107
+ $ref: "#/components/schemas/ClaimSetScope"
15044
15108
  /{rangeId}/export:
15045
15109
  get:
15046
15110
  tags:
@@ -21235,6 +21299,32 @@ components:
21235
21299
  - id
21236
21300
  - title
21237
21301
  - component
21302
+ ConnectorAction:
21303
+ type: object
21304
+ properties:
21305
+ name:
21306
+ type: string
21307
+ title:
21308
+ type: string
21309
+ description:
21310
+ type: string
21311
+ function:
21312
+ type: string
21313
+ visibility:
21314
+ type: string
21315
+ enum:
21316
+ - admin
21317
+ - member
21318
+ - public
21319
+ input:
21320
+ type: object
21321
+ additionalProperties: true
21322
+ output:
21323
+ type: object
21324
+ additionalProperties: true
21325
+ required:
21326
+ - name
21327
+ - function
21238
21328
  AppConnectorDeclaration:
21239
21329
  type: object
21240
21330
  properties:
@@ -21288,6 +21378,10 @@ components:
21288
21378
  type: array
21289
21379
  items:
21290
21380
  $ref: "#/components/schemas/ConnectorPanel"
21381
+ actions:
21382
+ type: array
21383
+ items:
21384
+ $ref: "#/components/schemas/ConnectorAction"
21291
21385
  aiGuide:
21292
21386
  type: string
21293
21387
  recordId:
@@ -25889,6 +25983,8 @@ components:
25889
25983
  type: string
25890
25984
  namespace:
25891
25985
  type: string
25986
+ kind:
25987
+ $ref: "#/components/schemas/ClaimSetRangeKind"
25892
25988
  name:
25893
25989
  type: string
25894
25990
  notes:
@@ -25942,6 +26038,7 @@ components:
25942
26038
  - id
25943
26039
  - claimSetId
25944
26040
  - namespace
26041
+ - kind
25945
26042
  - name
25946
26043
  - notes
25947
26044
  - startIndex
@@ -25965,6 +26062,8 @@ components:
25965
26062
  CreateClaimSetRangeRequest:
25966
26063
  type: object
25967
26064
  properties:
26065
+ kind:
26066
+ $ref: "#/components/schemas/ClaimSetRangeKind"
25968
26067
  name:
25969
26068
  type: string
25970
26069
  quantity:
@@ -25988,7 +26087,6 @@ components:
25988
26087
  type: string
25989
26088
  required:
25990
26089
  - name
25991
- - quantity
25992
26090
  UpdateClaimSetRangeRequest:
25993
26091
  type: object
25994
26092
  properties:
@@ -26032,6 +26130,10 @@ components:
26032
26130
  type: string
26033
26131
  proofId:
26034
26132
  type: string
26133
+ rangeId:
26134
+ type: string
26135
+ groupId:
26136
+ type: string
26035
26137
  programmedAt:
26036
26138
  type: string
26037
26139
  scans:
@@ -26045,6 +26147,8 @@ components:
26045
26147
  - productId
26046
26148
  - batchId
26047
26149
  - proofId
26150
+ - rangeId
26151
+ - groupId
26048
26152
  - programmedAt
26049
26153
  - scans
26050
26154
  ClaimSetRangeTagsPage:
@@ -26073,6 +26177,41 @@ components:
26073
26177
  required:
26074
26178
  - url
26075
26179
  - remaining
26180
+ AddClaimSetRangeMemberRequest:
26181
+ type: object
26182
+ properties:
26183
+ code:
26184
+ type: string
26185
+ uid:
26186
+ type: string
26187
+ collectionId:
26188
+ type: string
26189
+ productId:
26190
+ type: string
26191
+ batchId:
26192
+ type: string
26193
+ data:
26194
+ type: object
26195
+ additionalProperties: true
26196
+ clear:
26197
+ type: boolean
26198
+ AddClaimSetRangeMemberResponse:
26199
+ type: object
26200
+ properties:
26201
+ tag:
26202
+ $ref: "#/components/schemas/ClaimSetRangeTag"
26203
+ range:
26204
+ $ref: "#/components/schemas/ClaimSetRange"
26205
+ assigned:
26206
+ type: object
26207
+ additionalProperties: true
26208
+ movedFrom:
26209
+ type: string
26210
+ required:
26211
+ - tag
26212
+ - range
26213
+ - assigned
26214
+ - movedFrom
26076
26215
  ImportClaimSetRangeUidsRequest:
26077
26216
  type: object
26078
26217
  properties:
@@ -26273,6 +26412,12 @@ components:
26273
26412
  - ntag424
26274
26413
  - ntag21x
26275
26414
  - qr
26415
+ ClaimSetRangeKind:
26416
+ type: string
26417
+ enum:
26418
+ - range
26419
+ - open
26420
+ - group
26276
26421
  ClaimSetRangeStatus:
26277
26422
  type: string
26278
26423
  enum:
@@ -26281,6 +26426,11 @@ components:
26281
26426
  - sent
26282
26427
  - received
26283
26428
  - assigned
26429
+ - open
26430
+ - group
26431
+ CloseClaimSetRangeResponse:
26432
+ type: object
26433
+ additionalProperties: true
26284
26434
  ClaimSetImportResult:
26285
26435
  type: string
26286
26436
  enum:
@@ -29060,6 +29210,10 @@ components:
29060
29210
  additionalProperties: true
29061
29211
  eventMapping:
29062
29212
  $ref: "#/components/schemas/EventMapping"
29213
+ envelopeFormat:
29214
+ type: string
29215
+ enum:
29216
+ - cloudevents
29063
29217
  connection:
29064
29218
  $ref: "#/components/schemas/FlowConnection"
29065
29219
  fieldMappings:
@@ -29520,6 +29674,8 @@ components:
29520
29674
  EventEnvelope:
29521
29675
  type: object
29522
29676
  properties:
29677
+ specVersion:
29678
+ type: number
29523
29679
  id:
29524
29680
  type: string
29525
29681
  type:
@@ -29547,6 +29703,10 @@ components:
29547
29703
  type: object
29548
29704
  additionalProperties:
29549
29705
  type: string
29706
+ anonymousId:
29707
+ type: string
29708
+ sessionId:
29709
+ type: string
29550
29710
  emailHash:
29551
29711
  type: string
29552
29712
  phoneHash:
@@ -29567,6 +29727,9 @@ components:
29567
29727
  proof:
29568
29728
  type: object
29569
29729
  additionalProperties: true
29730
+ consent:
29731
+ type: object
29732
+ additionalProperties: true
29570
29733
  subject:
29571
29734
  type: object
29572
29735
  additionalProperties: true
@@ -29577,6 +29740,7 @@ components:
29577
29740
  type: object
29578
29741
  additionalProperties: true
29579
29742
  required:
29743
+ - specVersion
29580
29744
  - id
29581
29745
  - type
29582
29746
  - entityType
@@ -29589,9 +29753,12 @@ components:
29589
29753
  - contactId
29590
29754
  - userId
29591
29755
  - externalIds
29756
+ - anonymousId
29757
+ - sessionId
29592
29758
  - identityPolicy
29593
29759
  - product
29594
29760
  - proof
29761
+ - consent
29595
29762
  - subject
29596
29763
  - attributes
29597
29764
  CapturedRequest:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "2.1.14",
3
+ "version": "2.1.16",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",