@proveanything/smartlinks 2.1.13 → 2.1.15

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.
@@ -302,6 +302,23 @@ A loyalty failure never causes the interaction event to fail — errors are caug
302
302
 
303
303
  ---
304
304
 
305
+ ## External loyalty platforms
306
+
307
+ The schemes above are SmartLinks' own points. When a brand runs its programme on an external loyalty or
308
+ engagement platform, the platform stays the record of truth and SmartLinks connects to it through a
309
+ **connector app** and the integration engine — no member base is mirrored here:
310
+
311
+ | Flow | How |
312
+ |---|---|
313
+ | **Earn** — what happens on a pack or in a Hub awards points there | an outbound flow with `sourceEntity: 'interaction' \| 'event'`, an `eventMapping` onto the provider's registered events, signed delivery, retries — [integrations.md](integrations.md#event-sourced-flows-and-the-envelope) |
314
+ | **Profile** — member id, tier and points on the contact | the connector's webhook function calls `ctx.sl.members.apply(...)`; the contact gets `customFields.loyalty` and flat `loyaltyTier / loyaltyPoints / loyaltyMemberId`, usable in segments and comms targeting — [connectors.md](connectors.md#loyalty-and-crm-connectors) |
315
+ | **Gate / display** — "Gold members only", live balance | the entitlement check, answered on demand from a signed claim, the connector's cached `lookup`, or the contact's fields — [entitlements.md](entitlements.md) |
316
+
317
+ A collection runs one loyalty source: the native schemes here, or an external provider through a
318
+ connector.
319
+
320
+ ---
321
+
305
322
  ## TypeScript Types
306
323
 
307
324
  ```typescript
@@ -517,6 +517,9 @@ Prefer the platform primitives above over a dependency; when you do need one, pi
517
517
  | Check a tapped tag is real | `ctx.sl.tags.resolve(proofId)` → `{ id, claimSetId, code }` or `null` | `sl:tags:read` |
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
+ | 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` |
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` |
520
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) |
521
524
 
522
525
  Notes:
package/dist/openapi.yaml CHANGED
@@ -21235,6 +21235,32 @@ components:
21235
21235
  - id
21236
21236
  - title
21237
21237
  - component
21238
+ ConnectorAction:
21239
+ type: object
21240
+ properties:
21241
+ name:
21242
+ type: string
21243
+ title:
21244
+ type: string
21245
+ description:
21246
+ type: string
21247
+ function:
21248
+ type: string
21249
+ visibility:
21250
+ type: string
21251
+ enum:
21252
+ - admin
21253
+ - member
21254
+ - public
21255
+ input:
21256
+ type: object
21257
+ additionalProperties: true
21258
+ output:
21259
+ type: object
21260
+ additionalProperties: true
21261
+ required:
21262
+ - name
21263
+ - function
21238
21264
  AppConnectorDeclaration:
21239
21265
  type: object
21240
21266
  properties:
@@ -21280,10 +21306,18 @@ components:
21280
21306
  type: string
21281
21307
  fetch:
21282
21308
  type: string
21309
+ describeEvents:
21310
+ type: string
21311
+ lookup:
21312
+ type: string
21283
21313
  panels:
21284
21314
  type: array
21285
21315
  items:
21286
21316
  $ref: "#/components/schemas/ConnectorPanel"
21317
+ actions:
21318
+ type: array
21319
+ items:
21320
+ $ref: "#/components/schemas/ConnectorAction"
21287
21321
  aiGuide:
21288
21322
  type: string
21289
21323
  recordId:
@@ -29014,9 +29048,52 @@ components:
29014
29048
  additionalProperties: true
29015
29049
  required:
29016
29050
  - appId
29051
+ EventMappingRule:
29052
+ type: object
29053
+ properties:
29054
+ id:
29055
+ type: string
29056
+ match:
29057
+ type: object
29058
+ additionalProperties: true
29059
+ event:
29060
+ type: string
29061
+ attributes:
29062
+ type: object
29063
+ additionalProperties:
29064
+ type: object
29065
+ additionalProperties: true
29066
+ required:
29067
+ - event
29068
+ EventMapping:
29069
+ type: object
29070
+ properties:
29071
+ rules:
29072
+ type: array
29073
+ items:
29074
+ $ref: "#/components/schemas/EventMappingRule"
29075
+ unmatched:
29076
+ type: string
29077
+ enum:
29078
+ - skip
29079
+ - passthrough
29080
+ required:
29081
+ - rules
29017
29082
  IntegrationFlowConfig:
29018
29083
  type: object
29019
29084
  properties:
29085
+ signing:
29086
+ type: object
29087
+ additionalProperties: true
29088
+ identity:
29089
+ type: object
29090
+ additionalProperties: true
29091
+ eventMapping:
29092
+ $ref: "#/components/schemas/EventMapping"
29093
+ envelopeFormat:
29094
+ type: string
29095
+ enum:
29096
+ - cloudevents
29020
29097
  connection:
29021
29098
  $ref: "#/components/schemas/FlowConnection"
29022
29099
  fieldMappings:
@@ -29049,9 +29126,11 @@ components:
29049
29126
  schedule:
29050
29127
  type: string
29051
29128
  sourceEntity:
29052
- type: string
29129
+ type: object
29130
+ additionalProperties: true
29053
29131
  targetEntity:
29054
- type: string
29132
+ type: object
29133
+ additionalProperties: true
29055
29134
  config:
29056
29135
  $ref: "#/components/schemas/IntegrationFlowConfig"
29057
29136
  createdBy:
@@ -29424,6 +29503,144 @@ components:
29424
29503
  settings:
29425
29504
  type: object
29426
29505
  additionalProperties: true
29506
+ ConnectorEventsSchema:
29507
+ type: object
29508
+ properties:
29509
+ events:
29510
+ type: array
29511
+ items:
29512
+ type: object
29513
+ additionalProperties: true
29514
+ limits:
29515
+ type: object
29516
+ additionalProperties: true
29517
+ required:
29518
+ - events
29519
+ ConnectorLookupInput:
29520
+ type: object
29521
+ properties:
29522
+ installation:
29523
+ $ref: "#/components/schemas/ConnectorInstallation"
29524
+ identity:
29525
+ type: object
29526
+ additionalProperties: true
29527
+ want:
29528
+ type: array
29529
+ items:
29530
+ type: string
29531
+ required:
29532
+ - installation
29533
+ - identity
29534
+ ConnectorLookupResult:
29535
+ type: object
29536
+ properties:
29537
+ found:
29538
+ type: boolean
29539
+ memberId:
29540
+ type: string
29541
+ tier:
29542
+ type: string
29543
+ points:
29544
+ type: number
29545
+ status:
29546
+ type: string
29547
+ attributes:
29548
+ type: object
29549
+ additionalProperties: true
29550
+ asOf:
29551
+ type: string
29552
+ required:
29553
+ - found
29554
+ EventEnvelope:
29555
+ type: object
29556
+ properties:
29557
+ specVersion:
29558
+ type: number
29559
+ id:
29560
+ type: string
29561
+ type:
29562
+ type: string
29563
+ entityType:
29564
+ type: string
29565
+ entityId:
29566
+ type: string
29567
+ occurredAt:
29568
+ type: string
29569
+ collectionId:
29570
+ type: string
29571
+ orgId:
29572
+ type: string
29573
+ appId:
29574
+ type: string
29575
+ identity:
29576
+ type: object
29577
+ additionalProperties: true
29578
+ contactId:
29579
+ type: string
29580
+ userId:
29581
+ type: string
29582
+ externalIds:
29583
+ type: object
29584
+ additionalProperties:
29585
+ type: string
29586
+ anonymousId:
29587
+ type: string
29588
+ sessionId:
29589
+ type: string
29590
+ emailHash:
29591
+ type: string
29592
+ phoneHash:
29593
+ type: string
29594
+ hashAlgorithm:
29595
+ type: string
29596
+ enum:
29597
+ - hmac-sha256
29598
+ email:
29599
+ type: string
29600
+ phone:
29601
+ type: string
29602
+ identityPolicy:
29603
+ $ref: "#/components/schemas/IdentityPolicy"
29604
+ product:
29605
+ type: object
29606
+ additionalProperties: true
29607
+ proof:
29608
+ type: object
29609
+ additionalProperties: true
29610
+ consent:
29611
+ type: object
29612
+ additionalProperties: true
29613
+ subject:
29614
+ type: object
29615
+ additionalProperties: true
29616
+ event:
29617
+ type: object
29618
+ additionalProperties: true
29619
+ attributes:
29620
+ type: object
29621
+ additionalProperties: true
29622
+ required:
29623
+ - specVersion
29624
+ - id
29625
+ - type
29626
+ - entityType
29627
+ - entityId
29628
+ - occurredAt
29629
+ - collectionId
29630
+ - orgId
29631
+ - appId
29632
+ - identity
29633
+ - contactId
29634
+ - userId
29635
+ - externalIds
29636
+ - anonymousId
29637
+ - sessionId
29638
+ - identityPolicy
29639
+ - product
29640
+ - proof
29641
+ - consent
29642
+ - subject
29643
+ - attributes
29427
29644
  CapturedRequest:
29428
29645
  type: object
29429
29646
  properties:
@@ -29763,6 +29980,24 @@ components:
29763
29980
  - webhook
29764
29981
  - oauth2
29765
29982
  - none
29983
+ IdentityPolicy:
29984
+ type: string
29985
+ enum:
29986
+ - externalIds
29987
+ - hashed
29988
+ - raw
29989
+ FlowSourceEntity:
29990
+ type: string
29991
+ enum:
29992
+ - product
29993
+ - interaction
29994
+ - contact
29995
+ - event
29996
+ FlowTargetEntity:
29997
+ type: string
29998
+ enum:
29999
+ - product
30000
+ - contact
29766
30001
  UpdateFlowInput:
29767
30002
  $ref: "#/components/schemas/CreateFlowInput"
29768
30003
  ConnectorConsoleStep:
@@ -29773,6 +30008,8 @@ components:
29773
30008
  - sample
29774
30009
  - shape
29775
30010
  - serialize
30011
+ - describeEvents
30012
+ - lookup
29776
30013
  RunTrigger:
29777
30014
  type: string
29778
30015
  enum:
@@ -1030,6 +1030,27 @@ export interface ConnectorPanel {
1030
1030
  /** Exported component name in the app's widgets bundle (manifest `widgets.files`). */
1031
1031
  component: string;
1032
1032
  }
1033
+ /**
1034
+ * A named provider operation invoked ON DEMAND (not by the engine): redeem a reward, enrol a member,
1035
+ * issue a coupon, raise a ticket, send a receipt. Called by an app function (`ctx.sl.connectors.call`),
1036
+ * by the Hub (public route — only `public`, or `member` for a signed-in person) or by the console. The
1037
+ * function receives `{ input, installation, caller }` and returns JSON; it runs with the connector's
1038
+ * own credentials and is told who asked.
1039
+ */
1040
+ export interface ConnectorAction {
1041
+ /** `^[a-zA-Z][a-zA-Z0-9_-]{0,63}$` */
1042
+ name: string;
1043
+ title?: string;
1044
+ description?: string;
1045
+ /** This app's server function implementing it. */
1046
+ function: string;
1047
+ /** Who may invoke it outside functions and the console. Default 'admin'. */
1048
+ visibility?: 'admin' | 'member' | 'public';
1049
+ /** JSON Schema for `input`; `required` fields are checked before the function runs. */
1050
+ input?: Record<string, unknown>;
1051
+ /** JSON Schema for the result (documentation; not enforced). */
1052
+ output?: Record<string, unknown>;
1053
+ }
1033
1054
  /**
1034
1055
  * The connector contract. Every function named here is one of this app's server functions
1035
1056
  * (manifest `functions.definitions`); the engine calls it with collection authority, capped by the
@@ -1078,8 +1099,22 @@ export interface AppConnectorDeclaration {
1078
1099
  send?: string;
1079
1100
  /** (ConnectorFetchInput) → ConnectorRecords: inbound pull, one page per call. */
1080
1101
  fetch?: string;
1102
+ /**
1103
+ * Loyalty / engagement providers: → ConnectorEventsSchema, the event names (and attributes) the
1104
+ * provider has registered for this installation. A flow's eventMapping is validated against it when
1105
+ * saved, so a mapping that names an unregistered event or breaks a limit is refused with the reason.
1106
+ */
1107
+ describeEvents?: string;
1108
+ /**
1109
+ * (ConnectorLookupInput) → ConnectorLookupResult: one identity → member id, tier, points, status.
1110
+ * Called by the entitlement check (see docs/entitlements.md) through a cache keyed on an identity
1111
+ * hash, for the TTL the collection sets — one call per active person per TTL, never a sync.
1112
+ */
1113
+ lookup?: string;
1081
1114
  };
1082
1115
  panels?: ConnectorPanel[];
1116
+ /** On-demand operations the connector offers (see ConnectorAction). */
1117
+ actions?: ConnectorAction[];
1083
1118
  /** Path (under the app's public files) of a guide the AI mapping assistant reads. */
1084
1119
  aiGuide?: string;
1085
1120
  /**
@@ -1088,9 +1123,15 @@ export interface AppConnectorDeclaration {
1088
1123
  * Default 'id'; a flow can override it with `config.upsert.externalIdPath`.
1089
1124
  */
1090
1125
  recordId?: string;
1091
- /** Requests per second toward the provider, shared by every SmartLinks worker. */
1126
+ /**
1127
+ * Requests per second toward the provider, shared by every SmartLinks worker; and, for providers that
1128
+ * cap registered events, how many distinct events / attributes per event they accept (a flow's
1129
+ * eventMapping is checked against them when saved).
1130
+ */
1092
1131
  limits?: {
1093
1132
  requestsPerSecond?: number;
1133
+ maxEvents?: number;
1134
+ maxAttributes?: number;
1094
1135
  };
1095
1136
  }
1096
1137
  /**
@@ -38,7 +38,66 @@ export interface FlowConnectorRef {
38
38
  /** Per-flow settings, layered over the installation's. */
39
39
  settings?: Record<string, unknown>;
40
40
  }
41
+ import type { ConnectorInstallation } from './appManifest.js';
42
+ /** What about a person an event-sourced flow lets leave. Default 'hashed'. */
43
+ export type IdentityPolicy = 'externalIds' | 'hashed' | 'raw';
44
+ /**
45
+ * Many SmartLinks events → the provider's few registered event names. Rules are tried in order; the
46
+ * first match names the provider event and renders its attributes (a literal, a Liquid template over
47
+ * the envelope, or `{ path, default }`). An unmatched event is skipped (recorded as such) unless
48
+ * `unmatched: 'passthrough'`. Validated against the connector's `describeEvents` when the flow is saved.
49
+ */
50
+ export interface EventMappingRule {
51
+ id?: string;
52
+ match?: {
53
+ types?: string[];
54
+ interactionIds?: string[];
55
+ outcomes?: string[];
56
+ appIds?: string[];
57
+ };
58
+ /** The provider's registered event name. */
59
+ event: string;
60
+ attributes?: Record<string, string | number | boolean | {
61
+ path: string;
62
+ default?: unknown;
63
+ } | {
64
+ value: unknown;
65
+ }>;
66
+ }
67
+ export interface EventMapping {
68
+ rules: EventMappingRule[];
69
+ unmatched?: 'skip' | 'passthrough';
70
+ }
71
+ /** What an outbound flow reads: the product that changed, or ONE spine event as the envelope. */
72
+ export type FlowSourceEntity = 'product' | 'interaction' | 'contact' | 'event';
73
+ /** What an inbound flow writes. */
74
+ export type FlowTargetEntity = 'product' | 'contact';
41
75
  export interface IntegrationFlowConfig {
76
+ /**
77
+ * Signed deliveries: X-SmartLinks-Signature `t=<unix>,v1=<hex HMAC-SHA256(secret, t + '.' + body)>`
78
+ * over the exact bytes sent. `previousCredentialRef` keeps the old secret signing during a rotation.
79
+ */
80
+ signing?: {
81
+ credentialRef: string;
82
+ previousCredentialRef?: string;
83
+ };
84
+ /**
85
+ * Event-sourced flows: what about the person leaves (default 'hashed': ids + HMAC hashes of email and
86
+ * phone, never the values). `hashKeyCredentialRef` names the brand's own hash key, so hashes match a
87
+ * mirror they built; otherwise a per-collection platform key is used.
88
+ */
89
+ identity?: {
90
+ policy?: IdentityPolicy;
91
+ hashKeyCredentialRef?: string;
92
+ };
93
+ /** Event-sourced flows: fold SmartLinks events into the provider's registered events. */
94
+ eventMapping?: EventMapping;
95
+ /**
96
+ * Wrap each delivered body as a CloudEvents 1.0 structured event
97
+ * ({ specversion, id, type, source, subject, time, datacontenttype, dataschema, data }) — the shape
98
+ * event buses, lakes and serverless tools consume without custom code.
99
+ */
100
+ envelopeFormat?: 'cloudevents';
42
101
  connection?: FlowConnection;
43
102
  fieldMappings?: FieldMapping[];
44
103
  /** Run this flow through a connector app's functions instead of the built-in mapper/transport. */
@@ -57,8 +116,10 @@ export interface IntegrationFlow {
57
116
  eventTypes: string[];
58
117
  /** Cron/interval string for scheduled flows, or null. */
59
118
  schedule: string | null;
60
- sourceEntity: string | null;
61
- targetEntity: string | null;
119
+ /** Outbound: what the flow reads (product | interaction | contact | event). */
120
+ sourceEntity: FlowSourceEntity | string | null;
121
+ /** Inbound: what the flow writes (product | contact). */
122
+ targetEntity: FlowTargetEntity | string | null;
62
123
  config: IntegrationFlowConfig;
63
124
  createdBy: string | null;
64
125
  createdAt: string;
@@ -267,7 +328,111 @@ export interface SaveConnectorInstallationInput {
267
328
  settings?: Record<string, unknown>;
268
329
  }
269
330
  /** Steps the console may run directly (send and fetch only run inside a flow; shape/serialize as dry runs). */
270
- export type ConnectorConsoleStep = 'testConnection' | 'describeTarget' | 'sample' | 'shape' | 'serialize';
331
+ export type ConnectorConsoleStep = 'testConnection' | 'describeTarget' | 'sample' | 'shape' | 'serialize' | 'describeEvents' | 'lookup';
332
+ /** `describeEvents` → what the provider has registered for this installation (loyalty / engagement). */
333
+ export interface ConnectorEventsSchema {
334
+ events: Array<{
335
+ name: string;
336
+ title?: string;
337
+ attributes?: Array<{
338
+ name: string;
339
+ type?: string;
340
+ required?: boolean;
341
+ }>;
342
+ }>;
343
+ limits?: {
344
+ maxEvents?: number;
345
+ maxAttributes?: number;
346
+ };
347
+ }
348
+ /** `lookup` input: one identity, as the collection's lookup policy sends it (raw, or hashed). */
349
+ export interface ConnectorLookupInput {
350
+ installation: ConnectorInstallation;
351
+ identity: {
352
+ externalId?: string | null;
353
+ email?: string | null;
354
+ phone?: string | null;
355
+ emailHash?: string | null;
356
+ phoneHash?: string | null;
357
+ contactId?: string | null;
358
+ };
359
+ /** Attribute names the gate wants, e.g. ['tier', 'points', 'status']. */
360
+ want?: string[];
361
+ }
362
+ /** `lookup` result. Throw (or return non-2xx) for an outage — never `found: false`. */
363
+ export interface ConnectorLookupResult {
364
+ found: boolean;
365
+ memberId?: string | null;
366
+ tier?: string | null;
367
+ points?: number | null;
368
+ status?: string | null;
369
+ attributes?: Record<string, unknown>;
370
+ /** When the provider's answer is from (ISO). */
371
+ asOf?: string | null;
372
+ }
373
+ /**
374
+ * The one shape a SmartLinks event has when it leaves the platform — what event-sourced flows hand a
375
+ * connector's `serialize` / `send`, after the flow's identity policy and event mapping.
376
+ */
377
+ export interface EventEnvelope {
378
+ /** Schema version of this envelope. Bumped only when a consumer could break (a field removed or changed). */
379
+ specVersion: number;
380
+ /** The event's id: stable across retries; use it as the provider's transaction / idempotency id. */
381
+ id: string;
382
+ /** Spine event name, e.g. 'interaction.submitted:<typeId>', 'contact.updated', 'tag.scanned:scan_redirect'. */
383
+ type: string;
384
+ entityType: string | null;
385
+ entityId: string | null;
386
+ occurredAt: string;
387
+ collectionId: string;
388
+ orgId: string;
389
+ appId: string | null;
390
+ identity: {
391
+ contactId: string | null;
392
+ userId: string | null;
393
+ /** Ids other systems know this person by, keyed by system (a connector's `provider`). */
394
+ externalIds: Record<string, string>;
395
+ /** The visitor / device id before anyone signed in (an analytics destination stitches on it later). Under every policy. */
396
+ anonymousId: string | null;
397
+ sessionId: string | null;
398
+ emailHash?: string | null;
399
+ phoneHash?: string | null;
400
+ hashAlgorithm?: 'hmac-sha256' | null;
401
+ /** Only under identity policy 'raw'. */
402
+ email?: string | null;
403
+ phone?: string | null;
404
+ };
405
+ identityPolicy: IdentityPolicy;
406
+ product: {
407
+ id: string;
408
+ name?: string | null;
409
+ sku?: string | null;
410
+ gtin?: string | null;
411
+ variantId?: string | null;
412
+ batchId?: string | null;
413
+ } | null;
414
+ proof: {
415
+ id: string;
416
+ } | null;
417
+ /**
418
+ * The person's communication consent, summarised per channel from the contact's comms state; null
419
+ * when the event is about nobody we hold. A CRM or CDP must not act against it.
420
+ */
421
+ consent: {
422
+ channels: Record<'email' | 'sms' | 'push' | 'whatsapp', 'opted_in' | 'opted_out' | 'suppressed' | 'unknown'>;
423
+ topics: Record<string, boolean>;
424
+ updatedAt: string | null;
425
+ } | null;
426
+ /** The event's own fields: an interaction's interactionId / outcome / metadata, a scan's codeId / country, a contact's profile. */
427
+ subject: Record<string, unknown>;
428
+ /** From the flow's event mapping, when it has one. */
429
+ event?: {
430
+ name: string;
431
+ attributes: Record<string, unknown>;
432
+ rule: string | null;
433
+ };
434
+ attributes: Record<string, unknown>;
435
+ }
271
436
  export type RunTrigger = 'manual' | 'event' | 'schedule' | 'test';
272
437
  export type FlowRunStatus = 'running' | 'success' | 'partial' | 'failed' | 'skipped';
273
438
  export type FlowRunItemStatus = 'sent' | 'failed' | 'skipped';