@proveanything/smartlinks 2.0.50 → 2.0.52

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.
@@ -30,6 +30,9 @@ export declare namespace sequence {
30
30
  * const { number, isNew } = await sequence.allocate(collectionId, {
31
31
  * appId: 'raffle-app', sequenceId: 'raffle', subjectId: claimSetId,
32
32
  * })
33
+ *
34
+ * @deprecated Being removed. Number things in your app's own server function with
35
+ * `ctx.sl.counters.next()` + `ctx.sl.appRecords.claim()` (docs/server-functions.md).
33
36
  */
34
37
  function allocate(collectionId: string, input: AllocateSequenceInput): Promise<AllocatedSequence>;
35
38
  }
@@ -18,6 +18,9 @@ export var sequence;
18
18
  * const { number, isNew } = await sequence.allocate(collectionId, {
19
19
  * appId: 'raffle-app', sequenceId: 'raffle', subjectId: claimSetId,
20
20
  * })
21
+ *
22
+ * @deprecated Being removed. Number things in your app's own server function with
23
+ * `ctx.sl.counters.next()` + `ctx.sl.appRecords.claim()` (docs/server-functions.md).
21
24
  */
22
25
  async function allocate(collectionId, input) {
23
26
  return post(`${base(collectionId)}/allocate`, input);
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.50 | Generated: 2026-10-09T10:28:38.661Z
3
+ Version: 2.0.52 | Generated: 2026-10-11T13:21:58.062Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -2600,10 +2600,73 @@ interface AppDataHandle {
2600
2600
  }
2601
2601
  ```
2602
2602
 
2603
+ **AppRecordClaimResult<T = any>** (interface)
2604
+ ```typescript
2605
+ interface AppRecordClaimResult<T = any> {
2606
+ record: T;
2607
+ created: boolean;
2608
+ conflict?: 'singleton' | 'customId';
2609
+ }
2610
+ ```
2611
+
2612
+ **ServerFunctionCounters** (interface)
2613
+ ```typescript
2614
+ interface ServerFunctionCounters {
2615
+ next(name: string, opts?: { start?: number; by?: number }): Promise<number>;
2616
+ get(name: string): Promise<number | null>;
2617
+ }
2618
+ ```
2619
+
2620
+ **ResolvedTag** (interface)
2621
+ ```typescript
2622
+ interface ResolvedTag {
2623
+ id: string;
2624
+ claimSetId: string;
2625
+ code: string;
2626
+ }
2627
+ ```
2628
+
2629
+ **ContactUpsertDetails** (interface)
2630
+ ```typescript
2631
+ interface ContactUpsertDetails {
2632
+ email?: string;
2633
+ phone?: string;
2634
+ firstName?: string;
2635
+ lastName?: string;
2636
+ displayName?: string;
2637
+ company?: string;
2638
+ locale?: string;
2639
+ timezone?: string;
2640
+ customFields?: Record<string, string | number | boolean>;
2641
+ }
2642
+ ```
2643
+
2644
+ **ServerFunctionInteractionEvent** (interface)
2645
+ ```typescript
2646
+ interface ServerFunctionInteractionEvent {
2647
+ interactionId: string;
2648
+ contactId?: string | null;
2649
+ outcome?: string;
2650
+ metadata?: Record<string, any>;
2651
+ productId?: string;
2652
+ proofId?: string;
2653
+ variantId?: string;
2654
+ batchId?: string;
2655
+ eventType?: string;
2656
+ }
2657
+ ```
2658
+
2603
2659
  **ServerFunctionSl** (interface)
2604
2660
  ```typescript
2605
2661
  interface ServerFunctionSl {
2606
- appRecords: any;
2662
+ appRecords: { claim(fields: Record<string, any>): Promise<AppRecordClaimResult>; [method: string]: any };
2663
+ counters: ServerFunctionCounters;
2664
+ tags: { resolve(id: string): Promise<ResolvedTag | null> };
2665
+ contacts: {
2666
+ upsert(details: ContactUpsertDetails): Promise<{ contactId: string; created: boolean }>;
2667
+ forCaller(): Promise<{ contactId: string } | null>;
2668
+ };
2669
+ interactions: { record(event: ServerFunctionInteractionEvent): Promise<{ eventId: string }> };
2607
2670
  products: any;
2608
2671
  attestations: any;
2609
2672
  appData: AppDataHandle & { global: AppDataHandle; collection: AppDataHandle };
@@ -5229,8 +5292,11 @@ interface UnsubscribeResponse {
5229
5292
  ```typescript
5230
5293
  interface CommsSignupRequest {
5231
5294
  email: string
5295
+ templateId: string
5232
5296
  topics?: string[]
5233
5297
  firstName?: string
5298
+ fields?: { firstName?: string; lastName?: string; displayName?: string; company?: string; locale?: string; timezone?: string; customFields?: Record<string, string | number | boolean> }
5299
+ appId?: string
5234
5300
  }
5235
5301
  ```
5236
5302
 
@@ -5597,9 +5663,23 @@ export interface UnsubscribeResponse { ok: true; applied?: { channels?: Record<s
5597
5663
  // Public sign-up (double opt-in)
5598
5664
  export interface CommsSignupRequest {
5599
5665
  email: string
5666
+ /**
5667
+ * Required: YOUR app's confirmation email — a template in this collection (SL.template) whose body
5668
+ * uses `{{ action_url }}` for the confirm link (`{{ topics }}` lists what they signed up for). The
5669
+ * platform sends no default sign-up email.
5670
+ */
5671
+ templateId: string
5600
5672
  /** Topic ids from the collection's comm settings (e.g. ["newsletter"]). */
5601
5673
  topics?: string[]
5602
5674
  firstName?: string
5675
+ /**
5676
+ * The form's other answers — firstName/lastName/displayName/company/locale/timezone and
5677
+ * `customFields`. Applied to the contact when the address is confirmed, and only where the contact
5678
+ * has no value yet (never overwritten). Small: ~2 KB.
5679
+ */
5680
+ fields?: { firstName?: string; lastName?: string; displayName?: string; company?: string; locale?: string; timezone?: string; customFields?: Record<string, string | number | boolean> }
5681
+ /** Your app id, recorded with the sent email. */
5682
+ appId?: string
5603
5683
  }
5604
5684
  export interface CommsSignupResponse { ok: true; pending: true }
5605
5685
 
@@ -12157,7 +12237,7 @@ Soft-delete a secret. DELETE /secrets/:ref
12157
12237
  ### sequence
12158
12238
 
12159
12239
  **allocate**(collectionId: string, input: AllocateSequenceInput) → `Promise<AllocatedSequence>`
12160
- Allocate (or return the existing) sequence number for a subject. Idempotent — safe to call on load (auto-enter) and on a button tap; a subject that already has a number gets it back with `isNew: false`. const { number, isNew } = await sequence.allocate(collectionId, { appId: 'raffle-app', sequenceId: 'raffle', subjectId: claimSetId, })
12240
+ Allocate (or return the existing) sequence number for a subject. Idempotent — safe to call on load (auto-enter) and on a button tap; a subject that already has a number gets it back with `isNew: false`. const { number, isNew } = await sequence.allocate(collectionId, { appId: 'raffle-app', sequenceId: 'raffle', subjectId: claimSetId, }) `ctx.sl.counters.next()` + `ctx.sl.appRecords.claim()` (docs/server-functions.md).
12161
12241
 
12162
12242
  ### sessions
12163
12243
 
@@ -320,14 +320,30 @@ link; nothing is consented until it's clicked. The answer is always the same, so
320
320
  used to find out who's registered.
321
321
 
322
322
  ```typescript
323
- await comms.signup('collectionId', { email: 'sam@example.com', topics: ['newsletter'] })
323
+ await comms.signup('collectionId', {
324
+ email: 'sam@example.com',
325
+ topics: ['newsletter'],
326
+ templateId: config.confirmationTemplateId, // YOUR confirmation email (required)
327
+ fields: { firstName: 'Sam', customFields: { favouriteStore: 'Leeds' } }, // optional
328
+ appId: 'my-newsletter',
329
+ })
324
330
  // → { ok: true, pending: true } — tell the visitor to check their inbox
325
331
  ```
326
332
 
333
+ - **The confirmation email is the app's own** (emails belong to the app that runs the flow — there is
334
+ no platform default): a template in the collection, created and edited in the app's admin, e.g.
335
+ `SL.template.create(collectionId, { name: 'Confirm your subscription', type: 'email', engine: 'liquid',
336
+ subject: 'Please confirm your subscription', body: '…<a href="{{ action_url }}">Yes, subscribe me</a>…' })`.
337
+ It gets `{{ action_url }}` (the confirm link), `{{ topics }}` (labels) and `{{ contact.firstName }}`.
338
+ - **Form answers** (`fields`) are applied only once the address is confirmed, and only to the
339
+ contact's empty fields — a sign-up can never overwrite someone else's details.
340
+
327
341
  - Topics must exist in the collection's comm settings. Give a newsletter topic
328
342
  `defaults.policy: 'opt-in'`: broadcasts on an opt-in topic only reach people who said yes
329
343
  (confirmed sign-ups), never the whole contact list.
330
344
  - Confirming turns on the email channel and the topics, and marks the address verified.
345
+ - Don't use `contact.publicUpsert` + `comms.upsertConsent` for a signed-out form: consent calls act only
346
+ for the signed-in contact. `comms.signup` is the signed-out path.
331
347
  - Set comm settings `signup.confirmedUrl` (https) to send people to your own "thanks" page after
332
348
  they confirm (`?confirmed=1`, or `0` for an expired link); otherwise SmartLinks shows a short page.
333
349
 
@@ -62,7 +62,39 @@ Iframe Mode:
62
62
  **Where routing goes:**
63
63
  - ✅ Your **iframe entry point** (`App.tsx` / `main.tsx`) should use `HashRouter`
64
64
  - ✅ Your **exported container** (`PublicContainer.tsx`) should include `<Routes>` and `<Route>` elements
65
- - ❌ Your **exported container** should NOT include any `<Router>` wrapper
65
+ - ❌ Your **exported container** should NOT include any `<Router>` wrapper — *unless isolated* (below)
66
+
67
+ ### Pages that read their context from the URL: an isolated router
68
+
69
+ The example app's pages read `collectionId` / `productId` / `proofId` from the URL. The framework's
70
+ router starts at `/` without them, so the example's container keeps **its own** `MemoryRouter` (seeded
71
+ with those params) — and **isolates** it from the host's router first. Since R5, `react-router-dom` is a
72
+ shared dependency (the host's copy), so a plain nested router is the same library as the host's and
73
+ throws _"You cannot render a `<Router>` inside another `<Router>`"_. Resetting the parent's location and
74
+ navigation context gives your container a router of its own — the same isolation portal-framework puts
75
+ around every direct component:
76
+
77
+ ```tsx
78
+ import { MemoryRouter, Routes, Route, UNSAFE_LocationContext, UNSAFE_NavigationContext } from 'react-router-dom';
79
+
80
+ export const PublicContainer = ({ collectionId, appId, productId, proofId }: SmartLinksContainerProps) => {
81
+ const q = new URLSearchParams({ collectionId, appId, ...(productId ? { productId } : {}), ...(proofId ? { proofId } : {}) });
82
+ return (
83
+ <UNSAFE_LocationContext.Provider value={null as any}>
84
+ <UNSAFE_NavigationContext.Provider value={null as any}>
85
+ <MemoryRouter initialEntries={[`/?${q}`]}>
86
+ <Routes>
87
+ <Route path="/*" element={<PublicPage />} />
88
+ </Routes>
89
+ </MemoryRouter>
90
+ </UNSAFE_NavigationContext.Provider>
91
+ </UNSAFE_LocationContext.Provider>
92
+ );
93
+ };
94
+ ```
95
+
96
+ Your container's own navigation then stays inside it (it isn't mirrored into the portal's deep link).
97
+ `smartlinks-doctor` warns when a containers bundle uses a router without this isolation.
66
98
 
67
99
  ### The Router Contract
68
100
 
@@ -93,7 +93,7 @@ The SmartLinks SDK (`@proveanything/smartlinks`) includes comprehensive document
93
93
  | **Container Tracking** | `docs/container-tracking.md` | Physical/logical **item** container groupings — *not* app containers (see containers.md) |
94
94
  | **Lots** | `docs/lots.md` | Cross-SKU production "Lot" entity |
95
95
  | **Loyalty** | `docs/loyalty.md` | Points, members, earning rules |
96
- | **Sequences** | `docs/sequences.md` | Atomic monotonic number allocation (raffle / queue / edition) |
96
+ | **Sequences** | `docs/sequences.md` | RETIRING — number things in your own server function: `ctx.sl.counters` + `appRecords.claim` (server-functions.md) |
97
97
  | **Integrations** | `docs/integrations.md` | Inbound/outbound external-system integration flows |
98
98
  | **Item Context** | `docs/item-context.md` | The `itemContext` container prop (serial / NFC authenticity context) |
99
99
  | **Native Facade** | `docs/native-facade.md` | `host.native` / `SL.native` device-capability facade |
@@ -1,5 +1,11 @@
1
1
  # Sequences & claim-order allocation
2
2
 
3
+ > **Retiring — don't build on this.** `SL.sequence.allocate` is being removed. Number things in your
4
+ > app's own server function instead: `ctx.sl.counters.next()` for the next number and
5
+ > `ctx.sl.appRecords.claim()` to key it to a tag/person and make sure no number is ever held twice —
6
+ > see server-functions.md → "Uniqueness, numbering and people". (The counter here lived inside the
7
+ > app config doc, where an admin save could rewind it.)
8
+
3
9
  > **SmartLinks SDK 2.x** (current `latest`). Install `@proveanything/smartlinks@^2`.
4
10
 
5
11
  A **sequence** hands out a guaranteed-unique, monotonic number — `1, 2, 3, …` — and stamps it
@@ -182,8 +182,9 @@ interface ServerFunctionContext {
182
182
  appId: string
183
183
 
184
184
  /** SmartLinks SDK, pre-scoped to your declared `authority`. Capabilities cap what it may do.
185
- * Key resources: `sl.appRecords` (per-app records), `sl.appData` (general/app-wide config +
186
- * data — URLs, flags, counters, arrays), `sl.products`, `sl.attestations`. */
185
+ * Key resources: `sl.appRecords` (per-app records, incl. race-safe `claim`), `sl.appData`
186
+ * (general/app-wide config + data — URLs, flags, arrays), `sl.counters` (atomic numbering),
187
+ * `sl.tags`, `sl.contacts`, `sl.interactions`, `sl.products`, `sl.attestations`. */
187
188
  sl: SmartLinks
188
189
 
189
190
  /** Capability-gated secrets (need `secrets:<name>`). `get(name)` reads this app's own secret on the
@@ -506,7 +507,11 @@ Prefer the platform primitives above over a dependency; when you do need one, pi
506
507
  | Read or write SmartLinks data | `ctx.sl.*` (records, products, attestations, …) | the matching `sl:<res>:<read\|write>` |
507
508
  | Read/write your app's own data (config, URLs, counters, arrays) | `ctx.sl.appData.get()` / `ctx.sl.appData.set({…})` — collection-scoped by default; `ctx.sl.appData.global.*` (or `{ scope:'global' }`, or declare `dataScope:'global'`) for the app-wide bucket. It's your OWN namespace, so **any** authority may read/write it | `sl:data:read` / `sl:data:write` |
508
509
  | Read ANOTHER app's data (as the caller may see it) | `ctx.sl.app('other-app').data.get()` — caller-authority, this collection, public-filtered, read-only | `sl:data:read` |
509
- | Guarantee a UNIQUE claim (pool of numbers, one-per-user, idempotency key) | `ctx.sl.appRecords.create({ ref: 'ball:57' })` — the DB unique index rejects a duplicate `ref` (catch = "already taken") | `sl:records:write` |
510
+ | Guarantee a UNIQUE claim (one entry per tag, one-per-user, idempotency key, a number nobody else holds) | `ctx.sl.appRecords.claim({ …, singletonPer, customId })` → `{ record, created, conflict? }` — see below | `sl:records:write` |
511
+ | Hand out the next number (tickets, order numbers) | `ctx.sl.counters.next('tickets')` — atomic, distinct per caller | `sl:data:write` |
512
+ | Check a tapped tag is real | `ctx.sl.tags.resolve(proofId)` → `{ id, claimSetId, code }` or `null` | `sl:tags:read` |
513
+ | Put a visitor's details on a contact | `ctx.sl.contacts.upsert({ email, firstName, … })` → `{ contactId, created }` (fills empty fields only) | `sl:contacts:write` |
514
+ | Record "they did X" | `ctx.sl.interactions.record({ interactionId, contactId, outcome, metadata })` | `sl:interactions:write` |
510
515
 
511
516
  Notes:
512
517
  - **Talking to the SmartLinks core is `ctx.sl`, not HTTP.** The common pattern — *validate /
@@ -525,6 +530,81 @@ Notes:
525
530
 
526
531
  ---
527
532
 
533
+ ## Uniqueness, numbering and people — the race-safe primitives
534
+
535
+ A public function is called by many people at once. These primitives make the outcome right no matter
536
+ how the calls interleave — the **database** decides, never a read-then-write in your code.
537
+
538
+ ### `appRecords.claim(fields)` — a unique create
539
+
540
+ Like `create`, but the record names a **unique key**, and it's inserted only if that key is free.
541
+ Otherwise you get the record already holding it back, **unchanged** (never updated):
542
+
543
+ ```ts
544
+ const { record, created, conflict } = await ctx.sl.appRecords.claim({
545
+ recordType: 'entry',
546
+ singletonPer: 'proof', proofId: tag.id, // key 1: one live entry per tag
547
+ customId: String(number), // key 2: one record per number, ever
548
+ data: { number },
549
+ })
550
+ // created: true → you got it
551
+ // created: false → conflict 'singleton' (this tag already has an entry: `record` is it)
552
+ // or 'customId' (that number is held by someone else: take another)
553
+ ```
554
+
555
+ - **`singletonPer`** — `'collection' | 'product' | 'variant' | 'batch' | 'proof'`, with its anchor id
556
+ (`productId`, `proofId`, …). One **live** record per key; deleting it frees the key.
557
+ - **`customId`** — unique per `(recordType, scopeType, scopeId, sourceSystem)`, case-insensitive, and
558
+ **never reused** (deleted records keep theirs). Right for numbers that must never be handed out twice.
559
+ - Give at least one; with both, the record is created only if both are free.
560
+ - Of any number of concurrent claims on one key, **exactly one** gets `created: true`.
561
+
562
+ ### `counters.next(name, { start?, by? })` — the next number
563
+
564
+ An atomic counter for your app in this collection: every caller gets a distinct value, even under a
565
+ burst (one row, incremented in one statement). The first call returns `start` (default 1). Counters only
566
+ move forward and live apart from your config, so no config save can rewind one. `counters.get(name)`
567
+ reads the last value handed out (`null` before the first). Name a new counter to start again from `start`.
568
+
569
+ **Counter + claim = a number nobody else can ever hold.** Take `next()`, then `claim` with
570
+ `customId: String(n)`. If the claim says `conflict: 'customId'`, take `next()` again. A failed or raced
571
+ attempt only leaves a gap — never a duplicate.
572
+
573
+ ### `tags.resolve(id)` — is this tap real?
574
+
575
+ A tap hands your app the virtual-proof id `<claimSetId>-<code>`. `resolve` returns
576
+ `{ id, claimSetId, code }` when that code exists in a claim set of **this** collection, else `null`.
577
+ Key records to the returned canonical `id`, and refuse `null` — otherwise anyone can invent ids.
578
+
579
+ ### `contacts.upsert(details)` / `contacts.forCaller()`
580
+
581
+ `upsert` finds the contact by `email` or `phone` (one is required) or creates it, and only **fills
582
+ empty fields** — it never overwrites a name or field the contact already has, because a visitor's typed
583
+ details are unverified. It's race-safe: two simultaneous entries with one email end on one contact.
584
+ Returns `{ contactId, created }`. `forCaller()` is the signed-in caller's own contact (`null` when
585
+ anonymous). **Never return contact ids to public callers** — keep them on your records.
586
+
587
+ This is not consent. A ticked "keep me updated" box is evidence you can record (below); subscribing
588
+ someone to marketing is the double opt-in flow (`comms.signup`, see comms.md).
589
+
590
+ ### `interactions.record(event)` — "they did X"
591
+
592
+ Records an event of one of **your app's interaction types**: create the type once in admin with
593
+ `SL.interactions.ensureType(collectionId, { appId, key: 'raffle-entry' })`, keep its id in your app
594
+ config, and pass that id. Unknown ids are refused (`INTERACTION_TYPE_NOT_FOUND`). The event runs the
595
+ type's effects and emits `interaction.submitted`, like any submission.
596
+
597
+ ```ts
598
+ await ctx.sl.interactions.record({
599
+ interactionId: config.interactionIds.entered,
600
+ contactId, // optional
601
+ outcome: 'entered',
602
+ metadata: { number, consent: true },
603
+ })
604
+ ```
605
+
606
+ ---
607
+
528
608
  ## Worked example — a public competition entry
529
609
 
530
610
  ```ts
@@ -756,6 +836,9 @@ const ctx = createFunctionTestContext({
756
836
 
757
837
  const res = await submitCompetitionEntry(ctx, { method: 'POST', body: { email: 'a@b.com', answer: '42' } })
758
838
  // ctx.sl.appRecords.create(...) throws CapabilityError unless `def` declares sl:records:write
839
+ // With no `sl` impl, records / counters / contacts / interactions run in memory with the platform's
840
+ // uniqueness rules — assert on ctx.memory. Share createFunctionTestMemory() between contexts to
841
+ // simulate concurrent callers; restrict ids with knownTags / knownInteractionTypes.
759
842
  ```
760
843
 
761
844
  Pass the **`def`** (not a hand-typed capability list) so "tested" can't drift from
package/dist/openapi.yaml CHANGED
@@ -19218,16 +19218,113 @@ components:
19218
19218
  AppDataHandle:
19219
19219
  type: object
19220
19220
  properties: {}
19221
+ AppRecordClaimResult:
19222
+ type: object
19223
+ properties:
19224
+ record:
19225
+ type: object
19226
+ additionalProperties: true
19227
+ created:
19228
+ type: boolean
19229
+ conflict:
19230
+ type: string
19231
+ enum:
19232
+ - singleton
19233
+ - customId
19234
+ required:
19235
+ - record
19236
+ - created
19237
+ ServerFunctionCounters:
19238
+ type: object
19239
+ properties: {}
19240
+ ResolvedTag:
19241
+ type: object
19242
+ properties:
19243
+ id:
19244
+ type: string
19245
+ claimSetId:
19246
+ type: string
19247
+ code:
19248
+ type: string
19249
+ required:
19250
+ - id
19251
+ - claimSetId
19252
+ - code
19253
+ ContactUpsertDetails:
19254
+ type: object
19255
+ properties:
19256
+ email:
19257
+ type: string
19258
+ phone:
19259
+ type: string
19260
+ firstName:
19261
+ type: string
19262
+ lastName:
19263
+ type: string
19264
+ displayName:
19265
+ type: string
19266
+ company:
19267
+ type: string
19268
+ locale:
19269
+ type: string
19270
+ timezone:
19271
+ type: string
19272
+ customFields:
19273
+ type: object
19274
+ additionalProperties:
19275
+ type: object
19276
+ additionalProperties: true
19277
+ ServerFunctionInteractionEvent:
19278
+ type: object
19279
+ properties:
19280
+ interactionId:
19281
+ type: string
19282
+ contactId:
19283
+ type: string
19284
+ outcome:
19285
+ type: string
19286
+ metadata:
19287
+ type: object
19288
+ additionalProperties: true
19289
+ productId:
19290
+ type: string
19291
+ proofId:
19292
+ type: string
19293
+ variantId:
19294
+ type: string
19295
+ batchId:
19296
+ type: string
19297
+ eventType:
19298
+ type: string
19299
+ required:
19300
+ - interactionId
19221
19301
  ServerFunctionSl:
19222
19302
  type: object
19223
19303
  properties:
19224
- appRecords: {}
19304
+ appRecords:
19305
+ type: object
19306
+ additionalProperties: true
19307
+ counters:
19308
+ $ref: "#/components/schemas/ServerFunctionCounters"
19309
+ tags:
19310
+ type: object
19311
+ additionalProperties: true
19312
+ contacts:
19313
+ type: object
19314
+ additionalProperties: true
19315
+ interactions:
19316
+ type: object
19317
+ additionalProperties: true
19225
19318
  products: {}
19226
19319
  attestations: {}
19227
19320
  appData:
19228
19321
  $ref: "#/components/schemas/AppDataHandle"
19229
19322
  required:
19230
19323
  - appRecords
19324
+ - counters
19325
+ - tags
19326
+ - contacts
19327
+ - interactions
19231
19328
  - products
19232
19329
  - attestations
19233
19330
  - appData
@@ -23658,14 +23755,22 @@ components:
23658
23755
  properties:
23659
23756
  email:
23660
23757
  type: string
23758
+ templateId:
23759
+ type: string
23661
23760
  topics:
23662
23761
  type: array
23663
23762
  items:
23664
23763
  type: string
23665
23764
  firstName:
23666
23765
  type: string
23766
+ fields:
23767
+ type: object
23768
+ additionalProperties: true
23769
+ appId:
23770
+ type: string
23667
23771
  required:
23668
23772
  - email
23773
+ - templateId
23669
23774
  CommsSignupResponse:
23670
23775
  type: object
23671
23776
  properties:
@@ -8,6 +8,7 @@ export declare class CapabilityError extends Error {
8
8
  export interface TestSlImpl {
9
9
  appRecords?: {
10
10
  create?(fields: any): any;
11
+ claim?(fields: any): any;
11
12
  update?(id: string, fields: any): any;
12
13
  upsert?(fields: any): any;
13
14
  delete?(id: string): any;
@@ -15,6 +16,21 @@ export interface TestSlImpl {
15
16
  query?(params: any): any;
16
17
  listTypes?(): any;
17
18
  };
19
+ counters?: {
20
+ next?(name: string, opts?: any): any;
21
+ get?(name: string): any;
22
+ };
23
+ /** Tag lookup. Default: any `<claimSetId>-<code>` resolves (pass `knownTags` to restrict). */
24
+ tags?: {
25
+ resolve?(id: string): any;
26
+ };
27
+ contacts?: {
28
+ upsert?(details: any): any;
29
+ forCaller?(): any;
30
+ };
31
+ interactions?: {
32
+ record?(event: any): any;
33
+ };
18
34
  products?: {
19
35
  get?(id: string, opts?: any): any;
20
36
  query?(body?: any, opts?: any): any;
@@ -64,7 +80,27 @@ export interface CreateFunctionTestContextOptions {
64
80
  fetch?: typeof fetch;
65
81
  /** What the platform would have prefetched as `ctx.inputs` (e.g. `{ entity: product }`). Default `{}`. */
66
82
  inputs?: ServerFunctionInputs;
83
+ /** Tags that exist, as `<claimSetId>-<code>` ids — `ctx.sl.tags.resolve` returns null for any other. */
84
+ knownTags?: string[];
85
+ /** Interaction type ids that exist for this app — `interactions.record` refuses others. Default: any. */
86
+ knownInteractionTypes?: string[];
87
+ /** Share one in-memory store between several contexts (e.g. concurrent callers in one test). */
88
+ memory?: FunctionTestMemory;
89
+ }
90
+ /** The harness's in-memory platform state (when no impl is injected). */
91
+ export interface FunctionTestMemory {
92
+ records: any[];
93
+ counters: Record<string, number>;
94
+ contacts: Array<{
95
+ contactId: string;
96
+ email?: string;
97
+ phone?: string;
98
+ [k: string]: any;
99
+ }>;
100
+ interactions: any[];
67
101
  }
102
+ /** A fresh in-memory store — pass the same one to several test contexts to simulate concurrent callers. */
103
+ export declare function createFunctionTestMemory(): FunctionTestMemory;
68
104
  export interface FunctionTestContext extends ServerFunctionContext {
69
105
  /** Captured log lines (also written via ctx.log). */
70
106
  logs: Array<{
@@ -72,6 +108,8 @@ export interface FunctionTestContext extends ServerFunctionContext {
72
108
  message: string;
73
109
  data?: Record<string, any>;
74
110
  }>;
111
+ /** The in-memory platform state this context reads and writes. */
112
+ memory: FunctionTestMemory;
75
113
  }
76
114
  /**
77
115
  * Build a capability-enforcing test ctx for a server function. Run your handler with it: