@proveanything/smartlinks 1.11.5 → 1.11.7

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,4 +1,4 @@
1
- import type { AdminInteractionsCountsByOutcomeRequest, AdminInteractionsQueryRequest, AdminInteractionsAggregateRequest, AdminInteractionsAggregateResponse, AppendInteractionBody, UpdateInteractionBody, OutcomeCount, InteractionEventRow, PublicInteractionsCountsByOutcomeRequest, PublicInteractionsByUserRequest, CreateInteractionTypeBody, UpdateInteractionTypeBody, ListInteractionTypesQuery, InteractionTypeRecord, InteractionTypeList } from "../types/interaction";
1
+ import type { AdminInteractionsCountsByOutcomeRequest, AdminInteractionsQueryRequest, AdminInteractionsAggregateRequest, AdminInteractionsAggregateResponse, AppendInteractionBody, UpdateInteractionBody, OutcomeCount, InteractionEventRow, PublicInteractionsCountsByOutcomeRequest, PublicInteractionsByUserRequest, SubmitInteractionResponse, SubmitInteractionError, CreateInteractionTypeBody, UpdateInteractionTypeBody, ListInteractionTypesQuery, InteractionTypeRecord, InteractionTypeList } from "../types/interaction";
2
2
  export declare namespace interactions {
3
3
  /**
4
4
  * POST /admin/collection/:collectionId/interactions/query
@@ -30,11 +30,14 @@ export declare namespace interactions {
30
30
  success: true;
31
31
  }>;
32
32
  /**
33
- * Appends one interaction event from a public source.
33
+ * POST /api/v1/public/collection/:collectionId/interactions/submit
34
+ *
35
+ * Submits an interaction event from a public/client-side context.
36
+ * When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor
37
+ * `contactId` is required. Pass `anonId` inside `metadata` to enable
38
+ * device-level deduplication via `uniquePerAnonId`.
34
39
  */
35
- function submitPublicEvent(collectionId: string, body: AppendInteractionBody): Promise<{
36
- success: true;
37
- }>;
40
+ function submitPublicEvent(collectionId: string, body: AppendInteractionBody): Promise<SubmitInteractionResponse | SubmitInteractionError>;
38
41
  function create(collectionId: string, body: CreateInteractionTypeBody): Promise<InteractionTypeRecord>;
39
42
  function list(collectionId: string, query?: ListInteractionTypesQuery): Promise<InteractionTypeList>;
40
43
  function get(collectionId: string, id: string): Promise<InteractionTypeRecord>;
@@ -73,12 +73,14 @@ export var interactions;
73
73
  }
74
74
  interactions.updateEvent = updateEvent;
75
75
  /**
76
- * Appends one interaction event from a public source.
76
+ * POST /api/v1/public/collection/:collectionId/interactions/submit
77
+ *
78
+ * Submits an interaction event from a public/client-side context.
79
+ * When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor
80
+ * `contactId` is required. Pass `anonId` inside `metadata` to enable
81
+ * device-level deduplication via `uniquePerAnonId`.
77
82
  */
78
83
  async function submitPublicEvent(collectionId, body) {
79
- if (!body.userId && !body.contactId) {
80
- throw new Error("AppendInteractionBody must include one of userId or contactId");
81
- }
82
84
  const path = `/public/collection/${encodeURIComponent(collectionId)}/interactions/submit`;
83
85
  return post(path, body);
84
86
  }
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.11.5 | Generated: 2026-04-30T16:31:53.616Z
3
+ Version: 1.11.7 | Generated: 2026-05-02T09:06:52.034Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -5153,6 +5153,10 @@ interface InteractionPermissions {
5153
5153
  * Authenticated summary visibility (counts, aggregates) when user is signed in.
5154
5154
  allowAuthenticatedSummary?: boolean
5155
5155
  allowOwnRead?: boolean
5156
+ uniquePerAnonId?: boolean
5157
+ * Time window in seconds for `uniquePerAnonId` enforcement.
5158
+ * `0` or omitted means all-time deduplication.
5159
+ uniquePerAnonIdWindowSeconds?: number
5156
5160
  }
5157
5161
  ```
5158
5162
 
@@ -5220,6 +5224,30 @@ interface ListInteractionTypesQuery {
5220
5224
  }
5221
5225
  ```
5222
5226
 
5227
+ **SubmitInteractionResponse** (interface)
5228
+ ```typescript
5229
+ interface SubmitInteractionResponse {
5230
+ success: true
5231
+ eventId: string
5232
+ }
5233
+ ```
5234
+
5235
+ **SubmitInteractionError** (interface)
5236
+ ```typescript
5237
+ interface SubmitInteractionError {
5238
+ error: 'FORBIDDEN'
5239
+ reason:
5240
+ | 'not_public'
5241
+ | 'auth_required'
5242
+ | 'duplicate'
5243
+ | 'duplicate_anon'
5244
+ | 'disabled'
5245
+ | 'before_start'
5246
+ | 'after_end'
5247
+ | 'origin_forbidden'
5248
+ }
5249
+ ```
5250
+
5223
5251
  ### jobs
5224
5252
 
5225
5253
  **Job** (interface)
@@ -8333,47 +8361,47 @@ POST /admin/collection/:collectionId/interactions/append Appends one interaction
8333
8361
  POST /admin/collection/:collectionId/interactions/append Appends one interaction event.
8334
8362
 
8335
8363
  **submitPublicEvent**(collectionId: string,
8336
- body: AppendInteractionBody) → `Promise<`
8337
- Appends one interaction event from a public source.
8364
+ body: AppendInteractionBody) → `Promise<SubmitInteractionResponse | SubmitInteractionError>`
8365
+ POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
8338
8366
 
8339
8367
  **create**(collectionId: string,
8340
8368
  body: CreateInteractionTypeBody) → `Promise<InteractionTypeRecord>`
8341
- Appends one interaction event from a public source.
8369
+ POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
8342
8370
 
8343
8371
  **list**(collectionId: string,
8344
8372
  query: ListInteractionTypesQuery = {}) → `Promise<InteractionTypeList>`
8345
- Appends one interaction event from a public source.
8373
+ POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
8346
8374
 
8347
8375
  **get**(collectionId: string,
8348
8376
  id: string) → `Promise<InteractionTypeRecord>`
8349
- Appends one interaction event from a public source.
8377
+ POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
8350
8378
 
8351
8379
  **update**(collectionId: string,
8352
8380
  id: string,
8353
8381
  patchBody: UpdateInteractionTypeBody) → `Promise<InteractionTypeRecord>`
8354
- Appends one interaction event from a public source.
8382
+ POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
8355
8383
 
8356
8384
  **remove**(collectionId: string,
8357
8385
  id: string) → `Promise<void>`
8358
- Appends one interaction event from a public source.
8386
+ POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
8359
8387
 
8360
8388
  **publicCountsByOutcome**(collectionId: string,
8361
8389
  body: PublicInteractionsCountsByOutcomeRequest,
8362
8390
  authToken?: string) → `Promise<OutcomeCount[]>`
8363
- Appends one interaction event from a public source.
8391
+ POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
8364
8392
 
8365
8393
  **publicMyInteractions**(collectionId: string,
8366
8394
  body: PublicInteractionsByUserRequest,
8367
8395
  authToken?: string) → `Promise<InteractionEventRow[]>`
8368
- Appends one interaction event from a public source.
8396
+ POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
8369
8397
 
8370
8398
  **publicList**(collectionId: string,
8371
8399
  query: ListInteractionTypesQuery = {}) → `Promise<InteractionTypeList>`
8372
- Appends one interaction event from a public source.
8400
+ POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
8373
8401
 
8374
8402
  **publicGet**(collectionId: string,
8375
8403
  id: string) → `Promise<InteractionTypeRecord>`
8376
- Appends one interaction event from a public source.
8404
+ POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
8377
8405
 
8378
8406
  ### jobs
8379
8407
 
@@ -85,6 +85,7 @@ Zones are **automatically filtered** based on the caller's role:
85
85
  ### Zone Writing Rules
86
86
 
87
87
  - **Non-admin callers** attempting to write to the `admin` zone are silently ignored
88
+ - **Authenticated record owners** can write to `data` and `owner` by default; individual keys can be restricted via the `ownerEdit` app config policy (see [Owner Edit Policy](#owner-edit-policy) below)
88
89
  - **Public callers** can write to `data` and `owner` (if visibility allows)
89
90
  - **Admins** can write to all three zones
90
91
 
@@ -1098,6 +1099,61 @@ The `enforce` values are **merged over** the caller's request body, so you can l
1098
1099
 
1099
1100
  ---
1100
1101
 
1102
+ ## Owner Edit Policy
1103
+
1104
+ Gives per-zone, field-level control over what an **authenticated record owner** can update via `PATCH /api/v1/public/collection/:collectionId/app/:appId/records/:recordId`.
1105
+
1106
+ Set the policy in the same app config document used for `publicCreate` (stored at `sites/{collectionId}/apps/{appId}`):
1107
+
1108
+ ```json
1109
+ {
1110
+ "ownerEdit": {
1111
+ "records": {
1112
+ "data": { "allow": ["paypalEmail"] },
1113
+ "owner": { "allow": ["paypalEmail", "paypalEmailUpdatedAt"] }
1114
+ }
1115
+ }
1116
+ }
1117
+ ```
1118
+
1119
+ ### Zone visibility and write access
1120
+
1121
+ | Zone | Who can read | Who can write (owner) |
1122
+ |---------|------------------------|----------------------------------------------------------|
1123
+ | `data` | public | Allow-listed keys only (if policy set); all keys if not |
1124
+ | `owner` | owner + admin | Allow-listed keys only (if policy set); all keys if not |
1125
+ | `admin` | admin | Never — admin zone is always immutable to owners |
1126
+
1127
+ ### Allow-list semantics
1128
+
1129
+ | Config | Behaviour |
1130
+ |----------------------------|-------------------------------------------------------------------------------|
1131
+ | No `ownerEdit` key | Default-allow — both zones fully writable (no change to existing behaviour) |
1132
+ | `allow` array with keys | Only the listed keys are accepted from the PATCH body; the rest are silently ignored and their existing values preserved |
1133
+ | `allow: []` (empty array) | Zone is effectively read-only for the owner |
1134
+
1135
+ Accepted keys are **merged** onto the existing zone blob — you do not need to re-send unchanged values.
1136
+
1137
+ ### Example: commission record with protected fields
1138
+
1139
+ An app that lets owners update their payout email but not their commission total:
1140
+
1141
+ ```json
1142
+ {
1143
+ "ownerEdit": {
1144
+ "records": {
1145
+ "owner": { "allow": ["paypalEmail", "paypalEmailUpdatedAt"] }
1146
+ }
1147
+ }
1148
+ }
1149
+ ```
1150
+
1151
+ A PATCH body of `{ "owner": { "paypalEmail": "x@y.com", "totalCommission": 99 } }` will update `paypalEmail` only. `totalCommission` is silently ignored and its existing value is preserved.
1152
+
1153
+ > **App design note:** If your app creates records with sensitive fields that owners should never modify (e.g. computed totals, server-assigned fields), add an `ownerEdit` policy from the start. It is significantly easier to relax restrictions later than to tighten them after data has been mutated.
1154
+
1155
+ ---
1156
+
1101
1157
  ## Anonymous Edit Tokens
1102
1158
 
1103
1159
  Enables an anonymous caller to amend a record they just created — without authentication — by presenting a short-lived secret token.
@@ -119,18 +119,37 @@ await SL.interactions.appendEvent(collectionId, {
119
119
 
120
120
  ### Public Event Submit
121
121
 
122
- Use in client-side app code. Same body shape as `appendEvent` but hits the public endpoint (respects interaction permissions like `allowPublicSubmit`, `requireAuth`).
122
+ Use in client-side app code. Hits the public endpoint and respects interaction permissions (`allowPublicSubmit`, `allowAnonymousSubmit`, `requireAuth`, etc.).
123
123
 
124
124
  ```typescript
125
+ // Authenticated submission
125
126
  await SL.interactions.submitPublicEvent(collectionId, {
126
127
  appId: 'my-app',
127
128
  interactionId: 'competition-entry',
128
129
  outcome: 'entered',
129
- userId: currentUser.id,
130
+ contactId: currentUser.contactId,
130
131
  metadata: { answer: 'Paris' },
131
132
  });
133
+
134
+ // Anonymous submission (interaction must have allowAnonymousSubmit: true)
135
+ const response = await SL.interactions.submitPublicEvent(collectionId, {
136
+ appId: 'my-app',
137
+ interactionId: 'nps-score',
138
+ outcome: '9',
139
+ metadata: {
140
+ anonId: SL.utils.getAnonId(), // device-level dedup signal
141
+ },
142
+ });
143
+
144
+ if (!response.success) {
145
+ if (response.reason === 'duplicate_anon') {
146
+ // this device has already submitted
147
+ }
148
+ }
132
149
  ```
133
150
 
151
+ > **Anonymous submissions** — when `allowAnonymousSubmit: true` is set on the interaction, neither `userId` nor `contactId` is required. Use `utils.getAnonId()` to generate a stable browser-local UUID and pass it as `metadata.anonId`; the server will enforce `uniquePerAnonId` if configured.
152
+
134
153
  ### Update an Existing Event
135
154
 
136
155
  ```typescript
@@ -237,6 +256,8 @@ Set on the interaction type definition via `permissions`:
237
256
  | `uniquePerUser` | boolean | Prevent duplicate submissions per user |
238
257
  | `uniquePerUserWindowSeconds` | number | Time window for uniqueness (e.g., `86400` = 1 day) |
239
258
  | `uniqueOutcome` | string | Outcome tag to check for duplicates (e.g., `"submitted"`) |
259
+ | `uniquePerAnonId` | boolean | Reject a second submission that carries the same `anonId` in metadata |
260
+ | `uniquePerAnonIdWindowSeconds` | number | Time window for `uniquePerAnonId` enforcement; `0` or omitted = all-time |
240
261
  | `allowPublicSummary` | boolean | Show counts/aggregates to unauthenticated users |
241
262
  | `allowAuthenticatedSummary` | boolean | Show counts/aggregates to authenticated users |
242
263
  | `allowOwnRead` | boolean | Let users read their own event history via public API |
@@ -263,8 +284,10 @@ When defining a journey trigger, reference the `interactionId` that should fire
263
284
 
264
285
  ```typescript
265
286
  import type {
266
- AppendInteractionBody, // Event body for appendEvent / submitPublicEvent
287
+ AppendInteractionBody, // Event body for appendEvent and submitPublicEvent
267
288
  UpdateInteractionBody, // Event body for updateEvent
289
+ SubmitInteractionResponse, // { success: true; eventId: string }
290
+ SubmitInteractionError, // { error: 'FORBIDDEN'; reason: string }
268
291
  InteractionEventRow, // Raw event record returned by query()
269
292
  OutcomeCount, // { outcome: string | null; count: number }
270
293
  InteractionPermissions, // Full permissions config shape
package/dist/openapi.yaml CHANGED
@@ -10355,6 +10355,40 @@ paths:
10355
10355
  application/json:
10356
10356
  schema:
10357
10357
  $ref: "#/components/schemas/PublicInteractionsCountsByOutcomeRequest"
10358
+ /public/collection/{collectionId}/interactions/submit:
10359
+ post:
10360
+ tags:
10361
+ - interactions
10362
+ summary: "POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context."
10363
+ operationId: interactions_submitPublicEvent
10364
+ security: []
10365
+ parameters:
10366
+ - name: collectionId
10367
+ in: path
10368
+ required: true
10369
+ schema:
10370
+ type: string
10371
+ responses:
10372
+ 200:
10373
+ description: Success
10374
+ content:
10375
+ application/json:
10376
+ schema:
10377
+ oneOf:
10378
+ - $ref: "#/components/schemas/SubmitInteractionResponse"
10379
+ - $ref: "#/components/schemas/SubmitInteractionError"
10380
+ 400:
10381
+ description: Bad request
10382
+ 401:
10383
+ description: Unauthorized
10384
+ 404:
10385
+ description: Not found
10386
+ requestBody:
10387
+ required: true
10388
+ content:
10389
+ application/json:
10390
+ schema:
10391
+ $ref: "#/components/schemas/AppendInteractionBody"
10358
10392
  /public/collection/{collectionId}/interactions/{id}:
10359
10393
  get:
10360
10394
  tags:
@@ -20187,6 +20221,10 @@ components:
20187
20221
  type: boolean
20188
20222
  allowOwnRead:
20189
20223
  type: boolean
20224
+ uniquePerAnonId:
20225
+ type: boolean
20226
+ uniquePerAnonIdWindowSeconds:
20227
+ type: number
20190
20228
  InteractionDisplay:
20191
20229
  type: object
20192
20230
  properties:
@@ -20275,6 +20313,26 @@ components:
20275
20313
  type: number
20276
20314
  offset:
20277
20315
  type: number
20316
+ SubmitInteractionResponse:
20317
+ type: object
20318
+ properties:
20319
+ success:
20320
+ type: object
20321
+ additionalProperties: true
20322
+ eventId:
20323
+ type: string
20324
+ required:
20325
+ - success
20326
+ - eventId
20327
+ SubmitInteractionError:
20328
+ type: object
20329
+ properties:
20330
+ error:
20331
+ type: string
20332
+ enum:
20333
+ - FORBIDDEN
20334
+ required:
20335
+ - error
20278
20336
  Job:
20279
20337
  type: object
20280
20338
  properties:
@@ -162,6 +162,13 @@ export interface InteractionPermissions {
162
162
  allowAuthenticatedSummary?: boolean;
163
163
  /** Allow an authenticated user to read their own interaction history via the public API. */
164
164
  allowOwnRead?: boolean;
165
+ /** Reject a second submission that carries the same `anonId` in metadata. */
166
+ uniquePerAnonId?: boolean;
167
+ /**
168
+ * Time window in seconds for `uniquePerAnonId` enforcement.
169
+ * `0` or omitted means all-time deduplication.
170
+ */
171
+ uniquePerAnonIdWindowSeconds?: number;
165
172
  }
166
173
  export interface InteractionDisplay {
167
174
  title?: string;
@@ -203,3 +210,11 @@ export interface ListInteractionTypesQuery {
203
210
  limit?: number;
204
211
  offset?: number;
205
212
  }
213
+ export interface SubmitInteractionResponse {
214
+ success: true;
215
+ eventId: string;
216
+ }
217
+ export interface SubmitInteractionError {
218
+ error: 'FORBIDDEN';
219
+ reason: 'not_public' | 'auth_required' | 'duplicate' | 'duplicate_anon' | 'disabled' | 'before_start' | 'after_end' | 'origin_forbidden';
220
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Returns a stable anonymous device ID stored in `localStorage`.
3
+ *
4
+ * On first call a random UUID is generated and persisted. All subsequent calls
5
+ * from the same browser return the same value. Returns `null` during SSR or in
6
+ * any environment without `window` (safe to call universally).
7
+ *
8
+ * The value is scoped to the browser's `localStorage` and is cleared if the
9
+ * user clears site data. It does **not** create any contact or user record on
10
+ * the server.
11
+ *
12
+ * Pass the result in `metadata.anonId` when submitting interactions to enable
13
+ * device-level deduplication via `uniquePerAnonId`.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { utils } from '@proveanything/smartlinks'
18
+ *
19
+ * const response = await interactions.submitPublicEvent(collectionId, {
20
+ * appId: 'my-app',
21
+ * interactionId: 'nps-score',
22
+ * outcome: '9',
23
+ * metadata: { anonId: utils.getAnonId() },
24
+ * })
25
+ * ```
26
+ */
27
+ export declare function getAnonId(): string | null;
@@ -0,0 +1,38 @@
1
+ // src/utils/anonId.ts
2
+ const ANON_ID_KEY = '_pa_anon_id';
3
+ /**
4
+ * Returns a stable anonymous device ID stored in `localStorage`.
5
+ *
6
+ * On first call a random UUID is generated and persisted. All subsequent calls
7
+ * from the same browser return the same value. Returns `null` during SSR or in
8
+ * any environment without `window` (safe to call universally).
9
+ *
10
+ * The value is scoped to the browser's `localStorage` and is cleared if the
11
+ * user clears site data. It does **not** create any contact or user record on
12
+ * the server.
13
+ *
14
+ * Pass the result in `metadata.anonId` when submitting interactions to enable
15
+ * device-level deduplication via `uniquePerAnonId`.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { utils } from '@proveanything/smartlinks'
20
+ *
21
+ * const response = await interactions.submitPublicEvent(collectionId, {
22
+ * appId: 'my-app',
23
+ * interactionId: 'nps-score',
24
+ * outcome: '9',
25
+ * metadata: { anonId: utils.getAnonId() },
26
+ * })
27
+ * ```
28
+ */
29
+ export function getAnonId() {
30
+ if (typeof window === 'undefined')
31
+ return null;
32
+ let id = window.localStorage.getItem(ANON_ID_KEY);
33
+ if (!id) {
34
+ id = crypto.randomUUID();
35
+ window.localStorage.setItem(ANON_ID_KEY, id);
36
+ }
37
+ return id;
38
+ }
@@ -4,3 +4,4 @@
4
4
  */
5
5
  export * from './paths';
6
6
  export * from './conditions';
7
+ export * from './anonId';
@@ -5,3 +5,4 @@
5
5
  */
6
6
  export * from './paths';
7
7
  export * from './conditions';
8
+ export * from './anonId';
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.11.5 | Generated: 2026-04-30T16:31:53.616Z
3
+ Version: 1.11.7 | Generated: 2026-05-02T09:06:52.034Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -5153,6 +5153,10 @@ interface InteractionPermissions {
5153
5153
  * Authenticated summary visibility (counts, aggregates) when user is signed in.
5154
5154
  allowAuthenticatedSummary?: boolean
5155
5155
  allowOwnRead?: boolean
5156
+ uniquePerAnonId?: boolean
5157
+ * Time window in seconds for `uniquePerAnonId` enforcement.
5158
+ * `0` or omitted means all-time deduplication.
5159
+ uniquePerAnonIdWindowSeconds?: number
5156
5160
  }
5157
5161
  ```
5158
5162
 
@@ -5220,6 +5224,30 @@ interface ListInteractionTypesQuery {
5220
5224
  }
5221
5225
  ```
5222
5226
 
5227
+ **SubmitInteractionResponse** (interface)
5228
+ ```typescript
5229
+ interface SubmitInteractionResponse {
5230
+ success: true
5231
+ eventId: string
5232
+ }
5233
+ ```
5234
+
5235
+ **SubmitInteractionError** (interface)
5236
+ ```typescript
5237
+ interface SubmitInteractionError {
5238
+ error: 'FORBIDDEN'
5239
+ reason:
5240
+ | 'not_public'
5241
+ | 'auth_required'
5242
+ | 'duplicate'
5243
+ | 'duplicate_anon'
5244
+ | 'disabled'
5245
+ | 'before_start'
5246
+ | 'after_end'
5247
+ | 'origin_forbidden'
5248
+ }
5249
+ ```
5250
+
5223
5251
  ### jobs
5224
5252
 
5225
5253
  **Job** (interface)
@@ -8333,47 +8361,47 @@ POST /admin/collection/:collectionId/interactions/append Appends one interaction
8333
8361
  POST /admin/collection/:collectionId/interactions/append Appends one interaction event.
8334
8362
 
8335
8363
  **submitPublicEvent**(collectionId: string,
8336
- body: AppendInteractionBody) → `Promise<`
8337
- Appends one interaction event from a public source.
8364
+ body: AppendInteractionBody) → `Promise<SubmitInteractionResponse | SubmitInteractionError>`
8365
+ POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
8338
8366
 
8339
8367
  **create**(collectionId: string,
8340
8368
  body: CreateInteractionTypeBody) → `Promise<InteractionTypeRecord>`
8341
- Appends one interaction event from a public source.
8369
+ POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
8342
8370
 
8343
8371
  **list**(collectionId: string,
8344
8372
  query: ListInteractionTypesQuery = {}) → `Promise<InteractionTypeList>`
8345
- Appends one interaction event from a public source.
8373
+ POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
8346
8374
 
8347
8375
  **get**(collectionId: string,
8348
8376
  id: string) → `Promise<InteractionTypeRecord>`
8349
- Appends one interaction event from a public source.
8377
+ POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
8350
8378
 
8351
8379
  **update**(collectionId: string,
8352
8380
  id: string,
8353
8381
  patchBody: UpdateInteractionTypeBody) → `Promise<InteractionTypeRecord>`
8354
- Appends one interaction event from a public source.
8382
+ POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
8355
8383
 
8356
8384
  **remove**(collectionId: string,
8357
8385
  id: string) → `Promise<void>`
8358
- Appends one interaction event from a public source.
8386
+ POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
8359
8387
 
8360
8388
  **publicCountsByOutcome**(collectionId: string,
8361
8389
  body: PublicInteractionsCountsByOutcomeRequest,
8362
8390
  authToken?: string) → `Promise<OutcomeCount[]>`
8363
- Appends one interaction event from a public source.
8391
+ POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
8364
8392
 
8365
8393
  **publicMyInteractions**(collectionId: string,
8366
8394
  body: PublicInteractionsByUserRequest,
8367
8395
  authToken?: string) → `Promise<InteractionEventRow[]>`
8368
- Appends one interaction event from a public source.
8396
+ POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
8369
8397
 
8370
8398
  **publicList**(collectionId: string,
8371
8399
  query: ListInteractionTypesQuery = {}) → `Promise<InteractionTypeList>`
8372
- Appends one interaction event from a public source.
8400
+ POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
8373
8401
 
8374
8402
  **publicGet**(collectionId: string,
8375
8403
  id: string) → `Promise<InteractionTypeRecord>`
8376
- Appends one interaction event from a public source.
8404
+ POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
8377
8405
 
8378
8406
  ### jobs
8379
8407
 
@@ -85,6 +85,7 @@ Zones are **automatically filtered** based on the caller's role:
85
85
  ### Zone Writing Rules
86
86
 
87
87
  - **Non-admin callers** attempting to write to the `admin` zone are silently ignored
88
+ - **Authenticated record owners** can write to `data` and `owner` by default; individual keys can be restricted via the `ownerEdit` app config policy (see [Owner Edit Policy](#owner-edit-policy) below)
88
89
  - **Public callers** can write to `data` and `owner` (if visibility allows)
89
90
  - **Admins** can write to all three zones
90
91
 
@@ -1098,6 +1099,61 @@ The `enforce` values are **merged over** the caller's request body, so you can l
1098
1099
 
1099
1100
  ---
1100
1101
 
1102
+ ## Owner Edit Policy
1103
+
1104
+ Gives per-zone, field-level control over what an **authenticated record owner** can update via `PATCH /api/v1/public/collection/:collectionId/app/:appId/records/:recordId`.
1105
+
1106
+ Set the policy in the same app config document used for `publicCreate` (stored at `sites/{collectionId}/apps/{appId}`):
1107
+
1108
+ ```json
1109
+ {
1110
+ "ownerEdit": {
1111
+ "records": {
1112
+ "data": { "allow": ["paypalEmail"] },
1113
+ "owner": { "allow": ["paypalEmail", "paypalEmailUpdatedAt"] }
1114
+ }
1115
+ }
1116
+ }
1117
+ ```
1118
+
1119
+ ### Zone visibility and write access
1120
+
1121
+ | Zone | Who can read | Who can write (owner) |
1122
+ |---------|------------------------|----------------------------------------------------------|
1123
+ | `data` | public | Allow-listed keys only (if policy set); all keys if not |
1124
+ | `owner` | owner + admin | Allow-listed keys only (if policy set); all keys if not |
1125
+ | `admin` | admin | Never — admin zone is always immutable to owners |
1126
+
1127
+ ### Allow-list semantics
1128
+
1129
+ | Config | Behaviour |
1130
+ |----------------------------|-------------------------------------------------------------------------------|
1131
+ | No `ownerEdit` key | Default-allow — both zones fully writable (no change to existing behaviour) |
1132
+ | `allow` array with keys | Only the listed keys are accepted from the PATCH body; the rest are silently ignored and their existing values preserved |
1133
+ | `allow: []` (empty array) | Zone is effectively read-only for the owner |
1134
+
1135
+ Accepted keys are **merged** onto the existing zone blob — you do not need to re-send unchanged values.
1136
+
1137
+ ### Example: commission record with protected fields
1138
+
1139
+ An app that lets owners update their payout email but not their commission total:
1140
+
1141
+ ```json
1142
+ {
1143
+ "ownerEdit": {
1144
+ "records": {
1145
+ "owner": { "allow": ["paypalEmail", "paypalEmailUpdatedAt"] }
1146
+ }
1147
+ }
1148
+ }
1149
+ ```
1150
+
1151
+ A PATCH body of `{ "owner": { "paypalEmail": "x@y.com", "totalCommission": 99 } }` will update `paypalEmail` only. `totalCommission` is silently ignored and its existing value is preserved.
1152
+
1153
+ > **App design note:** If your app creates records with sensitive fields that owners should never modify (e.g. computed totals, server-assigned fields), add an `ownerEdit` policy from the start. It is significantly easier to relax restrictions later than to tighten them after data has been mutated.
1154
+
1155
+ ---
1156
+
1101
1157
  ## Anonymous Edit Tokens
1102
1158
 
1103
1159
  Enables an anonymous caller to amend a record they just created — without authentication — by presenting a short-lived secret token.
@@ -119,18 +119,37 @@ await SL.interactions.appendEvent(collectionId, {
119
119
 
120
120
  ### Public Event Submit
121
121
 
122
- Use in client-side app code. Same body shape as `appendEvent` but hits the public endpoint (respects interaction permissions like `allowPublicSubmit`, `requireAuth`).
122
+ Use in client-side app code. Hits the public endpoint and respects interaction permissions (`allowPublicSubmit`, `allowAnonymousSubmit`, `requireAuth`, etc.).
123
123
 
124
124
  ```typescript
125
+ // Authenticated submission
125
126
  await SL.interactions.submitPublicEvent(collectionId, {
126
127
  appId: 'my-app',
127
128
  interactionId: 'competition-entry',
128
129
  outcome: 'entered',
129
- userId: currentUser.id,
130
+ contactId: currentUser.contactId,
130
131
  metadata: { answer: 'Paris' },
131
132
  });
133
+
134
+ // Anonymous submission (interaction must have allowAnonymousSubmit: true)
135
+ const response = await SL.interactions.submitPublicEvent(collectionId, {
136
+ appId: 'my-app',
137
+ interactionId: 'nps-score',
138
+ outcome: '9',
139
+ metadata: {
140
+ anonId: SL.utils.getAnonId(), // device-level dedup signal
141
+ },
142
+ });
143
+
144
+ if (!response.success) {
145
+ if (response.reason === 'duplicate_anon') {
146
+ // this device has already submitted
147
+ }
148
+ }
132
149
  ```
133
150
 
151
+ > **Anonymous submissions** — when `allowAnonymousSubmit: true` is set on the interaction, neither `userId` nor `contactId` is required. Use `utils.getAnonId()` to generate a stable browser-local UUID and pass it as `metadata.anonId`; the server will enforce `uniquePerAnonId` if configured.
152
+
134
153
  ### Update an Existing Event
135
154
 
136
155
  ```typescript
@@ -237,6 +256,8 @@ Set on the interaction type definition via `permissions`:
237
256
  | `uniquePerUser` | boolean | Prevent duplicate submissions per user |
238
257
  | `uniquePerUserWindowSeconds` | number | Time window for uniqueness (e.g., `86400` = 1 day) |
239
258
  | `uniqueOutcome` | string | Outcome tag to check for duplicates (e.g., `"submitted"`) |
259
+ | `uniquePerAnonId` | boolean | Reject a second submission that carries the same `anonId` in metadata |
260
+ | `uniquePerAnonIdWindowSeconds` | number | Time window for `uniquePerAnonId` enforcement; `0` or omitted = all-time |
240
261
  | `allowPublicSummary` | boolean | Show counts/aggregates to unauthenticated users |
241
262
  | `allowAuthenticatedSummary` | boolean | Show counts/aggregates to authenticated users |
242
263
  | `allowOwnRead` | boolean | Let users read their own event history via public API |
@@ -263,8 +284,10 @@ When defining a journey trigger, reference the `interactionId` that should fire
263
284
 
264
285
  ```typescript
265
286
  import type {
266
- AppendInteractionBody, // Event body for appendEvent / submitPublicEvent
287
+ AppendInteractionBody, // Event body for appendEvent and submitPublicEvent
267
288
  UpdateInteractionBody, // Event body for updateEvent
289
+ SubmitInteractionResponse, // { success: true; eventId: string }
290
+ SubmitInteractionError, // { error: 'FORBIDDEN'; reason: string }
268
291
  InteractionEventRow, // Raw event record returned by query()
269
292
  OutcomeCount, // { outcome: string | null; count: number }
270
293
  InteractionPermissions, // Full permissions config shape
package/openapi.yaml CHANGED
@@ -10355,6 +10355,40 @@ paths:
10355
10355
  application/json:
10356
10356
  schema:
10357
10357
  $ref: "#/components/schemas/PublicInteractionsCountsByOutcomeRequest"
10358
+ /public/collection/{collectionId}/interactions/submit:
10359
+ post:
10360
+ tags:
10361
+ - interactions
10362
+ summary: "POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context."
10363
+ operationId: interactions_submitPublicEvent
10364
+ security: []
10365
+ parameters:
10366
+ - name: collectionId
10367
+ in: path
10368
+ required: true
10369
+ schema:
10370
+ type: string
10371
+ responses:
10372
+ 200:
10373
+ description: Success
10374
+ content:
10375
+ application/json:
10376
+ schema:
10377
+ oneOf:
10378
+ - $ref: "#/components/schemas/SubmitInteractionResponse"
10379
+ - $ref: "#/components/schemas/SubmitInteractionError"
10380
+ 400:
10381
+ description: Bad request
10382
+ 401:
10383
+ description: Unauthorized
10384
+ 404:
10385
+ description: Not found
10386
+ requestBody:
10387
+ required: true
10388
+ content:
10389
+ application/json:
10390
+ schema:
10391
+ $ref: "#/components/schemas/AppendInteractionBody"
10358
10392
  /public/collection/{collectionId}/interactions/{id}:
10359
10393
  get:
10360
10394
  tags:
@@ -20187,6 +20221,10 @@ components:
20187
20221
  type: boolean
20188
20222
  allowOwnRead:
20189
20223
  type: boolean
20224
+ uniquePerAnonId:
20225
+ type: boolean
20226
+ uniquePerAnonIdWindowSeconds:
20227
+ type: number
20190
20228
  InteractionDisplay:
20191
20229
  type: object
20192
20230
  properties:
@@ -20275,6 +20313,26 @@ components:
20275
20313
  type: number
20276
20314
  offset:
20277
20315
  type: number
20316
+ SubmitInteractionResponse:
20317
+ type: object
20318
+ properties:
20319
+ success:
20320
+ type: object
20321
+ additionalProperties: true
20322
+ eventId:
20323
+ type: string
20324
+ required:
20325
+ - success
20326
+ - eventId
20327
+ SubmitInteractionError:
20328
+ type: object
20329
+ properties:
20330
+ error:
20331
+ type: string
20332
+ enum:
20333
+ - FORBIDDEN
20334
+ required:
20335
+ - error
20278
20336
  Job:
20279
20337
  type: object
20280
20338
  properties:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "1.11.5",
3
+ "version": "1.11.7",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",