@proveanything/smartlinks 2.0.47 → 2.0.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,12 @@
1
1
  import type { CommunicationEvent, CommsQueryByUser, CommsRecipientIdsQuery, CommsRecipientsWithoutActionQuery, CommsRecipientsWithActionQuery, RecipientId, RecipientWithOutcome, LogCommunicationEventBody, LogBulkCommunicationEventsBody, AppendResult, AppendBulkResult, TransactionalSendRequest, TransactionalSendResult } from "../types/comms.js";
2
2
  /**
3
- * Communications namespace for sending notifications and managing user communications
3
+ * Communications namespace for sending notifications and managing user communications.
4
+ *
5
+ * The public preference / consent / subscription / method endpoints act only for the SIGNED-IN contact
6
+ * (their own contact in the collection), or for any contact when the caller is a collection admin —
7
+ * signed out they answer 401, someone else's contactId 403. `contactId` may be omitted to mean "me".
8
+ * `unsubscribe` also works signed out with a valid signed unsubscribe `token`. An email/phone
9
+ * registered here is only marked verified when it's the one the user signed in with.
4
10
  */
5
11
  export declare namespace comms {
6
12
  /**
@@ -37,6 +43,15 @@ export declare namespace comms {
37
43
  * GET /public/collection/:collectionId/comm/unsubscribe
38
44
  */
39
45
  function unsubscribe(collectionId: string, query: import("../types/comms.js").UnsubscribeQuery): Promise<import("../types/comms.js").UnsubscribeResponse>;
46
+ /**
47
+ * Public: sign an email address up for topics (a newsletter, updates) — works signed out.
48
+ * Double opt-in: nothing is consented until the address's owner clicks the confirmation link
49
+ * SmartLinks emails them. Always answers `{ ok: true, pending: true }` (it never reveals whether
50
+ * the address is already known). Topics must be ones configured in the collection's comm settings.
51
+ * Set comm settings `signup.confirmedUrl` to send people to your own page after confirming.
52
+ * POST /public/collection/:collectionId/comm/signup
53
+ */
54
+ function signup(collectionId: string, body: import("../types/comms.js").CommsSignupRequest): Promise<import("../types/comms.js").CommsSignupResponse>;
40
55
  /**
41
56
  * Public: Upsert default consent for a contact.
42
57
  * POST /public/collection/:collectionId/comm/consent
package/dist/api/comms.js CHANGED
@@ -2,7 +2,13 @@
2
2
  // Communications and notifications API for Smartlinks
3
3
  import { post, request, patch } from "../http.js";
4
4
  /**
5
- * Communications namespace for sending notifications and managing user communications
5
+ * Communications namespace for sending notifications and managing user communications.
6
+ *
7
+ * The public preference / consent / subscription / method endpoints act only for the SIGNED-IN contact
8
+ * (their own contact in the collection), or for any contact when the caller is a collection admin —
9
+ * signed out they answer 401, someone else's contactId 403. `contactId` may be omitted to mean "me".
10
+ * `unsubscribe` also works signed out with a valid signed unsubscribe `token`. An email/phone
11
+ * registered here is only marked verified when it's the one the user signed in with.
6
12
  */
7
13
  export var comms;
8
14
  (function (comms) {
@@ -64,7 +70,8 @@ export var comms;
64
70
  */
65
71
  async function unsubscribe(collectionId, query) {
66
72
  const params = new URLSearchParams();
67
- params.set('contactId', query.contactId);
73
+ if (query.contactId)
74
+ params.set('contactId', query.contactId);
68
75
  if (query.topic)
69
76
  params.set('topic', query.topic);
70
77
  if (query.channel)
@@ -76,6 +83,19 @@ export var comms;
76
83
  return request(path);
77
84
  }
78
85
  comms.unsubscribe = unsubscribe;
86
+ /**
87
+ * Public: sign an email address up for topics (a newsletter, updates) — works signed out.
88
+ * Double opt-in: nothing is consented until the address's owner clicks the confirmation link
89
+ * SmartLinks emails them. Always answers `{ ok: true, pending: true }` (it never reveals whether
90
+ * the address is already known). Topics must be ones configured in the collection's comm settings.
91
+ * Set comm settings `signup.confirmedUrl` to send people to your own page after confirming.
92
+ * POST /public/collection/:collectionId/comm/signup
93
+ */
94
+ async function signup(collectionId, body) {
95
+ const path = `/public/collection/${encodeURIComponent(collectionId)}/comm/signup`;
96
+ return post(path, body);
97
+ }
98
+ comms.signup = signup;
79
99
  /**
80
100
  * Public: Upsert default consent for a contact.
81
101
  * POST /public/collection/:collectionId/comm/consent
@@ -109,7 +129,8 @@ export var comms;
109
129
  */
110
130
  async function checkSubscription(collectionId, query) {
111
131
  const params = new URLSearchParams();
112
- params.set('contactId', query.contactId);
132
+ if (query.contactId)
133
+ params.set('contactId', query.contactId);
113
134
  params.set('subjectType', query.subjectType);
114
135
  params.set('subjectId', query.subjectId);
115
136
  if (query.productId)
@@ -125,7 +146,8 @@ export var comms;
125
146
  */
126
147
  async function listMethods(collectionId, query) {
127
148
  const params = new URLSearchParams();
128
- params.set('contactId', query.contactId);
149
+ if (query.contactId)
150
+ params.set('contactId', query.contactId);
129
151
  if (query.type)
130
152
  params.set('type', query.type);
131
153
  const qs = `?${params.toString()}`;
@@ -27,7 +27,9 @@
27
27
  // installed release regardless.
28
28
  import { post, request, getAppContext, getAppChannel } from "../http.js";
29
29
  import { readContext } from "../context.js";
30
- const CHANNELS = ['dev', 'alpha', 'beta', 'stable'];
30
+ // 'preview' = the app's in-progress server code inside Forge's preview (needs the preview pass Forge
31
+ // puts in the context as `slPreview`; SmartLinks refuses preview calls without it).
32
+ const CHANNELS = ['dev', 'alpha', 'beta', 'stable', 'preview'];
31
33
  /**
32
34
  * The release channel a call targets, or undefined for "the collection's installed release".
33
35
  * A host context `appChannel` can be scoped to ONE app with `appChannelApp` — needed where several
@@ -52,6 +54,16 @@ export function resolveFunctionChannel(opts = {}, appId) {
52
54
  throw new Error(`Unknown release channel "${raw}" (expected ${CHANNELS.join(' | ')})`);
53
55
  return ch;
54
56
  }
57
+ /** Add Forge's preview pass to a preview-channel URL (a query param, so host API proxies keep it). */
58
+ function withPreviewPass(url, ch, appId) {
59
+ if (ch !== 'preview')
60
+ return url;
61
+ const ctx = readContext();
62
+ if (ctx.appChannelApp && appId && ctx.appChannelApp !== appId)
63
+ return url;
64
+ const pass = ctx.slPreview;
65
+ return pass ? `${url}${url.includes('?') ? '&' : '?'}slPreview=${encodeURIComponent(pass)}` : url;
66
+ }
55
67
  function appBase(surface, collectionId, opts) {
56
68
  var _a;
57
69
  const c = encodeURIComponent(collectionId);
@@ -63,7 +75,10 @@ function appBase(surface, collectionId, opts) {
63
75
  }
64
76
  /** The API path a function call goes to (exported for hosts/tests that need the exact URL). */
65
77
  export function functionPath(surface, collectionId, name, opts = {}) {
66
- return `${appBase(surface, collectionId, opts)}/functions/${encodeURIComponent(name)}`;
78
+ var _a;
79
+ const app = (_a = opts.appId) !== null && _a !== void 0 ? _a : getAppContext();
80
+ const ch = app ? resolveFunctionChannel(opts, app) : undefined;
81
+ return withPreviewPass(`${appBase(surface, collectionId, opts)}/functions/${encodeURIComponent(name)}`, ch, app);
67
82
  }
68
83
  const fnPath = functionPath;
69
84
  export var functions;
@@ -123,7 +138,10 @@ export var functions;
123
138
  * appId is given (or set as the SDK app context): `GET /public/collection/:c[/app/:appId]/functions`.
124
139
  */
125
140
  async function list(collectionId, opts = {}) {
126
- return request(`${appBase('public', collectionId, opts)}/functions`);
141
+ var _a;
142
+ const app = (_a = opts.appId) !== null && _a !== void 0 ? _a : getAppContext();
143
+ const ch = app ? resolveFunctionChannel(opts, app) : undefined;
144
+ return request(withPreviewPass(`${appBase('public', collectionId, opts)}/functions`, ch, app));
127
145
  }
128
146
  functions.list = list;
129
147
  })(functions || (functions = {}));
package/dist/context.d.ts CHANGED
@@ -10,6 +10,11 @@ export interface SmartLinksContext {
10
10
  appChannel?: string;
11
11
  /** Limits `appChannel` to one app (where several apps share a page, e.g. a portal preview). */
12
12
  appChannelApp?: string;
13
+ /**
14
+ * Forge's preview pass: lets this preview run the app's in-progress server code
15
+ * (`appChannel: 'preview'`) on this collection. Set by Forge; never present elsewhere.
16
+ */
17
+ slPreview?: string;
13
18
  /** Any other declared view params (e.g. pageId, voteId, orientation, tvMode). */
14
19
  [key: string]: string | undefined;
15
20
  }
package/dist/context.js CHANGED
@@ -54,6 +54,7 @@ export function readContext(overrides) {
54
54
  fill('appChannel', site.channel);
55
55
  fill('appChannelApp', site.appId);
56
56
  }
57
+ fill('slPreview', site.previewPass);
57
58
  }
58
59
  }
59
60
  catch (_a) {
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.47 | Generated: 2026-10-08T12:38:10.855Z
3
+ Version: 2.0.48 | Generated: 2026-10-09T09:58:35.560Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -2401,6 +2401,17 @@ interface AppWidgetComponent {
2401
2401
  optional?: string[];
2402
2402
  };
2403
2403
  settings?: Record<string, any>;
2404
+ styleSupport?: WidgetStyleSupport;
2405
+ }
2406
+ ```
2407
+
2408
+ **WidgetStyleSupport** (interface)
2409
+ ```typescript
2410
+ interface WidgetStyleSupport {
2411
+ accent?: boolean;
2412
+ text?: boolean;
2413
+ textAlign?: boolean;
2414
+ background?: 'transparent' | 'own';
2404
2415
  }
2405
2416
  ```
2406
2417
 
@@ -5140,7 +5151,7 @@ interface PushSubscribeResponse {
5140
5151
  **RegisterPushMethodRequest** (interface)
5141
5152
  ```typescript
5142
5153
  interface RegisterPushMethodRequest {
5143
- contactId: string
5154
+ contactId?: string
5144
5155
  endpoint: string
5145
5156
  keys: { p256dh: string; auth: string }
5146
5157
  meta?: Record<string, any>
@@ -5200,7 +5211,7 @@ interface CommsPublicTopicsResponse {
5200
5211
  **UnsubscribeQuery** (interface)
5201
5212
  ```typescript
5202
5213
  interface UnsubscribeQuery {
5203
- contactId: string
5214
+ contactId?: string
5204
5215
  topic?: string
5205
5216
  channel?: BroadcastChannel
5206
5217
  token?: string
@@ -5214,10 +5225,26 @@ interface UnsubscribeResponse {
5214
5225
  }
5215
5226
  ```
5216
5227
 
5228
+ **CommsSignupRequest** (interface)
5229
+ ```typescript
5230
+ interface CommsSignupRequest {
5231
+ email: string
5232
+ topics?: string[]
5233
+ firstName?: string
5234
+ }
5235
+ ```
5236
+
5237
+ **CommsSignupResponse** (interface)
5238
+ ```typescript
5239
+ interface CommsSignupResponse {
5240
+ ok: true; pending: true
5241
+ }
5242
+ ```
5243
+
5217
5244
  **CommsConsentUpsertRequest** (interface)
5218
5245
  ```typescript
5219
5246
  interface CommsConsentUpsertRequest {
5220
- contactId: string
5247
+ contactId?: string
5221
5248
  channels?: ConsentChannels
5222
5249
  topics?: Record<string, boolean>
5223
5250
  topicsByChannel?: Partial<Record<BroadcastChannel, Record<string, boolean>>>
@@ -5227,7 +5254,7 @@ interface CommsConsentUpsertRequest {
5227
5254
  **CommsPreferencesUpsertRequest** (interface)
5228
5255
  ```typescript
5229
5256
  interface CommsPreferencesUpsertRequest {
5230
- contactId: string
5257
+ contactId?: string
5231
5258
  subject?: { type: SubjectType; id: string; productId?: string }
5232
5259
  channels?: ConsentChannels
5233
5260
  topics?: Record<string, boolean>
@@ -5238,7 +5265,7 @@ interface CommsPreferencesUpsertRequest {
5238
5265
  **CommsSubscribeRequest** (interface)
5239
5266
  ```typescript
5240
5267
  interface CommsSubscribeRequest {
5241
- contactId: string
5268
+ contactId?: string
5242
5269
  subject: { type: SubjectType; id: string; productId?: string }
5243
5270
  subscribe: boolean
5244
5271
  source?: string
@@ -5255,7 +5282,7 @@ interface CommsSubscribeResponse {
5255
5282
  **CommsSubscriptionCheckQuery** (interface)
5256
5283
  ```typescript
5257
5284
  interface CommsSubscriptionCheckQuery {
5258
- contactId: string
5285
+ contactId?: string
5259
5286
  subjectType: SubjectType
5260
5287
  subjectId: string
5261
5288
  productId?: string
@@ -5272,7 +5299,7 @@ interface CommsSubscriptionCheckResponse {
5272
5299
  **CommsListMethodsQuery** (interface)
5273
5300
  ```typescript
5274
5301
  interface CommsListMethodsQuery {
5275
- contactId: string
5302
+ contactId?: string
5276
5303
  type?: BroadcastChannel
5277
5304
  }
5278
5305
  ```
@@ -5502,7 +5529,8 @@ export interface PushSubscribeResponse { ok: true; id: string }
5502
5529
 
5503
5530
  // Public: register a push contact method
5504
5531
  export interface RegisterPushMethodRequest {
5505
- contactId: string
5532
+ /** Optional: defaults to the signed-in user's own contact. Only collection admins may name another contact. */
5533
+ contactId?: string
5506
5534
  endpoint: string
5507
5535
  keys: { p256dh: string; auth: string }
5508
5536
  meta?: Record<string, any>
@@ -5557,7 +5585,8 @@ export type CommsSettingsPatchBody = Partial<CommsSettings>
5557
5585
  export interface CommsPublicTopicsResponse { ok: true; topics: Record<string, TopicConfig> }
5558
5586
 
5559
5587
  export interface UnsubscribeQuery {
5560
- contactId: string
5588
+ /** Optional: defaults to the signed-in user's own contact. Only collection admins may name another contact. */
5589
+ contactId?: string
5561
5590
  topic?: string
5562
5591
  channel?: BroadcastChannel
5563
5592
  token?: string
@@ -5565,19 +5594,30 @@ export interface UnsubscribeQuery {
5565
5594
 
5566
5595
  export interface UnsubscribeResponse { ok: true; applied?: { channels?: Record<string, boolean>; topics?: Record<string, boolean> } }
5567
5596
 
5597
+ // Public sign-up (double opt-in)
5598
+ export interface CommsSignupRequest {
5599
+ email: string
5600
+ /** Topic ids from the collection's comm settings (e.g. ["newsletter"]). */
5601
+ topics?: string[]
5602
+ firstName?: string
5603
+ }
5604
+ export interface CommsSignupResponse { ok: true; pending: true }
5605
+
5568
5606
  // Public consent/preferences/subscribe
5569
5607
  export type ConsentChannels = Partial<Record<BroadcastChannel, boolean>>
5570
5608
  type SubjectType = import('./contact').SubjectType
5571
5609
 
5572
5610
  export interface CommsConsentUpsertRequest {
5573
- contactId: string
5611
+ /** Optional: defaults to the signed-in user's own contact. Only collection admins may name another contact. */
5612
+ contactId?: string
5574
5613
  channels?: ConsentChannels
5575
5614
  topics?: Record<string, boolean>
5576
5615
  topicsByChannel?: Partial<Record<BroadcastChannel, Record<string, boolean>>>
5577
5616
  }
5578
5617
 
5579
5618
  export interface CommsPreferencesUpsertRequest {
5580
- contactId: string
5619
+ /** Optional: defaults to the signed-in user's own contact. Only collection admins may name another contact. */
5620
+ contactId?: string
5581
5621
  subject?: { type: SubjectType; id: string; productId?: string }
5582
5622
  channels?: ConsentChannels
5583
5623
  topics?: Record<string, boolean>
@@ -5585,7 +5625,8 @@ export interface CommsPreferencesUpsertRequest {
5585
5625
  }
5586
5626
 
5587
5627
  export interface CommsSubscribeRequest {
5588
- contactId: string
5628
+ /** Optional: defaults to the signed-in user's own contact. Only collection admins may name another contact. */
5629
+ contactId?: string
5589
5630
  subject: { type: SubjectType; id: string; productId?: string }
5590
5631
  subscribe: boolean
5591
5632
  source?: string
@@ -5593,7 +5634,8 @@ export interface CommsSubscribeRequest {
5593
5634
  export interface CommsSubscribeResponse { ok: true; subscriptionId: string }
5594
5635
 
5595
5636
  export interface CommsSubscriptionCheckQuery {
5596
- contactId: string
5637
+ /** Optional: defaults to the signed-in user's own contact. Only collection admins may name another contact. */
5638
+ contactId?: string
5597
5639
  subjectType: SubjectType
5598
5640
  subjectId: string
5599
5641
  productId?: string
@@ -5601,7 +5643,8 @@ export interface CommsSubscriptionCheckQuery {
5601
5643
  export interface CommsSubscriptionCheckResponse { ok: true; subscribed: boolean }
5602
5644
 
5603
5645
  export interface CommsListMethodsQuery {
5604
- contactId: string
5646
+ /** Optional: defaults to the signed-in user's own contact. Only collection admins may name another contact. */
5647
+ contactId?: string
5605
5648
  type?: BroadcastChannel
5606
5649
  }
5607
5650
  export interface CommsListMethodsResponse { ok: true; methods: import('./contact').CommMethod[] }
@@ -10921,82 +10964,86 @@ Invite a user to a collection by email (admin only). Creates or links their acco
10921
10964
  **getPushVapidPublicKey**(collectionId: string) → `Promise<import("../types/comms").PushVapidResponse>`
10922
10965
  Public: Get VAPID public key used for Web Push subscriptions. GET /public/collection/:collectionId/comm/push/vapidPublicKey Note: Key may be global; path is collection-scoped for consistency.
10923
10966
 
10924
- **registerPush**(collectionId: string,
10967
+ **registerPush**(collectionId: string,
10925
10968
  body: import("../types/comms") → `void`
10926
10969
  Public: Register a Web Push subscription under unified comms. POST /public/collection/:collectionId/comm/push/register
10927
10970
 
10928
- **getSettings**(collectionId: string,
10971
+ **getSettings**(collectionId: string,
10929
10972
  opts: { includeSecret?: boolean } = {}) → `Promise<import("../types/comms").CommsSettingsGetResponse>`
10930
10973
  Admin: Get current comms settings for a collection. GET /admin/collection/:collectionId/comm.settings Optional query: includeSecret=true to include unsub.secret in response.
10931
10974
 
10932
- **patchSettings**(collectionId: string,
10975
+ **patchSettings**(collectionId: string,
10933
10976
  body: import("../types/comms") → `void`
10934
10977
  Admin: Patch comms settings for a collection. PATCH /admin/collection/:collectionId/comm.settings
10935
10978
 
10936
10979
  **getPublicTopics**(collectionId: string) → `Promise<import("../types/comms").CommsPublicTopicsResponse>`
10937
10980
  Public: Fetch configured topics for a collection. GET /public/collection/:collectionId/comm/topics
10938
10981
 
10939
- **unsubscribe**(collectionId: string,
10982
+ **unsubscribe**(collectionId: string,
10940
10983
  query: import("../types/comms") → `void`
10941
10984
  Public: Unsubscribe a contact from a category or channel. GET /public/collection/:collectionId/comm/unsubscribe
10942
10985
 
10943
- **upsertConsent**(collectionId: string,
10986
+ **signup**(collectionId: string,
10987
+ body: import("../types/comms") → `void`
10988
+ Public: sign an email address up for topics (a newsletter, updates) — works signed out. Double opt-in: nothing is consented until the address's owner clicks the confirmation link SmartLinks emails them. Always answers `{ ok: true, pending: true }` (it never reveals whether the address is already known). Topics must be ones configured in the collection's comm settings. Set comm settings `signup.confirmedUrl` to send people to your own page after confirming. POST /public/collection/:collectionId/comm/signup
10989
+
10990
+ **upsertConsent**(collectionId: string,
10944
10991
  body: import("../types/comms") → `void`
10945
10992
  Public: Upsert default consent for a contact. POST /public/collection/:collectionId/comm/consent
10946
10993
 
10947
- **upsertPreferences**(collectionId: string,
10994
+ **upsertPreferences**(collectionId: string,
10948
10995
  body: import("../types/comms") → `void`
10949
10996
  Public: Upsert preferences for a specific subject (or default if subject omitted). POST /public/collection/:collectionId/comm/preferences
10950
10997
 
10951
- **subscribe**(collectionId: string,
10998
+ **subscribe**(collectionId: string,
10952
10999
  body: import("../types/comms") → `void`
10953
11000
  Public: Subscribe/unsubscribe contact to a subject. POST /public/collection/:collectionId/comm/subscribe
10954
11001
 
10955
- **checkSubscription**(collectionId: string,
11002
+ **checkSubscription**(collectionId: string,
10956
11003
  query: import("../types/comms") → `void`
10957
11004
  Public: Check subscription status for a subject. GET /public/collection/:collectionId/comm/subscription/check
10958
11005
 
10959
- **listMethods**(collectionId: string,
11006
+ **listMethods**(collectionId: string,
10960
11007
  query: import("../types/comms") → `void`
10961
11008
  Public: List registered contact methods. GET /public/collection/:collectionId/comm/methods
10962
11009
 
10963
- **registerEmail**(collectionId: string,
11010
+ **registerEmail**(collectionId: string,
10964
11011
  body: import("../types/comms") → `void`
10965
11012
  Public: Register email method for a contact. POST /public/collection/:collectionId/comm/email/register
10966
11013
 
10967
- **registerSms**(collectionId: string,
11014
+ **registerSms**(collectionId: string,
10968
11015
  body: import("../types/comms") → `void`
10969
11016
  Public: Register SMS method for a contact. POST /public/collection/:collectionId/comm/sms/register
10970
11017
 
10971
- **resolveSubscriptions**(collectionId: string,
11018
+ **resolveSubscriptions**(collectionId: string,
10972
11019
  body: import("../types/comms") → `void`
10973
11020
  Public: Resolve contacts for a subject with identity hints. POST /public/collection/:collectionId/comm/subscriptions/resolve
10974
11021
 
10975
- **queryByUser**(collectionId: string,
11022
+ **queryByUser**(collectionId: string,
10976
11023
  body: CommsQueryByUser = {}) → `Promise<CommunicationEvent[]>`
10977
11024
  Analytics: Query communication events by user or contact. POST /admin/collection/:collectionId/comm/query/by-user
10978
11025
 
10979
- **queryRecipientIds**(collectionId: string,
11026
+ **queryRecipientIds**(collectionId: string,
10980
11027
  body: CommsRecipientIdsQuery) → `Promise<RecipientId[]>`
10981
11028
  Analytics: Recipient IDs for a communication source. POST /admin/collection/:collectionId/comm/query/recipient-ids
10982
11029
 
10983
- **queryRecipientsWithoutAction**(collectionId: string,
11030
+ **queryRecipientsWithoutAction**(collectionId: string,
10984
11031
  body: CommsRecipientsWithoutActionQuery) → `Promise<RecipientId[]>`
10985
11032
  Analytics: Recipients who did not perform an action. POST /admin/collection/:collectionId/comm/query/recipients/without-action
10986
11033
 
10987
- **queryRecipientsWithAction**(collectionId: string,
11034
+ **queryRecipientsWithAction**(collectionId: string,
10988
11035
  body: CommsRecipientsWithActionQuery) → `Promise<RecipientId[] | RecipientWithOutcome[]>`
10989
11036
  Analytics: Recipients who performed an action, optionally with outcome. POST /admin/collection/:collectionId/comm/query/recipients/with-action
10990
11037
 
10991
- **sendTransactional**(collectionId: string,
11038
+ **sendTransactional**(collectionId: string,
10992
11039
  body: TransactionalSendRequest) → `Promise<TransactionalSendResult>`
10993
11040
  Send a single transactional message to one contact using a template. No broadcast record is created. The send is logged to the contact's communication history with sourceType: 'transactional'. POST /admin/collection/:collectionId/comm/send ```typescript const result = await comms.sendTransactional(collectionId, { contactId: 'e4f2a1b0-...', templateId: 'warranty-update', channel: 'preferred', props: { claimRef: 'CLM-0042', decision: 'approved' }, include: { productId: 'prod-abc123', appCase: 'c9d1e2f3-...' }, ref: 'warranty-decision-notification', appId: 'warrantyApp', }) if (result.ok) { console.log(`Sent via ${result.channel}`, result.messageId) } else { console.error('Send failed:', result.error) } ```
10994
11041
 
10995
- **logCommunicationEvent**(collectionId: string,
11042
+ **logCommunicationEvent**(collectionId: string,
10996
11043
  body: LogCommunicationEventBody) → `Promise<AppendResult>`
10997
11044
  Logging: Append a single communication event. POST /admin/collection/:collectionId/comm/log
10998
11045
 
10999
- **logBulkCommunicationEvents**(collectionId: string,
11046
+ **logBulkCommunicationEvents**(collectionId: string,
11000
11047
  body: LogBulkCommunicationEventsBody | ({ sourceId: string; ids: string[]; idField?: 'userId'|'contactId'; [k: string]: any }) → `void`
11001
11048
  Logging: Append many communication events for a list of IDs. POST /admin/collection/:collectionId/comm/log/bulk
11002
11049
 
@@ -313,9 +313,30 @@ function effectivePolicy(topic: any, channel: 'email' | 'sms' | 'push' | 'wallet
313
313
  }
314
314
  ```
315
315
 
316
+ ### Sign-up with double opt-in (newsletters)
317
+
318
+ For a sign-up form — works signed out. SmartLinks emails the address a "confirm your subscription"
319
+ link; nothing is consented until it's clicked. The answer is always the same, so the form can't be
320
+ used to find out who's registered.
321
+
322
+ ```typescript
323
+ await comms.signup('collectionId', { email: 'sam@example.com', topics: ['newsletter'] })
324
+ // → { ok: true, pending: true } — tell the visitor to check their inbox
325
+ ```
326
+
327
+ - Topics must exist in the collection's comm settings. Give a newsletter topic
328
+ `defaults.policy: 'opt-in'`: broadcasts on an opt-in topic only reach people who said yes
329
+ (confirmed sign-ups), never the whole contact list.
330
+ - Confirming turns on the email channel and the topics, and marks the address verified.
331
+ - Set comm settings `signup.confirmedUrl` (https) to send people to your own "thanks" page after
332
+ they confirm (`?confirmed=1`, or `0` for an expired link); otherwise SmartLinks shows a short page.
333
+
316
334
  ### Consent (Default)
317
335
 
318
- Record a contact's default channel and topic opt-ins.
336
+ Record a contact's default channel and topic opt-ins. These calls (consent, preferences, subscribe,
337
+ methods, push/email/SMS registration) act for the **signed-in** contact — `contactId` may be omitted
338
+ to mean "me"; only collection admins may name another contact. An email registered here is verified
339
+ only if it's the one the user signed in with; any other address is sent a confirmation link.
319
340
 
320
341
  ```typescript
321
342
  await comms.upsertConsent('collectionId', {
@@ -165,6 +165,17 @@ The canonical channels are **`dev` · `alpha` · `beta` · `stable`**. Legacy `p
165
165
  A collection chooses which channel it follows, so you can point a test collection at `dev`
166
166
  and exercise a build before it reaches `prod`.
167
167
 
168
+ ### `preview` — server code while you build (Forge)
169
+
170
+ Forge publishes an app's **server functions only** to a private `preview` channel whenever the
171
+ agent changes them, so the app's own preview can call the new code straight away. It is not a
172
+ channel a collection can follow, release to or promote from. A preview call runs only with the
173
+ **preview pass** Forge gives its preview — for one app on one collection, for a few hours — so
174
+ nobody else ever runs in-progress server code, while the builder can preview against any
175
+ collection they administer. The SDK sends the pass automatically (`SL.functions` with
176
+ `appChannel: 'preview'` and `slPreview` in the context); apps need no code for it. `dev` only
177
+ changes when you choose to release to it.
178
+
168
179
  ---
169
180
 
170
181
  ## Deploy keys
@@ -651,6 +651,78 @@ Widgets use semantic class names that reference these variables:
651
651
  <Button className="bg-blue-500 text-white">
652
652
  ```
653
653
 
654
+ ### Style support — how a widget takes styling from where it's placed
655
+
656
+ Hosts that place widgets (Link Page, Hub, …) offer per-widget styling: width, alignment, padding,
657
+ scale, corners, border, background, and colours. The host only ever styles the **box** around the
658
+ widget; it never reaches inside (no selector tricks, no `!important`). So a widget **declares** what
659
+ it honours, the host shows only those controls, and colours reach the widget through variables.
660
+
661
+ **Declare it** on the widget's entry in `app.manifest.json` (`widgets.components[]`):
662
+
663
+ ```jsonc
664
+ {
665
+ "name": "TapToDonate",
666
+ "styleSupport": {
667
+ "accent": true, // reads --sl-widget-accent / --sl-widget-accent-contrast
668
+ "text": true, // reads --sl-widget-text
669
+ "textAlign": true, // titles/messages use text-align: inherit
670
+ "background": "transparent" // or "own" if it paints its own card
671
+ }
672
+ }
673
+ ```
674
+
675
+ List only what you actually implemented. No `styleSupport` (older widgets) → the host offers layout
676
+ controls only and says the widget has no colour customisation; nothing breaks.
677
+
678
+ | Setting | Owner | Works when |
679
+ |---|---|---|
680
+ | width, alignment, padding, scale | Host | always (scale: keep 60–130%) |
681
+ | radius, border | Host | always — visible when the widget is transparent or fills its box |
682
+ | background | Host | the widget is transparent (hidden when `background: "own"`) |
683
+ | text | Widget | declared, and the widget reads `--sl-widget-text` |
684
+ | accent | Widget | declared, and the widget reads `--sl-widget-accent` + `--sl-widget-accent-contrast` |
685
+ | textAlign | Widget | declared, and the widget uses `text-align: inherit` |
686
+ | anything else (shapes, button style…) | Widget | the widget's own `settings` schema — not host styling |
687
+
688
+ **Variables the host sets** on the box (only the ones chosen):
689
+
690
+ ```css
691
+ --sl-widget-accent /* hex */
692
+ --sl-widget-accent-contrast /* readable text on the accent: #111111 or #ffffff */
693
+ --sl-widget-text /* hex */
694
+ --sl-widget-radius /* e.g. 12px — use it on your own rounded elements */
695
+ ```
696
+
697
+ **Widget rules**
698
+
699
+ 1. **Transparent by default** — the root element has no background, border or padding of its own.
700
+ Need a card look? Declare `background: "own"` and make the card one of the widget's own settings.
701
+ 2. **Read the variables with fallbacks** to the theme tokens, for every colour that should follow:
702
+ `background: var(--sl-widget-accent, var(--sl-color-accent));`
703
+ `color: var(--sl-widget-accent-contrast, var(--sl-color-on-accent));`
704
+ `color: var(--sl-widget-text, inherit);`
705
+ 3. **Inherit text** — `color: inherit` / `text-align: inherit` on headings and messages if you declare
706
+ `text` / `textAlign`.
707
+ 4. **No hardcoded colours** for buttons, links or focus rings — accent + accent-contrast.
708
+ 5. **No global CSS** — ship scoped styles; nothing may affect elements outside the widget's box.
709
+
710
+ **Hosts** use the SDK helpers so every host behaves the same:
711
+
712
+ ```ts
713
+ import { widgetStyleControls, widgetStyleVars } from '@proveanything/smartlinks'
714
+
715
+ const controls = widgetStyleControls(component.styleSupport) // which controls to show
716
+ const vars = widgetStyleVars({ accent, text, radius }, component.styleSupport) // CSS vars for the box
717
+ ```
718
+
719
+ Conformance checklist (per widget, in the host's editor preview and live): unstyled looks as before
720
+ (light and dark); Background fills the whole box (unless `own`); Text recolours all text; Accent
721
+ recolours every button, link, focus ring and active state, and labels stay readable (try yellow and
722
+ near-black); undeclared controls are absent; padding 0 and 40 don't clip or double up; radius 0 and
723
+ 32 with a border clip cleanly; width 50% at each alignment; scale 60% and 130% don't overlap
724
+ neighbours; dialogs opened from the widget are unaffected; nothing outside the box changes.
725
+
654
726
  ---
655
727
 
656
728
  ## Best Practices
package/dist/index.d.ts CHANGED
@@ -3,6 +3,8 @@ export * from "./api/index.js";
3
3
  export * from "./types/index.js";
4
4
  export { iframe } from "./iframe.js";
5
5
  export { readContext } from "./context.js";
6
+ export { widgetStyleVars, widgetStyleControls, readableOn, WIDGET_STYLE_VARS } from "./widgetStyle.js";
7
+ export type { WidgetStyling } from "./widgetStyle.js";
6
8
  export type { SmartLinksContext } from "./context.js";
7
9
  export type { InvalidateCacheOptions } from "./http.js";
8
10
  export * as cache from './cache.js';
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ export * from "./types/index.js";
7
7
  export { iframe } from "./iframe.js";
8
8
  // App context reader (props → hash → search), for pages and containers alike
9
9
  export { readContext } from "./context.js";
10
+ export { widgetStyleVars, widgetStyleControls, readableOn, WIDGET_STYLE_VARS } from "./widgetStyle.js";
10
11
  import * as cache_1 from './cache.js';
11
12
  export { cache_1 as cache };
12
13
  // IframeResponder (also exported via iframe namespace)