@proveanything/smartlinks 2.0.55 → 2.0.56

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,103 +1,46 @@
1
- import type { AblyTokenRequest, RealtimeTokenRequest } from "../types/realtime.js";
1
+ import type { AblyTokenRequest, RealtimeTokenRequest, RealtimeChangeEvent, RealtimeChangeFilter, RealtimeMessage, RealtimeScope } from "../types/realtime.js";
2
2
  /**
3
- * Real-Time Communications API
3
+ * One of your app's declared channels (manifest `realtime.channels`).
4
4
  *
5
- * Provides access to real-time communication channels using Ably.
6
- * Supports both public (user-scoped) and admin tokens for different permission levels.
7
- *
8
- * Channel Patterns:
9
- * - With appId: collection:{collectionId}:app:{appId}:*
10
- * - Without appId: collection:{collectionId}:*
11
- *
12
- * @example
13
5
  * ```ts
14
- * import { realtime } from '@proveanything/smartlinks'
15
- *
16
- * // Get token for public user
17
- * const tokenRequest = await realtime.getPublicToken({
18
- * collectionId: 'abc123',
19
- * appId: 'xyz789'
20
- * })
21
- *
22
- * // Initialize Ably client
23
- * const ably = new Ably.Realtime.Promise({
24
- * authCallback: async (data, callback) => {
25
- * callback(null, tokenRequest)
26
- * }
27
- * })
28
- *
29
- * // Subscribe to a channel
30
- * const channel = ably.channels.get('collection:abc123:app:xyz789:chat:room-1')
31
- * await channel.subscribe((message) => {
32
- * console.log('Received:', message.data)
33
- * })
34
- *
35
- * // Publish a message
36
- * await channel.publish('message', { text: 'Hello!', userId: '123' })
6
+ * const draw = realtime.channel({ collectionId, appId }, "draw")
7
+ * const off = draw.subscribe("status", (m) => setStatus(m.data))
8
+ * await draw.publish("status", { phase: "spinning" }) // if the channel lets this viewer publish
37
9
  * ```
38
10
  */
11
+ export declare function channel(scope: RealtimeScope & {
12
+ appId: string;
13
+ }, name: string): {
14
+ /** Listen for an event (or "*" for every event). Returns a function that stops listening. */
15
+ subscribe(event: string, onMessage: (msg: RealtimeMessage) => void): () => void;
16
+ /** Send a message. Refused (403) unless the channel's `publish` rule allows this viewer. */
17
+ publish(event: string, data?: unknown): Promise<{
18
+ ok: true;
19
+ }>;
20
+ };
39
21
  /**
40
- * Get an Ably token for public (user-scoped) real-time communication.
22
+ * Something changed: refetch it. IDs only — never data.
41
23
  *
42
- * This endpoint returns an Ably TokenRequest that can be used to initialize an Ably client
43
- * with appropriate permissions for the specified collection and optional app.
24
+ * Admins of the collection see every change; everyone else sees changes to public things (public app
25
+ * records, products). Filter by `type` ('product' | 'proof' | 'asset' | 'record' | 'collection' |
26
+ * 'collection.apps' | 'settings'), `id`, `appId`, `recordType`, `productId`, `proofId`.
44
27
  *
45
- * Requires user authentication (Bearer token).
46
- *
47
- * @param params - Token request parameters
48
- * @param params.collectionId - The collection scope (required)
49
- * @param params.appId - Optional app scope for more specific permissions
50
- * @returns Ably TokenRequest that can be used with Ably.Realtime client
51
- *
52
- * @example
53
28
  * ```ts
54
- * const tokenRequest = await realtime.getPublicToken({
55
- * collectionId: 'my-collection-id',
56
- * appId: 'my-app-id'
57
- * })
58
- *
59
- * // Use with Ably
60
- * const ably = new Ably.Realtime.Promise({
61
- * authCallback: async (data, callback) => {
62
- * callback(null, tokenRequest)
63
- * }
64
- * })
65
- *
66
- * // Subscribe to channels matching the pattern
67
- * const channel = ably.channels.get('collection:my-collection-id:app:my-app-id:chat:general')
68
- * await channel.subscribe('message', (msg) => console.log(msg.data))
29
+ * const off = realtime.onChange({ collectionId }, { type: "record", appId, recordType: "entry" }, () => refetch())
69
30
  * ```
70
31
  */
32
+ export declare function onChange(scope: RealtimeScope, filter: RealtimeChangeFilter, onChangeFn: (change: RealtimeChangeEvent) => void): () => void;
33
+ /**
34
+ * Messages for the signed-in person only (e.g. "scan" — a tag they just scanned on another device).
35
+ */
36
+ export declare function onMine(scope: RealtimeScope, onMessage: (msg: RealtimeMessage) => void): () => void;
37
+ /**
38
+ * @deprecated Use `realtime.channel()` / `realtime.onChange()`. Returns a raw Ably TokenRequest for
39
+ * `collection:{collectionId}:*` (subscribe; publish only for the collection's admins).
40
+ */
71
41
  export declare function getPublicToken(params: RealtimeTokenRequest): Promise<AblyTokenRequest>;
72
42
  /**
73
- * Get an Ably token for admin real-time communication.
74
- *
75
- * This endpoint returns an Ably TokenRequest that can be used to initialize an Ably client
76
- * with admin permissions to receive system notifications and alerts.
77
- *
78
- * Admin users get subscribe-only (read-only) access to the interaction:{userId} channel pattern.
79
- *
80
- * Requires admin authentication (Bearer token).
81
- *
82
- * @returns Ably TokenRequest that can be used with Ably.Realtime client
83
- *
84
- * @example
85
- * ```ts
86
- * const tokenRequest = await realtime.getAdminToken()
87
- *
88
- * // Use with Ably
89
- * const ably = new Ably.Realtime.Promise({
90
- * authCallback: async (data, callback) => {
91
- * callback(null, tokenRequest)
92
- * }
93
- * })
94
- *
95
- * // Subscribe to admin interaction channel
96
- * const userId = 'my-user-id'
97
- * const channel = ably.channels.get(`interaction:${userId}`)
98
- * await channel.subscribe((message) => {
99
- * console.log('Admin notification:', message.data)
100
- * })
101
- * ```
43
+ * @deprecated Use `realtime.onMine()`. Returns a raw Ably TokenRequest for `interaction:{userId}`
44
+ * (subscribe only).
102
45
  */
103
46
  export declare function getAdminToken(): Promise<AblyTokenRequest>;
@@ -1,113 +1,259 @@
1
1
  // src/api/realtime.ts
2
- import { request } from "../http.js";
2
+ import { request, post } from "../http.js";
3
+ /** One live stream per (collection, app): every listener on this page shares it. */
4
+ class Stream {
5
+ constructor(collectionId, appId, onEmpty) {
6
+ this.collectionId = collectionId;
7
+ this.appId = appId;
8
+ this.onEmpty = onEmpty;
9
+ this.listeners = new Map(); // logical name ("draw", "feed:admin") → listeners
10
+ this.physical = new Map(); // physical channel → logical name
11
+ this.granted = new Set();
12
+ this.source = null;
13
+ this.lastEvent = null;
14
+ this.refreshTimer = null;
15
+ this.retryTimer = null;
16
+ this.pending = null;
17
+ this.retries = 0;
18
+ this.closed = false;
19
+ }
20
+ add(name, listener) {
21
+ let set = this.listeners.get(name);
22
+ const isNew = !set;
23
+ if (!set)
24
+ this.listeners.set(name, (set = new Set()));
25
+ set.add(listener);
26
+ if (isNew || !this.source)
27
+ this.schedule();
28
+ return () => {
29
+ const s = this.listeners.get(name);
30
+ if (!s)
31
+ return;
32
+ s.delete(listener);
33
+ if (!s.size)
34
+ this.listeners.delete(name);
35
+ if (!this.listeners.size)
36
+ this.close();
37
+ };
38
+ }
39
+ /** What was granted (empty until connected). */
40
+ has(name) {
41
+ return this.granted.has(name);
42
+ }
43
+ schedule(delay = 30) {
44
+ if (this.pending)
45
+ clearTimeout(this.pending);
46
+ this.pending = setTimeout(() => {
47
+ this.pending = null;
48
+ void this.open();
49
+ }, delay);
50
+ }
51
+ async open() {
52
+ var _a;
53
+ if (this.closed || typeof EventSource === "undefined")
54
+ return;
55
+ const names = [...this.listeners.keys()];
56
+ const feeds = names.filter((n) => n.startsWith("feed:")).map((n) => n.slice(5));
57
+ const channels = names.filter((n) => !n.startsWith("feed:"));
58
+ let reply;
59
+ try {
60
+ reply = await post("/public/realtime/connect", Object.assign(Object.assign({ collectionId: this.collectionId }, (this.appId ? { appId: this.appId } : {})), { feeds,
61
+ channels }));
62
+ }
63
+ catch (err) {
64
+ return this.retryLater(err);
65
+ }
66
+ if (this.closed)
67
+ return;
68
+ (_a = this.source) === null || _a === void 0 ? void 0 : _a.close();
69
+ this.source = null;
70
+ this.physical.clear();
71
+ this.granted = new Set(reply.channels.map((c) => c.name));
72
+ for (const c of reply.channels)
73
+ this.physical.set(c.channel, c.name);
74
+ if (reply.transport !== "sse" || !reply.url)
75
+ return; // nothing this viewer may see
76
+ const url = this.lastEvent ? `${reply.url}&lastEvent=${encodeURIComponent(this.lastEvent)}` : reply.url;
77
+ const source = new EventSource(url);
78
+ this.source = source;
79
+ source.addEventListener("message", (e) => this.deliver(e));
80
+ source.addEventListener("error", (e) => {
81
+ const data = e.data;
82
+ // A stream error with a body (e.g. the token expired): get a fresh token and resume.
83
+ if (data || source.readyState === EventSource.CLOSED)
84
+ this.retryLater(data);
85
+ });
86
+ source.addEventListener("open", () => {
87
+ this.retries = 0;
88
+ });
89
+ // Renew a minute before the token expires (the stream can't renew its own token).
90
+ if (this.refreshTimer)
91
+ clearTimeout(this.refreshTimer);
92
+ if (reply.expiresAt) {
93
+ const ms = Math.max(10000, reply.expiresAt - Date.now() - 60000);
94
+ this.refreshTimer = setTimeout(() => this.schedule(0), ms);
95
+ }
96
+ }
97
+ retryLater(_why) {
98
+ var _a;
99
+ if (this.closed)
100
+ return;
101
+ (_a = this.source) === null || _a === void 0 ? void 0 : _a.close();
102
+ this.source = null;
103
+ if (this.retryTimer)
104
+ clearTimeout(this.retryTimer);
105
+ const ms = Math.min(30000, 1000 * 2 ** this.retries++);
106
+ this.retryTimer = setTimeout(() => this.schedule(0), ms);
107
+ }
108
+ deliver(e) {
109
+ var _a, _b;
110
+ if (e.lastEventId)
111
+ this.lastEvent = e.lastEventId;
112
+ let env;
113
+ try {
114
+ env = JSON.parse(String(e.data));
115
+ }
116
+ catch (_c) {
117
+ return;
118
+ }
119
+ const name = env.channel ? this.physical.get(env.channel) : undefined;
120
+ if (!name)
121
+ return;
122
+ let data = env.data;
123
+ if (typeof data === "string" && env.encoding && /json/.test(env.encoding)) {
124
+ try {
125
+ data = JSON.parse(data);
126
+ }
127
+ catch (_d) {
128
+ /* leave as the string */
129
+ }
130
+ }
131
+ const msg = { channel: name, event: (_a = env.name) !== null && _a !== void 0 ? _a : "", data, at: env.timestamp };
132
+ for (const l of [...((_b = this.listeners.get(name)) !== null && _b !== void 0 ? _b : [])]) {
133
+ try {
134
+ l(msg);
135
+ }
136
+ catch (err) {
137
+ console.error("[SmartLinks realtime] a listener threw", err);
138
+ }
139
+ }
140
+ }
141
+ close() {
142
+ var _a;
143
+ this.closed = true;
144
+ for (const t of [this.pending, this.refreshTimer, this.retryTimer])
145
+ if (t)
146
+ clearTimeout(t);
147
+ (_a = this.source) === null || _a === void 0 ? void 0 : _a.close();
148
+ this.source = null;
149
+ this.onEmpty();
150
+ }
151
+ }
152
+ const streams = new Map();
153
+ function streamFor(scope) {
154
+ var _a, _b;
155
+ if (!(scope === null || scope === void 0 ? void 0 : scope.collectionId))
156
+ throw new Error("realtime: collectionId is required");
157
+ const key = `${scope.collectionId}|${(_a = scope.appId) !== null && _a !== void 0 ? _a : ""}`;
158
+ let s = streams.get(key);
159
+ if (!s) {
160
+ s = new Stream(scope.collectionId, (_b = scope.appId) !== null && _b !== void 0 ? _b : null, () => streams.delete(key));
161
+ streams.set(key, s);
162
+ }
163
+ return s;
164
+ }
3
165
  /**
4
- * Real-Time Communications API
5
- *
6
- * Provides access to real-time communication channels using Ably.
7
- * Supports both public (user-scoped) and admin tokens for different permission levels.
8
- *
9
- * Channel Patterns:
10
- * - With appId: collection:{collectionId}:app:{appId}:*
11
- * - Without appId: collection:{collectionId}:*
166
+ * One of your app's declared channels (manifest `realtime.channels`).
12
167
  *
13
- * @example
14
168
  * ```ts
15
- * import { realtime } from '@proveanything/smartlinks'
16
- *
17
- * // Get token for public user
18
- * const tokenRequest = await realtime.getPublicToken({
19
- * collectionId: 'abc123',
20
- * appId: 'xyz789'
21
- * })
22
- *
23
- * // Initialize Ably client
24
- * const ably = new Ably.Realtime.Promise({
25
- * authCallback: async (data, callback) => {
26
- * callback(null, tokenRequest)
27
- * }
28
- * })
29
- *
30
- * // Subscribe to a channel
31
- * const channel = ably.channels.get('collection:abc123:app:xyz789:chat:room-1')
32
- * await channel.subscribe((message) => {
33
- * console.log('Received:', message.data)
34
- * })
35
- *
36
- * // Publish a message
37
- * await channel.publish('message', { text: 'Hello!', userId: '123' })
169
+ * const draw = realtime.channel({ collectionId, appId }, "draw")
170
+ * const off = draw.subscribe("status", (m) => setStatus(m.data))
171
+ * await draw.publish("status", { phase: "spinning" }) // if the channel lets this viewer publish
38
172
  * ```
39
173
  */
174
+ export function channel(scope, name) {
175
+ return {
176
+ /** Listen for an event (or "*" for every event). Returns a function that stops listening. */
177
+ subscribe(event, onMessage) {
178
+ return streamFor(scope).add(name, (m) => {
179
+ if (event === "*" || m.event === event)
180
+ onMessage(m);
181
+ });
182
+ },
183
+ /** Send a message. Refused (403) unless the channel's `publish` rule allows this viewer. */
184
+ publish(event, data) {
185
+ return post("/public/realtime/publish", {
186
+ collectionId: scope.collectionId,
187
+ appId: scope.appId,
188
+ channel: name,
189
+ event,
190
+ data: data === undefined ? null : data,
191
+ });
192
+ },
193
+ };
194
+ }
195
+ function matches(change, filter) {
196
+ for (const k of ["type", "id", "appId", "recordType", "productId", "proofId"]) {
197
+ if (filter[k] !== undefined && change[k] !== filter[k])
198
+ return false;
199
+ }
200
+ return true;
201
+ }
40
202
  /**
41
- * Get an Ably token for public (user-scoped) real-time communication.
203
+ * Something changed: refetch it. IDs only — never data.
42
204
  *
43
- * This endpoint returns an Ably TokenRequest that can be used to initialize an Ably client
44
- * with appropriate permissions for the specified collection and optional app.
205
+ * Admins of the collection see every change; everyone else sees changes to public things (public app
206
+ * records, products). Filter by `type` ('product' | 'proof' | 'asset' | 'record' | 'collection' |
207
+ * 'collection.apps' | 'settings'), `id`, `appId`, `recordType`, `productId`, `proofId`.
45
208
  *
46
- * Requires user authentication (Bearer token).
47
- *
48
- * @param params - Token request parameters
49
- * @param params.collectionId - The collection scope (required)
50
- * @param params.appId - Optional app scope for more specific permissions
51
- * @returns Ably TokenRequest that can be used with Ably.Realtime client
52
- *
53
- * @example
54
209
  * ```ts
55
- * const tokenRequest = await realtime.getPublicToken({
56
- * collectionId: 'my-collection-id',
57
- * appId: 'my-app-id'
58
- * })
59
- *
60
- * // Use with Ably
61
- * const ably = new Ably.Realtime.Promise({
62
- * authCallback: async (data, callback) => {
63
- * callback(null, tokenRequest)
64
- * }
65
- * })
66
- *
67
- * // Subscribe to channels matching the pattern
68
- * const channel = ably.channels.get('collection:my-collection-id:app:my-app-id:chat:general')
69
- * await channel.subscribe('message', (msg) => console.log(msg.data))
210
+ * const off = realtime.onChange({ collectionId }, { type: "record", appId, recordType: "entry" }, () => refetch())
70
211
  * ```
71
212
  */
213
+ export function onChange(scope, filter, onChangeFn) {
214
+ const s = streamFor({ collectionId: scope.collectionId });
215
+ // Ask for both feeds; an admin is granted both (public changes reach the admin feed too), so the
216
+ // public feed is ignored once the admin feed is granted — no change arrives twice.
217
+ const handle = (fromAdminFeed) => (m) => {
218
+ if (m.event !== "change" || !m.data || typeof m.data !== "object")
219
+ return;
220
+ if (!fromAdminFeed && s.has("feed:admin"))
221
+ return;
222
+ const change = m.data;
223
+ if (matches(change, filter))
224
+ onChangeFn(change);
225
+ };
226
+ const offAdmin = s.add("feed:admin", handle(true));
227
+ const offPublic = s.add("feed:public", handle(false));
228
+ return () => {
229
+ offAdmin();
230
+ offPublic();
231
+ };
232
+ }
233
+ /**
234
+ * Messages for the signed-in person only (e.g. "scan" — a tag they just scanned on another device).
235
+ */
236
+ export function onMine(scope, onMessage) {
237
+ return streamFor({ collectionId: scope.collectionId }).add("feed:me", onMessage);
238
+ }
239
+ // ---------------------------------------------------------------------------------------------------
240
+ // Legacy (raw Ably tokens). New code uses channel / onChange above — no provider in your app.
241
+ // ---------------------------------------------------------------------------------------------------
242
+ /**
243
+ * @deprecated Use `realtime.channel()` / `realtime.onChange()`. Returns a raw Ably TokenRequest for
244
+ * `collection:{collectionId}:*` (subscribe; publish only for the collection's admins).
245
+ */
72
246
  export async function getPublicToken(params) {
73
247
  const queryParams = new URLSearchParams();
74
- queryParams.append('collectionId', params.collectionId);
75
- if (params.appId) {
76
- queryParams.append('appId', params.appId);
77
- }
248
+ queryParams.append("collectionId", params.collectionId);
249
+ if (params.appId)
250
+ queryParams.append("appId", params.appId);
78
251
  return request(`/public/push/token?${queryParams.toString()}`);
79
252
  }
80
253
  /**
81
- * Get an Ably token for admin real-time communication.
82
- *
83
- * This endpoint returns an Ably TokenRequest that can be used to initialize an Ably client
84
- * with admin permissions to receive system notifications and alerts.
85
- *
86
- * Admin users get subscribe-only (read-only) access to the interaction:{userId} channel pattern.
87
- *
88
- * Requires admin authentication (Bearer token).
89
- *
90
- * @returns Ably TokenRequest that can be used with Ably.Realtime client
91
- *
92
- * @example
93
- * ```ts
94
- * const tokenRequest = await realtime.getAdminToken()
95
- *
96
- * // Use with Ably
97
- * const ably = new Ably.Realtime.Promise({
98
- * authCallback: async (data, callback) => {
99
- * callback(null, tokenRequest)
100
- * }
101
- * })
102
- *
103
- * // Subscribe to admin interaction channel
104
- * const userId = 'my-user-id'
105
- * const channel = ably.channels.get(`interaction:${userId}`)
106
- * await channel.subscribe((message) => {
107
- * console.log('Admin notification:', message.data)
108
- * })
109
- * ```
254
+ * @deprecated Use `realtime.onMine()`. Returns a raw Ably TokenRequest for `interaction:{userId}`
255
+ * (subscribe only).
110
256
  */
111
257
  export async function getAdminToken() {
112
- return request('/admin/auth/push');
258
+ return request("/admin/auth/push");
113
259
  }
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.55 | Generated: 2026-10-12T20:04:55.294Z
3
+ Version: 2.0.56 | Generated: 2026-10-12T20:58:59.335Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -2850,6 +2850,7 @@ interface AppManifest {
2850
2850
  publicViews?: PublicView[];
2851
2851
  executor?: AppManifestExecutor;
2852
2852
  functions?: AppManifestFunctions;
2853
+ realtime?: { channels: RealtimeChannelDeclaration[] };
2853
2854
  data?: AppDataDeclaration;
2854
2855
  headless?: AppHeadlessDeclaration;
2855
2856
  [key: string]: any;
@@ -9141,6 +9142,59 @@ interface AblyTokenRequest {
9141
9142
  }
9142
9143
  ```
9143
9144
 
9145
+ **RealtimeScope** (interface)
9146
+ ```typescript
9147
+ interface RealtimeScope {
9148
+ collectionId: string
9149
+ appId?: string
9150
+ }
9151
+ ```
9152
+
9153
+ **RealtimeMessage** (interface)
9154
+ ```typescript
9155
+ interface RealtimeMessage {
9156
+ channel: string
9157
+ event: string
9158
+ data: unknown
9159
+ at?: number
9160
+ }
9161
+ ```
9162
+
9163
+ **RealtimeChangeEvent** (interface)
9164
+ ```typescript
9165
+ interface RealtimeChangeEvent {
9166
+ type: string
9167
+ id?: string
9168
+ appId?: string
9169
+ recordType?: string
9170
+ productId?: string
9171
+ proofId?: string
9172
+ status?: string
9173
+ at: number
9174
+ }
9175
+ ```
9176
+
9177
+ **RealtimeChangeFilter** (interface)
9178
+ ```typescript
9179
+ interface RealtimeChangeFilter {
9180
+ type?: string
9181
+ id?: string
9182
+ appId?: string
9183
+ recordType?: string
9184
+ productId?: string
9185
+ proofId?: string
9186
+ }
9187
+ ```
9188
+
9189
+ **RealtimeChannelDeclaration** (interface)
9190
+ ```typescript
9191
+ interface RealtimeChannelDeclaration {
9192
+ name: string
9193
+ subscribe?: "public" | "signedIn" | "admin"
9194
+ publish?: "public" | "signedIn" | "admin" | "server"
9195
+ }
9196
+ ```
9197
+
9144
9198
  **RealtimeChannelPattern** = `string`
9145
9199
 
9146
9200
  ### research
@@ -12168,11 +12222,20 @@ Reset rate limit for a user
12168
12222
 
12169
12223
  ### realtime
12170
12224
 
12225
+ **channel**(scope: RealtimeScope & { appId: string }, name: string) → `void`
12226
+ One of your app's declared channels (manifest `realtime.channels`). ```ts const draw = realtime.channel({ collectionId, appId }, "draw") const off = draw.subscribe("status", (m) => setStatus(m.data)) await draw.publish("status", { phase: "spinning" }) // if the channel lets this viewer publish ```
12227
+
12228
+ **onChange**(scope: RealtimeScope, filter: RealtimeChangeFilter, onChangeFn: (change: RealtimeChangeEvent) → `void`
12229
+ Something changed: refetch it. IDs only — never data. Admins of the collection see every change; everyone else sees changes to public things (public app records, products). Filter by `type` ('product' | 'proof' | 'asset' | 'record' | 'collection' | 'collection.apps' | 'settings'), `id`, `appId`, `recordType`, `productId`, `proofId`. ```ts const off = realtime.onChange({ collectionId }, { type: "record", appId, recordType: "entry" }, () => refetch()) ```
12230
+
12231
+ **onMine**(scope: RealtimeScope, onMessage: (msg: RealtimeMessage) → `void`
12232
+ Messages for the signed-in person only (e.g. "scan" — a tag they just scanned on another device).
12233
+
12171
12234
  **getPublicToken**(params: RealtimeTokenRequest) → `Promise<AblyTokenRequest>`
12172
- Get an Ably token for public (user-scoped) real-time communication. This endpoint returns an Ably TokenRequest that can be used to initialize an Ably client with appropriate permissions for the specified collection and optional app. Requires user authentication (Bearer token). ```ts const tokenRequest = await realtime.getPublicToken({ collectionId: 'my-collection-id', appId: 'my-app-id' }) // Use with Ably const ably = new Ably.Realtime.Promise({ authCallback: async (data, callback) => { callback(null, tokenRequest) } }) // Subscribe to channels matching the pattern const channel = ably.channels.get('collection:my-collection-id:app:my-app-id:chat:general') await channel.subscribe('message', (msg) => console.log(msg.data)) ```
12235
+ `collection:{collectionId}:*` (subscribe; publish only for the collection's admins).
12173
12236
 
12174
12237
  **getAdminToken**() → `Promise<AblyTokenRequest>`
12175
- Get an Ably token for admin real-time communication. This endpoint returns an Ably TokenRequest that can be used to initialize an Ably client with admin permissions to receive system notifications and alerts. Admin users get subscribe-only (read-only) access to the interaction:{userId} channel pattern. Requires admin authentication (Bearer token). ```ts const tokenRequest = await realtime.getAdminToken() // Use with Ably const ably = new Ably.Realtime.Promise({ authCallback: async (data, callback) => { callback(null, tokenRequest) } }) // Subscribe to admin interaction channel const userId = 'my-user-id' const channel = ably.channels.get(`interaction:${userId}`) await channel.subscribe((message) => { console.log('Admin notification:', message.data) }) ```
12238
+ (subscribe only).
12176
12239
 
12177
12240
  ### research
12178
12241
 
@@ -69,7 +69,7 @@ The SmartLinks SDK (`@proveanything/smartlinks`) includes comprehensive document
69
69
  | **Portal Request Action** | `docs/portal-request-action.md` | Triggering portal built-in actions (__qrScanner, __share, __logout, etc.) from sub-apps |
70
70
  | **Interactions** | `docs/interactions.md` | Business events, outcomes, voting, competitions, and journey triggers |
71
71
  | **App Config Files** | `docs/app-manifest.md` | Full field-by-field reference for both JSON config files |
72
- | **Real-time Messaging** | `docs/realtime.md` | Adding Ably real-time features (chat, live updates) |
72
+ | **Realtime** | `docs/realtime.md` | Live updates: change events (refetch when something changes) and declared app channels (a draw, a status) — no provider library or keys |
73
73
  | **Liquid Templates** | `docs/liquid-templates.md` | Dynamic content rendering with LiquidJS |
74
74
  | **Iframe Responder** | `docs/iframe-responder.md` | Iframe communication and proxy mode internals |
75
75
  | **AI Guide Template** | `docs/ai-guide-template.md` | Template for creating `public/ai-guide.md` — customise per app |
@@ -358,7 +358,7 @@ The SmartLinks SDK is organised into namespaces. See `docs/API_SUMMARY.md` for t
358
358
  - `contact` — Customer contact management
359
359
  - `comms` / `broadcasts` — Notifications and campaigns
360
360
  - `asset` — File/image uploads
361
- - `realtime` — Ably real-time messaging (see `docs/realtime.md`)
361
+ - `realtime` — SmartLinks Realtime: `onChange`, `channel`, `onMine` (see `docs/realtime.md`)
362
362
 
363
363
  ---
364
364