@proveanything/smartlinks 2.0.54 → 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.
- package/dist/api/index.d.ts +0 -1
- package/dist/api/index.js +0 -1
- package/dist/api/realtime.d.ts +31 -88
- package/dist/api/realtime.js +239 -93
- package/dist/docs/API_SUMMARY.md +66 -29
- package/dist/docs/overview.md +2 -3
- package/dist/docs/realtime.md +118 -765
- package/dist/http.js +3 -0
- package/dist/openapi.yaml +105 -27
- package/dist/types/appManifest.d.ts +11 -0
- package/dist/types/realtime.d.ts +44 -0
- package/docs/API_SUMMARY.md +66 -29
- package/docs/overview.md +2 -3
- package/docs/realtime.md +118 -765
- package/openapi.yaml +105 -27
- package/package.json +1 -1
- package/scripts/doctor.mjs +25 -0
- package/dist/api/sequence.d.ts +0 -38
- package/dist/api/sequence.js +0 -29
- package/dist/docs/sequences.md +0 -125
- package/docs/sequences.md +0 -125
package/dist/api/index.d.ts
CHANGED
|
@@ -10,7 +10,6 @@ export { auth } from "./auth.js";
|
|
|
10
10
|
export { form } from "./form.js";
|
|
11
11
|
export { authKit } from "./authKit.js";
|
|
12
12
|
export { claimSet } from "./claimSet.js";
|
|
13
|
-
export { sequence } from "./sequence.js";
|
|
14
13
|
export { crate } from "./crate.js";
|
|
15
14
|
export { batch } from "./batch.js";
|
|
16
15
|
export { variant } from "./variant.js";
|
package/dist/api/index.js
CHANGED
|
@@ -12,7 +12,6 @@ export { auth } from "./auth.js";
|
|
|
12
12
|
export { form } from "./form.js";
|
|
13
13
|
export { authKit } from "./authKit.js";
|
|
14
14
|
export { claimSet } from "./claimSet.js";
|
|
15
|
-
export { sequence } from "./sequence.js";
|
|
16
15
|
export { crate } from "./crate.js";
|
|
17
16
|
export { batch } from "./batch.js";
|
|
18
17
|
export { variant } from "./variant.js";
|
package/dist/api/realtime.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* //
|
|
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
|
-
*
|
|
22
|
+
* Something changed: refetch it. IDs only — never data.
|
|
41
23
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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>;
|
package/dist/api/realtime.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* //
|
|
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
|
-
*
|
|
203
|
+
* Something changed: refetch it. IDs only — never data.
|
|
42
204
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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
|
|
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(
|
|
75
|
-
if (params.appId)
|
|
76
|
-
queryParams.append(
|
|
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
|
-
*
|
|
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(
|
|
258
|
+
return request("/admin/auth/push");
|
|
113
259
|
}
|
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 2.0.
|
|
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
|
|
|
@@ -154,7 +154,6 @@ The Smartlinks SDK is organized into the following namespaces:
|
|
|
154
154
|
- **realtime** - Functions for realtime operations
|
|
155
155
|
- **research** - Functions for research operations
|
|
156
156
|
- **secrets** - Functions for secrets operations
|
|
157
|
-
- **sequence** - Functions for sequence operations
|
|
158
157
|
- **tags** - Functions for tags operations
|
|
159
158
|
- **template** - Functions for template operations
|
|
160
159
|
- **translations** - Functions for translations operations
|
|
@@ -2851,6 +2850,7 @@ interface AppManifest {
|
|
|
2851
2850
|
publicViews?: PublicView[];
|
|
2852
2851
|
executor?: AppManifestExecutor;
|
|
2853
2852
|
functions?: AppManifestFunctions;
|
|
2853
|
+
realtime?: { channels: RealtimeChannelDeclaration[] };
|
|
2854
2854
|
data?: AppDataDeclaration;
|
|
2855
2855
|
headless?: AppHeadlessDeclaration;
|
|
2856
2856
|
[key: string]: any;
|
|
@@ -9142,6 +9142,59 @@ interface AblyTokenRequest {
|
|
|
9142
9142
|
}
|
|
9143
9143
|
```
|
|
9144
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
|
+
|
|
9145
9198
|
**RealtimeChannelPattern** = `string`
|
|
9146
9199
|
|
|
9147
9200
|
### research
|
|
@@ -9869,26 +9922,6 @@ interface AppSecretOptions {
|
|
|
9869
9922
|
}
|
|
9870
9923
|
```
|
|
9871
9924
|
|
|
9872
|
-
### sequence (api)
|
|
9873
|
-
|
|
9874
|
-
**AllocateSequenceInput** (interface)
|
|
9875
|
-
```typescript
|
|
9876
|
-
interface AllocateSequenceInput {
|
|
9877
|
-
appId: string
|
|
9878
|
-
sequenceId: string
|
|
9879
|
-
subjectId: string
|
|
9880
|
-
productId?: string
|
|
9881
|
-
}
|
|
9882
|
-
```
|
|
9883
|
-
|
|
9884
|
-
**AllocatedSequence** (interface)
|
|
9885
|
-
```typescript
|
|
9886
|
-
interface AllocatedSequence {
|
|
9887
|
-
number: number
|
|
9888
|
-
isNew: boolean
|
|
9889
|
-
}
|
|
9890
|
-
```
|
|
9891
|
-
|
|
9892
9925
|
### conditions (utils)
|
|
9893
9926
|
|
|
9894
9927
|
**BaseCondition** (interface)
|
|
@@ -12189,11 +12222,20 @@ Reset rate limit for a user
|
|
|
12189
12222
|
|
|
12190
12223
|
### realtime
|
|
12191
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
|
+
|
|
12192
12234
|
**getPublicToken**(params: RealtimeTokenRequest) → `Promise<AblyTokenRequest>`
|
|
12193
|
-
|
|
12235
|
+
`collection:{collectionId}:*` (subscribe; publish only for the collection's admins).
|
|
12194
12236
|
|
|
12195
12237
|
**getAdminToken**() → `Promise<AblyTokenRequest>`
|
|
12196
|
-
|
|
12238
|
+
(subscribe only).
|
|
12197
12239
|
|
|
12198
12240
|
### research
|
|
12199
12241
|
|
|
@@ -12251,11 +12293,6 @@ Soft-delete a secret. DELETE /secrets/:ref
|
|
|
12251
12293
|
id: string,
|
|
12252
12294
|
query: { limit?: number; offset?: number } = {}) → `Promise<SegmentRecipientsResponse>`
|
|
12253
12295
|
|
|
12254
|
-
### sequence
|
|
12255
|
-
|
|
12256
|
-
**allocate**(collectionId: string, input: AllocateSequenceInput) → `Promise<AllocatedSequence>`
|
|
12257
|
-
Allocate (or return the existing) sequence number for a subject. Idempotent — safe to call on load (auto-enter) and on a button tap; a subject that already has a number gets it back with `isNew: false`. const { number, isNew } = await sequence.allocate(collectionId, { appId: 'raffle-app', sequenceId: 'raffle', subjectId: claimSetId, }) `ctx.sl.counters.next()` + `ctx.sl.appRecords.claim()` (docs/server-functions.md).
|
|
12258
|
-
|
|
12259
12296
|
### sessions
|
|
12260
12297
|
|
|
12261
12298
|
**stats**(collectionId: string) → `Promise<SessionStatistics>`
|
package/dist/docs/overview.md
CHANGED
|
@@ -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
|
-
| **
|
|
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 |
|
|
@@ -93,7 +93,6 @@ The SmartLinks SDK (`@proveanything/smartlinks`) includes comprehensive document
|
|
|
93
93
|
| **Container Tracking** | `docs/container-tracking.md` | Physical/logical **item** container groupings — *not* app containers (see containers.md) |
|
|
94
94
|
| **Lots** | `docs/lots.md` | Cross-SKU production "Lot" entity |
|
|
95
95
|
| **Loyalty** | `docs/loyalty.md` | Points, members, earning rules |
|
|
96
|
-
| **Sequences** | `docs/sequences.md` | RETIRING — number things in your own server function: `ctx.sl.counters` + `appRecords.claim` (server-functions.md) |
|
|
97
96
|
| **Integrations** | `docs/integrations.md` | Inbound/outbound external-system integration flows |
|
|
98
97
|
| **Item Context** | `docs/item-context.md` | The `itemContext` container prop (serial / NFC authenticity context) |
|
|
99
98
|
| **Native Facade** | `docs/native-facade.md` | `host.native` / `SL.native` device-capability facade |
|
|
@@ -359,7 +358,7 @@ The SmartLinks SDK is organised into namespaces. See `docs/API_SUMMARY.md` for t
|
|
|
359
358
|
- `contact` — Customer contact management
|
|
360
359
|
- `comms` / `broadcasts` — Notifications and campaigns
|
|
361
360
|
- `asset` — File/image uploads
|
|
362
|
-
- `realtime` —
|
|
361
|
+
- `realtime` — SmartLinks Realtime: `onChange`, `channel`, `onMine` (see `docs/realtime.md`)
|
|
363
362
|
|
|
364
363
|
---
|
|
365
364
|
|