@proveanything/smartlinks 2.0.49 → 2.0.51

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.
@@ -14,6 +14,21 @@
14
14
  // - `ctx.secrets` are FIXTURES you provide; real sealed secrets are server-only and
15
15
  // never resolvable locally.
16
16
  // - `ctx.fetch` is gated by the `network` capability just like production.
17
+ // - Without an injected impl, appRecords / counters / contacts / interactions run against an
18
+ // in-memory store with the platform's uniqueness rules (claim: singletonPer + customId; contacts by
19
+ // email/phone, fill-empty only), so idempotency and "never two of the same number" are testable.
20
+ // Inspect it via `ctx.memory`.
21
+ var __rest = (this && this.__rest) || function (s, e) {
22
+ var t = {};
23
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
24
+ t[p] = s[p];
25
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
26
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
27
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
28
+ t[p[i]] = s[p[i]];
29
+ }
30
+ return t;
31
+ };
17
32
  export class CapabilityError extends Error {
18
33
  constructor(capability, message) {
19
34
  super(message || `capability not granted: ${capability}`);
@@ -58,6 +73,10 @@ function allowsSl(p, resource, op) {
58
73
  return true; // write implies read
59
74
  return false;
60
75
  }
76
+ /** A fresh in-memory store — pass the same one to several test contexts to simulate concurrent callers. */
77
+ export function createFunctionTestMemory() {
78
+ return { records: [], counters: {}, contacts: [], interactions: [] };
79
+ }
61
80
  const stub = (method, args) => ({ __stub: true, method, args });
62
81
  /**
63
82
  * Build a capability-enforcing test ctx for a server function. Run your handler with it:
@@ -79,18 +98,75 @@ export function createFunctionTestContext(opts) {
79
98
  throw new CapabilityError(`sl:${resource}:${op}`);
80
99
  return fn ? fn(...args) : stub(`${resource}.${name}`, args);
81
100
  };
101
+ const mem = opts.memory || createFunctionTestMemory();
82
102
  const ar = impl.appRecords || {};
83
103
  const pr = impl.products || {};
84
104
  const at = impl.attestations || {};
105
+ // In-memory records with the platform's unique keys (used when no appRecords impl is injected).
106
+ const singletonKey = (f) => {
107
+ const t = f.recordType || 'default';
108
+ switch (f.singletonPer) {
109
+ case 'collection': return `${t}:collection`;
110
+ case 'product': return f.productId ? `${t}:product:${f.productId}` : null;
111
+ case 'variant': return f.productId && f.variantId ? `${t}:variant:${f.productId}:${f.variantId}` : null;
112
+ case 'batch': return f.productId && f.batchId ? `${t}:batch:${f.productId}:${f.batchId}` : null;
113
+ case 'proof': return f.proofId ? `${t}:proof:${f.proofId}` : null;
114
+ default: return null;
115
+ }
116
+ };
117
+ const customKey = (f) => {
118
+ const c = f.customId == null ? '' : String(f.customId).trim().toLowerCase();
119
+ return c ? [f.recordType || 'default', f.scopeType || '', f.scopeId || '', f.sourceSystem || '', c].join('|') : null;
120
+ };
121
+ let nextRecordId = 1;
122
+ const memRecords = {
123
+ create: async (fields) => {
124
+ const rec = Object.assign({ id: `rec-${nextRecordId++}`, recordType: 'default', data: {}, admin: {}, status: 'active', createdAt: new Date().toISOString() }, fields);
125
+ mem.records.push(rec);
126
+ return Object.assign({}, rec);
127
+ },
128
+ claim: async (fields) => {
129
+ const sk = singletonKey(fields);
130
+ const ck = customKey(fields);
131
+ if (fields.singletonPer && !sk)
132
+ throw Object.assign(new Error(`claim: singletonPer "${fields.singletonPer}" needs its anchor id`), { code: 'INVALID_CLAIM' });
133
+ if (!sk && !ck)
134
+ throw Object.assign(new Error('claim: give the record a unique key — singletonPer (+ anchor) or customId'), { code: 'INVALID_CLAIM' });
135
+ const bySingleton = sk ? mem.records.find((r) => !r.deletedAt && singletonKey(r) === sk) : null;
136
+ if (bySingleton)
137
+ return { record: Object.assign({}, bySingleton), created: false, conflict: 'singleton' };
138
+ const byCustom = ck ? mem.records.find((r) => customKey(r) === ck) : null; // deleted records keep their customId
139
+ if (byCustom)
140
+ return { record: Object.assign({}, byCustom), created: false, conflict: 'customId' };
141
+ return { record: await memRecords.create(fields), created: true };
142
+ },
143
+ get: async (id) => { const r = mem.records.find((x) => x.id === id); return r ? Object.assign({}, r) : null; },
144
+ update: async (id, fields) => { const r = mem.records.find((x) => x.id === id); if (!r)
145
+ return null; Object.assign(r, fields); return Object.assign({}, r); },
146
+ delete: async (id) => { const r = mem.records.find((x) => x.id === id); if (r)
147
+ r.deletedAt = new Date().toISOString(); return {}; },
148
+ // Simple equality filter on top-level fields (recordType, proofId, customId, status, …).
149
+ query: async (params = {}) => {
150
+ const _a = params || {}, { limit, offset, sort } = _a, where = __rest(_a, ["limit", "offset", "sort"]);
151
+ const all = mem.records.filter((r) => !r.deletedAt && Object.entries(where).every(([k, v]) => r[k] === v));
152
+ const from = offset || 0;
153
+ const size = limit || 200;
154
+ const data = all.slice(from, from + size).map((r) => (Object.assign({}, r)));
155
+ // Same envelope as the platform: { data, pagination }.
156
+ return { data, pagination: { total: all.length, limit: size, offset: from, hasMore: from + data.length < all.length } };
157
+ },
158
+ };
159
+ const recordsFn = (name) => ar[name] ? ar[name].bind(ar) : (impl.appRecords ? undefined : memRecords[name]);
85
160
  const sl = {
86
161
  appRecords: {
87
- create: gated('records', 'write', ar.create && ar.create.bind(ar), 'create'),
88
- update: gated('records', 'write', ar.update && ar.update.bind(ar), 'update'),
89
- upsert: gated('records', 'write', ar.upsert && ar.upsert.bind(ar), 'upsert'),
90
- delete: gated('records', 'write', ar.delete && ar.delete.bind(ar), 'delete'),
91
- get: gated('records', 'read', ar.get && ar.get.bind(ar), 'get'),
92
- query: gated('records', 'read', ar.query && ar.query.bind(ar), 'query'),
93
- listTypes: gated('records', 'read', ar.listTypes && ar.listTypes.bind(ar), 'listTypes'),
162
+ create: gated('records', 'write', recordsFn('create'), 'create'),
163
+ claim: gated('records', 'write', recordsFn('claim'), 'claim'),
164
+ update: gated('records', 'write', recordsFn('update'), 'update'),
165
+ upsert: gated('records', 'write', recordsFn('upsert'), 'upsert'),
166
+ delete: gated('records', 'write', recordsFn('delete'), 'delete'),
167
+ get: gated('records', 'read', recordsFn('get'), 'get'),
168
+ query: gated('records', 'read', recordsFn('query'), 'query'),
169
+ listTypes: gated('records', 'read', recordsFn('listTypes'), 'listTypes'),
94
170
  },
95
171
  products: {
96
172
  get: gated('products', 'read', pr.get && pr.get.bind(pr), 'get'),
@@ -122,6 +198,62 @@ export function createFunctionTestContext(opts) {
122
198
  delete memConfig[k]; }), 'delete'),
123
199
  });
124
200
  sl.appData = Object.assign(makeAppData(), { global: makeAppData(), collection: makeAppData() });
201
+ const cn = impl.counters || {};
202
+ sl.counters = {
203
+ next: gated('data', 'write', cn.next ? cn.next.bind(cn) : (async (name, o = {}) => {
204
+ var _a, _b;
205
+ const start = (_a = o.start) !== null && _a !== void 0 ? _a : 1;
206
+ const by = (_b = o.by) !== null && _b !== void 0 ? _b : 1;
207
+ mem.counters[name] = mem.counters[name] == null ? start : mem.counters[name] + by;
208
+ return mem.counters[name];
209
+ }), 'counters.next'),
210
+ get: gated('data', 'read', cn.get ? cn.get.bind(cn) : (async (name) => { var _a; return (_a = mem.counters[name]) !== null && _a !== void 0 ? _a : null; }), 'counters.get'),
211
+ };
212
+ const tg = impl.tags || {};
213
+ sl.tags = {
214
+ resolve: gated('tags', 'read', tg.resolve ? tg.resolve.bind(tg) : (async (id) => {
215
+ const s = String(id || '');
216
+ const i = s.indexOf('-');
217
+ if (i <= 0 || i >= s.length - 1)
218
+ return null;
219
+ if (opts.knownTags && !opts.knownTags.includes(s))
220
+ return null;
221
+ return { id: s, claimSetId: s.slice(0, i), code: s.slice(i + 1) };
222
+ }), 'tags.resolve'),
223
+ };
224
+ const ct = impl.contacts || {};
225
+ const fillable = ['firstName', 'lastName', 'displayName', 'company', 'locale', 'timezone'];
226
+ sl.contacts = {
227
+ upsert: gated('contacts', 'write', ct.upsert ? ct.upsert.bind(ct) : (async (d = {}) => {
228
+ const email = d.email ? String(d.email).trim().toLowerCase() : undefined;
229
+ const phone = d.phone ? String(d.phone).replace(/[^\d+]/g, '') : undefined;
230
+ if (!email && !phone)
231
+ throw Object.assign(new Error('contacts.upsert: an email or phone is required'), { code: 'CONTACT_IDENTITY_REQUIRED' });
232
+ const found = mem.contacts.find((c) => (email && c.email === email) || (phone && c.phone === phone));
233
+ if (found) {
234
+ for (const k of fillable)
235
+ if (d[k] && !found[k])
236
+ found[k] = d[k];
237
+ found.customFields = Object.assign(Object.assign({}, (d.customFields || {})), (found.customFields || {}));
238
+ return { contactId: found.contactId, created: false };
239
+ }
240
+ const contactId = `contact-${mem.contacts.length + 1}`;
241
+ mem.contacts.push(Object.assign(Object.assign({}, d), { email, phone, contactId }));
242
+ return { contactId, created: true };
243
+ }), 'contacts.upsert'),
244
+ forCaller: gated('contacts', 'write', ct.forCaller ? ct.forCaller.bind(ct) : (async () => (opts.caller && opts.caller.userId ? { contactId: `contact-of-${opts.caller.userId}` } : null)), 'contacts.forCaller'),
245
+ };
246
+ const it = impl.interactions || {};
247
+ sl.interactions = {
248
+ record: gated('interactions', 'write', it.record ? it.record.bind(it) : (async (event = {}) => {
249
+ if (!event.interactionId || (opts.knownInteractionTypes && !opts.knownInteractionTypes.includes(event.interactionId))) {
250
+ throw Object.assign(new Error(`"${event.interactionId}" is not an interaction type of this app`), { code: 'INTERACTION_TYPE_NOT_FOUND' });
251
+ }
252
+ const eventId = `event-${mem.interactions.length + 1}`;
253
+ mem.interactions.push(Object.assign(Object.assign({}, event), { eventId, userId: (opts.caller && opts.caller.userId) || null }));
254
+ return { eventId };
255
+ }), 'interactions.record'),
256
+ };
125
257
  sl.app = (appId) => {
126
258
  const other = (impl.app && impl.app(appId)) || {};
127
259
  const od = other.data || {};
@@ -178,5 +310,6 @@ export function createFunctionTestContext(opts) {
178
310
  },
179
311
  inputs: Object.freeze(Object.assign({}, (opts.inputs || {}))),
180
312
  logs,
313
+ memory: mem,
181
314
  };
182
315
  }
@@ -679,7 +679,7 @@ export interface AgentRunRequest {
679
679
  prompt?: string;
680
680
  /** System instructions. */
681
681
  instructions?: string;
682
- /** Model id, e.g. 'openai/gpt-5.6-terra'. */
682
+ /** Model id, e.g. 'openai/gpt-6.1-sol'. */
683
683
  model?: string;
684
684
  /** Safety cap on model round-trips. */
685
685
  maxSteps?: number;
@@ -365,9 +365,96 @@ export interface AppDataHandle {
365
365
  * - by CALLER identity — everything else (products, attestations, and `app(id)` cross-app reads)
366
366
  * is bounded by what the invoking user could do through the permissioned API.
367
367
  */
368
+ /** `ctx.sl.appRecords.claim` result. `conflict` says which unique key was already held. */
369
+ export interface AppRecordClaimResult<T = any> {
370
+ record: T;
371
+ created: boolean;
372
+ conflict?: 'singleton' | 'customId';
373
+ }
374
+ /** `ctx.sl.counters` — this app's atomic named counters in the collection. */
375
+ export interface ServerFunctionCounters {
376
+ /** The next value — distinct for every caller, even under a burst. First call returns `start` (default 1). */
377
+ next(name: string, opts?: {
378
+ start?: number;
379
+ by?: number;
380
+ }): Promise<number>;
381
+ /** The last value handed out, or `null` before the first `next`. */
382
+ get(name: string): Promise<number | null>;
383
+ }
384
+ /** A tap's tag, resolved and checked to belong to this collection. */
385
+ export interface ResolvedTag {
386
+ /** Canonical virtual-proof id `<claimSetId>-<code>` — key things to THIS, not the raw input. */
387
+ id: string;
388
+ claimSetId: string;
389
+ code: string;
390
+ }
391
+ /** Contact details a function may pass to `ctx.sl.contacts.upsert` (plain strings, capped). */
392
+ export interface ContactUpsertDetails {
393
+ email?: string;
394
+ phone?: string;
395
+ firstName?: string;
396
+ lastName?: string;
397
+ displayName?: string;
398
+ company?: string;
399
+ locale?: string;
400
+ timezone?: string;
401
+ customFields?: Record<string, string | number | boolean>;
402
+ }
403
+ /** An interaction event recorded from a server function. */
404
+ export interface ServerFunctionInteractionEvent {
405
+ /** The interaction TYPE id — created first in admin with `interactions.ensureType`, kept in config. */
406
+ interactionId: string;
407
+ contactId?: string | null;
408
+ /** e.g. 'entered'. Letters, digits, `. : - _`. Default 'submitted'. */
409
+ outcome?: string;
410
+ metadata?: Record<string, any>;
411
+ productId?: string;
412
+ proofId?: string;
413
+ variantId?: string;
414
+ batchId?: string;
415
+ eventType?: string;
416
+ }
368
417
  export interface ServerFunctionSl {
369
- /** THIS app's records (own namespace). */
370
- appRecords: any;
418
+ /**
419
+ * THIS app's records (own namespace): create / update / upsert / delete / get / query / listTypes,
420
+ * plus `claim(fields)` — a race-safe UNIQUE create. The record names a unique key — `singletonPer`
421
+ * (+ its anchor, e.g. `{ singletonPer: 'proof', proofId }`) and/or `customId` — and is inserted only
422
+ * if the key is free; otherwise the record holding it comes back UNCHANGED with `created: false`.
423
+ * Of any number of concurrent claims on one key, exactly one is created (the database decides).
424
+ */
425
+ appRecords: {
426
+ claim(fields: Record<string, any>): Promise<AppRecordClaimResult>;
427
+ [method: string]: any;
428
+ };
429
+ /** Atomic named counters for this app in this collection (`sl:data:write` / `sl:data:read`). */
430
+ counters: ServerFunctionCounters;
431
+ /** Tags (`sl:tags:read`): `resolve(id)` → the real tag in THIS collection, or `null` for a made-up id. */
432
+ tags: {
433
+ resolve(id: string): Promise<ResolvedTag | null>;
434
+ };
435
+ /**
436
+ * Contacts (`sl:contacts:write`). `upsert` finds by email/phone or creates, and only FILLS EMPTY
437
+ * fields — never overwrites (details typed by a visitor are unverified). `forCaller` is the signed-in
438
+ * caller's own contact (null when anonymous). Never return contact ids to public callers.
439
+ */
440
+ contacts: {
441
+ upsert(details: ContactUpsertDetails): Promise<{
442
+ contactId: string;
443
+ created: boolean;
444
+ }>;
445
+ forCaller(): Promise<{
446
+ contactId: string;
447
+ } | null>;
448
+ };
449
+ /**
450
+ * Interactions (`sl:interactions:write`). `record` stores an event of one of THIS app's interaction
451
+ * types (refused if the type doesn't exist), runs the type's effects and emits `interaction.submitted`.
452
+ */
453
+ interactions: {
454
+ record(event: ServerFunctionInteractionEvent): Promise<{
455
+ eventId: string;
456
+ }>;
457
+ };
371
458
  /** Products, scoped by caller authority + declared capability. */
372
459
  products: any;
373
460
  /** Attestations, scoped by caller authority + declared capability. */
@@ -184,9 +184,31 @@ export interface UnsubscribeResponse {
184
184
  }
185
185
  export interface CommsSignupRequest {
186
186
  email: string;
187
+ /**
188
+ * Required: YOUR app's confirmation email — a template in this collection (SL.template) whose body
189
+ * uses `{{ action_url }}` for the confirm link (`{{ topics }}` lists what they signed up for). The
190
+ * platform sends no default sign-up email.
191
+ */
192
+ templateId: string;
187
193
  /** Topic ids from the collection's comm settings (e.g. ["newsletter"]). */
188
194
  topics?: string[];
189
195
  firstName?: string;
196
+ /**
197
+ * The form's other answers — firstName/lastName/displayName/company/locale/timezone and
198
+ * `customFields`. Applied to the contact when the address is confirmed, and only where the contact
199
+ * has no value yet (never overwritten). Small: ~2 KB.
200
+ */
201
+ fields?: {
202
+ firstName?: string;
203
+ lastName?: string;
204
+ displayName?: string;
205
+ company?: string;
206
+ locale?: string;
207
+ timezone?: string;
208
+ customFields?: Record<string, string | number | boolean>;
209
+ };
210
+ /** Your app id, recorded with the sent email. */
211
+ appId?: string;
190
212
  }
191
213
  export interface CommsSignupResponse {
192
214
  ok: true;
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.49 | Generated: 2026-10-09T10:14:16.325Z
3
+ Version: 2.0.51 | Generated: 2026-10-10T07:57:23.889Z
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
 
package/docs/ai.md CHANGED
@@ -408,15 +408,21 @@ Discover tools two ways:
408
408
 
409
409
  ### Recommended Models
410
410
 
411
- For agentic workflows on `v1/responses`, GPT-5.6 ships in three tiers. Pass either the full model
412
- id (`openai/gpt-5.6-sol`) or the shorthand tier alias (`'cheap'`, `'balanced'`, `'premium'`) as
413
- `model` — both resolve through the same server-side model registry.
414
-
415
- | Tier | Model | Alias | Use for |
416
- |------|-------|-------|---------|
417
- | Cheapest (default) | `openai/gpt-5.6-luna` | `'cheap'` / omit `model` | Everyday chat, high-volume/simple turns |
418
- | Balanced | `openai/gpt-5.6-terra` | `'balanced'` | Admin agent / tool-heavy setup workflows (route default) |
419
- | Flagship | `openai/gpt-5.6-sol` | `'premium'` | Most demanding reasoning, coding, and multi-step tool use |
411
+ Pass either a full model id (`openai/gpt-6.1-sol`) or a tier alias (`'cheap'`, `'balanced'`,
412
+ `'premium'`) as `model` — both resolve through the same server-side model registry. The tiers are the
413
+ GPT-6 family (prices per 1M tokens, input / output, OpenAI list price):
414
+
415
+ | Tier | Model | Alias | Price | Use for |
416
+ |------|-------|-------|-------|---------|
417
+ | Cheapest (default) | `openai/gpt-6-luna` | `'cheap'` / omit `model` | $0.10 / $0.50 | Everyday chat, high-volume/simple turns |
418
+ | Balanced | `openai/gpt-6.1-sol` | `'balanced'` | $2 / $10 | Admin agent / tool-heavy workflows (the `v1/responses` default) |
419
+ | Most capable | `openai/gpt-6-astra` | `'premium'` | $10 / $50 | The hardest reasoning, coding and long multi-step tool use |
420
+
421
+ The previous generation stays available by id — `openai/gpt-5.6-luna` ($0.20 / $1.20),
422
+ `openai/gpt-5.6-terra` ($2 / $12) and `openai/gpt-5.6-sol` ($4 / $20) — and `ai.models.list(collectionId)` returns
423
+ the full current set. GPT-6.1 Sol is the same input price as GPT-5.6 Terra with cheaper output, so prefer
424
+ the tiers. If OpenAI refuses a GPT-6 request for a model/option reason, SmartLinks retries it once on the
425
+ GPT-5.6 equivalent, so a call never fails just because a GPT-6 model doesn't accept something yet.
420
426
 
421
427
  Every tier supports function/tool calling, Programmatic Tool Calling, and Multi-agent (see below), and all
422
428
  three accept `service_tier: 'flex'` (≈50% cheaper, Batch-API rates, slower) or `'priority'` (premium, guaranteed
package/docs/comms.md CHANGED
@@ -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
 
package/docs/overview.md CHANGED
@@ -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 |
package/docs/sequences.md CHANGED
@@ -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