@asiyst/sdk 0.1.4 → 0.1.8

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/README.md CHANGED
@@ -13,20 +13,31 @@ npm install @asiyst/sdk
13
13
  Then connect the project:
14
14
 
15
15
  ```bash
16
- npx asiyst init
16
+ npx @asiyst/cli init
17
17
  ```
18
18
 
19
19
  `asiyst init` opens [https://asiyst.com](https://asiyst.com) so you can create a project and copy credentials. Avatar appearance and behavior live in Asiyst Cloud so you can change them without redeploying the site.
20
20
 
21
21
  ## Usage
22
22
 
23
+ Project ID is required. The value must be the public Project ID for the target Asiyst project, not the developer account user ID and not the internal Supabase UUID.
24
+
23
25
  ```ts
24
26
  import { Asiyst } from "@asiyst/sdk";
25
27
 
26
28
  await Asiyst.init({
27
- projectId: "asiyst_project_id",
28
- publicKey: "asiyst_public_key",
29
+ projectId: "<PUBLIC_PROJECT_ID>",
30
+ publicKey: "<PUBLIC_KEY>",
31
+ });
32
+ ```
33
+
34
+ Do not pass a secret API key as `publicKey` or omit `projectId`.
35
+
36
+ ```ts
37
+ await Asiyst.init({
38
+ publicKey: "<PUBLIC_KEY>",
29
39
  });
40
+ // throws: Asiyst SDK: projectId is required.
30
41
  ```
31
42
 
32
43
  Mark important controls so the assistant can find them across layouts:
@@ -41,7 +52,7 @@ Mark important controls so the assistant can find them across layouts:
41
52
  </button>
42
53
  ```
43
54
 
44
- The production API base is `https://asiyst.com/api/v1`. It is used by default and can only be overridden explicitly through `apiBaseUrl` for a controlled non-production deployment.
55
+ The production API base is `https://nqhxpgsjofzqudyqkqib.supabase.co/functions/v1/api`. It is used by default and can only be overridden explicitly through `apiBaseUrl` for a controlled non-production deployment.
45
56
 
46
57
  If Cloud is unreachable, the SDK keeps a fallback avatar configuration and exposes `getConnectionStatus()` as `offline`. Conversation replies and task plans are not invented locally; those requests fail until Cloud responds.
47
58
 
package/dist/index.cjs CHANGED
@@ -59,6 +59,9 @@ var ALL_ACTION_KINDS = [
59
59
  "scroll",
60
60
  "type",
61
61
  "select",
62
+ "open",
63
+ "close",
64
+ "filter",
62
65
  "open-menu",
63
66
  "open-modal",
64
67
  "search",
@@ -68,8 +71,8 @@ var ALL_ACTION_KINDS = [
68
71
  ];
69
72
 
70
73
  // src/core/constants.ts
71
- var SDK_VERSION = "0.1.0";
72
- var DEFAULT_API_BASE_URL = "https://asiyst.com/api/v1";
74
+ var SDK_VERSION = "0.1.7" ;
75
+ var DEFAULT_API_BASE_URL = "https://nqhxpgsjofzqudyqkqib.supabase.co/functions/v1/api";
73
76
  var CONFIG_SCHEMA_VERSION = 1;
74
77
  var HOST_ELEMENT_ID = "asiyst-host";
75
78
  var DATA_ATTR = "data-asiyst";
@@ -124,6 +127,19 @@ var ANCHORS = [
124
127
  "top-right",
125
128
  "top-left"
126
129
  ];
130
+ var PUBLIC_IDENTIFIER_PATTERN = /^(?=.{1,128}$)[A-Za-z0-9_][A-Za-z0-9_-]*$/;
131
+ function isValidPublicIdentifier(value) {
132
+ if (typeof value !== "string") return false;
133
+ const trimmed = value.trim();
134
+ if (!trimmed || trimmed.length > 128) return false;
135
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(trimmed)) {
136
+ return false;
137
+ }
138
+ return PUBLIC_IDENTIFIER_PATTERN.test(trimmed);
139
+ }
140
+ function isValidProjectId(value) {
141
+ return isValidPublicIdentifier(value);
142
+ }
127
143
  function isRecord(value) {
128
144
  return typeof value === "object" && value !== null && !Array.isArray(value);
129
145
  }
@@ -152,6 +168,88 @@ function parseAllowedActions(value, fallback) {
152
168
  }
153
169
  return next.length > 0 ? next : [...fallback];
154
170
  }
171
+ function parseStringList(value) {
172
+ if (typeof value === "string") {
173
+ return value.split(",").map((item) => sanitizeText(item.trim(), 200)).filter(Boolean);
174
+ }
175
+ if (!Array.isArray(value)) {
176
+ return [];
177
+ }
178
+ return value.filter((item) => typeof item === "string").map((item) => sanitizeText(item.trim(), 200)).filter(Boolean);
179
+ }
180
+ function parseRule(value) {
181
+ if (!isRecord(value)) return null;
182
+ const action = typeof value.action === "string" ? sanitizeText(value.action, 64) : void 0;
183
+ if (action && !ALL_ACTION_KINDS.includes(action)) {
184
+ return null;
185
+ }
186
+ const routeList = parseStringList(value.routes ?? value.route);
187
+ const domainList = parseStringList(value.domains ?? value.domain);
188
+ const hasAllowedValue = typeof value.allowed === "boolean";
189
+ const hasAnySemanticField = hasAllowedValue || typeof value.target === "string" || typeof value.route === "string" || routeList.length > 0 || typeof value.domain === "string" || domainList.length > 0 || typeof value.enabled === "boolean" || typeof value.sourceId === "string" || typeof value.dataSourceId === "string";
190
+ if (!hasAnySemanticField) {
191
+ return null;
192
+ }
193
+ const rule = {
194
+ action,
195
+ allowed: typeof value.allowed === "boolean" ? value.allowed : void 0,
196
+ target: typeof value.target === "string" ? sanitizeText(value.target, 200) : void 0,
197
+ route: typeof value.route === "string" ? sanitizeText(value.route, 200) : void 0,
198
+ routes: routeList,
199
+ domain: typeof value.domain === "string" ? sanitizeText(value.domain, 200) : void 0,
200
+ domains: domainList,
201
+ enabled: typeof value.enabled === "boolean" ? value.enabled : void 0,
202
+ sourceId: typeof value.sourceId === "string" ? sanitizeText(value.sourceId, 128) : void 0,
203
+ dataSourceId: typeof value.dataSourceId === "string" ? sanitizeText(value.dataSourceId, 128) : void 0
204
+ };
205
+ if (typeof value.allowed !== "undefined" && typeof value.allowed !== "boolean") {
206
+ return null;
207
+ }
208
+ return rule;
209
+ }
210
+ function parseRules(value) {
211
+ if (!Array.isArray(value)) {
212
+ return [];
213
+ }
214
+ const rules = [];
215
+ for (const entry of value) {
216
+ const rule = parseRule(entry);
217
+ if (rule) {
218
+ rules.push(rule);
219
+ }
220
+ }
221
+ return rules;
222
+ }
223
+ function parseDataSources(value) {
224
+ if (!Array.isArray(value)) {
225
+ return [];
226
+ }
227
+ const next = [];
228
+ for (const entry of value) {
229
+ if (!isRecord(entry)) continue;
230
+ const id = typeof entry.id === "string" ? sanitizeText(entry.id, 128) : "";
231
+ if (!id) continue;
232
+ next.push({
233
+ id,
234
+ name: typeof entry.name === "string" ? sanitizeText(entry.name, 128) : void 0,
235
+ type: typeof entry.type === "string" ? sanitizeText(entry.type, 64) : void 0,
236
+ enabled: typeof entry.enabled === "boolean" ? entry.enabled : void 0,
237
+ url: typeof entry.url === "string" ? sanitizeText(entry.url, 400) : void 0,
238
+ method: typeof entry.method === "string" ? sanitizeText(entry.method, 20) : void 0,
239
+ headers: isRecord(entry.headers) ? (() => {
240
+ const safeHeaders = {};
241
+ for (const [key, value2] of Object.entries(entry.headers)) {
242
+ if (typeof value2 === "string") {
243
+ safeHeaders[sanitizeText(key, 64)] = sanitizeText(value2, 200);
244
+ }
245
+ }
246
+ return safeHeaders;
247
+ })() : void 0,
248
+ credentials: typeof entry.credentials === "string" ? "[redacted]" : void 0
249
+ });
250
+ }
251
+ return next;
252
+ }
155
253
  function parseTheme(value) {
156
254
  if (!isRecord(value)) {
157
255
  return {};
@@ -222,6 +320,11 @@ function fallbackConfig() {
222
320
  },
223
321
  mode: "guided",
224
322
  allowedActions: ["navigate", "highlight", "scroll", "wait", "explain", "complete"],
323
+ allowedDomains: [],
324
+ allowedRoutes: [],
325
+ blockedRoutes: [],
326
+ rules: [],
327
+ dataSources: [],
225
328
  elementSelectors: {}
226
329
  };
227
330
  }
@@ -245,21 +348,30 @@ function normalizeProjectConfig(raw) {
245
348
  behavior: { ...base.behavior, ...parseBehavior(raw.behavior) },
246
349
  mode: raw.mode === "assist" ? "assist" : "guided",
247
350
  allowedActions: parseAllowedActions(raw.allowedActions, base.allowedActions),
351
+ allowedDomains: parseStringList(raw.allowedDomains ?? raw.allowed_domains),
352
+ allowedRoutes: parseStringList(raw.allowedRoutes ?? raw.allowed_routes),
353
+ blockedRoutes: parseStringList(raw.blockedRoutes ?? raw.blocked_routes),
354
+ rules: parseRules(raw.rules ?? raw.avatarRules),
355
+ dataSources: parseDataSources(raw.dataSources ?? raw.enabledDataSources),
248
356
  elementSelectors: parseElementSelectors(raw.elementSelectors)
249
357
  };
250
358
  }
251
359
  function validateInitOptions(options) {
252
- if (typeof options.projectId !== "string" || !options.projectId.trim()) {
253
- throw new ConfigurationError("projectId is required");
360
+ const projectIdValue = typeof options.projectId === "string" ? options.projectId.trim() : "";
361
+ if (!projectIdValue) {
362
+ throw new ConfigurationError("Asiyst SDK: projectId is required.");
363
+ }
364
+ if (!isValidProjectId(projectIdValue)) {
365
+ throw new ConfigurationError("Asiyst SDK: projectId is invalid. Use a public Project ID.");
254
366
  }
255
367
  if (typeof options.publicKey !== "string" || !options.publicKey.trim()) {
256
368
  throw new ConfigurationError("publicKey is required");
257
369
  }
258
- if (options.projectId.length > 128 || options.publicKey.length > 256) {
370
+ if (projectIdValue.length > 128 || options.publicKey.trim().length > 256) {
259
371
  throw new ConfigurationError("project credentials exceed allowed length");
260
372
  }
261
373
  return {
262
- projectId: options.projectId.trim(),
374
+ projectId: projectIdValue,
263
375
  publicKey: options.publicKey.trim()
264
376
  };
265
377
  }
@@ -982,7 +1094,17 @@ var TargetResolver = class {
982
1094
  if (!best || bestScore < 2) {
983
1095
  return null;
984
1096
  }
985
- return findElementByAsiystId(this.doc, best.id) ?? this.doc.querySelector(`[id="${best.id.replace(/^dom:/, "")}"]`);
1097
+ return findElementByAsiystId(this.doc, best.id) ?? this.doc.querySelector(`[id="${best.id.replace(/^dom:/, "")}"]`) ?? this.findMappedElement(best);
1098
+ }
1099
+ findMappedElement(mapped) {
1100
+ const candidates = Array.from(this.doc.querySelectorAll("a[href], button, input, select, textarea, form, nav, h1, h2, h3, [role]"));
1101
+ return candidates.find((element) => {
1102
+ if (!isElementVisible(element) || isDisabled(element)) return false;
1103
+ const text = normalize(`${element.getAttribute("aria-label") ?? ""} ${element.textContent ?? ""}`);
1104
+ const wanted = normalize(`${mapped.label} ${mapped.text}`);
1105
+ const role = normalize(element.getAttribute("role") ?? "");
1106
+ return wanted.length > 0 && (text.includes(wanted) || wanted.includes(text)) || mapped.role !== null && role === normalize(mapped.role);
1107
+ }) ?? null;
986
1108
  }
987
1109
  matchesMapped(item, element, ref) {
988
1110
  if (ref.id && item.id === ref.id) {
@@ -1627,14 +1749,110 @@ var AvatarController = class {
1627
1749
  };
1628
1750
 
1629
1751
  // src/interaction/permissions.ts
1630
- function isActionAllowed(action, config, source) {
1752
+ function normalizeHost(host) {
1753
+ return host.replace(/^https?:\/\//i, "").replace(/\/$/, "").toLowerCase();
1754
+ }
1755
+ function matchesRoutePattern(pathname, pattern) {
1756
+ const candidate = pathname;
1757
+ const normalized = pattern.trim();
1758
+ if (!normalized) {
1759
+ return false;
1760
+ }
1761
+ const escaped = normalized.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*");
1762
+ return new RegExp(`^${escaped}$`, "i").test(candidate);
1763
+ }
1764
+ function isLikelyJavaScriptUrl(value) {
1765
+ const lower = value.trim().toLowerCase();
1766
+ return lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:") || lower.includes("eval(") || lower.includes("new function") || lower.includes("document.cookie");
1767
+ }
1768
+ function validateWebsiteDomain(config, currentUrl) {
1769
+ const allowedDomains = config.allowedDomains.map((domain) => normalizeHost(domain));
1770
+ if (allowedDomains.length === 0) {
1771
+ return { allowed: true };
1772
+ }
1773
+ try {
1774
+ const parsed = new URL(currentUrl);
1775
+ const host = normalizeHost(parsed.host);
1776
+ const allowed = allowedDomains.some((domain) => host === domain || host.endsWith(`.${domain}`));
1777
+ if (!allowed) {
1778
+ return { allowed: false, reason: "DOMAIN_NOT_AUTHORIZED" };
1779
+ }
1780
+ return { allowed: true };
1781
+ } catch {
1782
+ return { allowed: false, reason: "DOMAIN_NOT_AUTHORIZED" };
1783
+ }
1784
+ }
1785
+ function canNavigateTo(config, targetUrl, currentUrl) {
1786
+ if (!targetUrl || typeof targetUrl !== "string") {
1787
+ return { allowed: false, reason: "ACTION_NOT_PERMITTED" };
1788
+ }
1789
+ if (isLikelyJavaScriptUrl(targetUrl)) {
1790
+ return { allowed: false, reason: "ACTION_NOT_PERMITTED" };
1791
+ }
1792
+ let target;
1793
+ try {
1794
+ target = new URL(targetUrl, currentUrl);
1795
+ } catch {
1796
+ return { allowed: false, reason: "ACTION_NOT_PERMITTED" };
1797
+ }
1798
+ if (!["http:", "https:"].includes(target.protocol)) {
1799
+ return { allowed: false, reason: "ACTION_NOT_PERMITTED" };
1800
+ }
1801
+ const domainCheck = validateWebsiteDomain(config, currentUrl);
1802
+ if (!domainCheck.allowed) {
1803
+ return domainCheck;
1804
+ }
1805
+ const host = normalizeHost(target.host);
1806
+ const allowedDomains = config.allowedDomains.map((domain) => normalizeHost(domain));
1807
+ if (allowedDomains.length > 0 && !allowedDomains.some((domain) => host === domain || host.endsWith(`.${domain}`))) {
1808
+ return { allowed: false, reason: "DOMAIN_NOT_ALLOWED" };
1809
+ }
1810
+ const pathname = target.pathname || "/";
1811
+ if (config.blockedRoutes.some((route) => matchesRoutePattern(pathname, route))) {
1812
+ return { allowed: false, reason: "ROUTE_BLOCKED" };
1813
+ }
1814
+ if (config.allowedRoutes.length > 0 && !config.allowedRoutes.some((route) => matchesRoutePattern(pathname, route))) {
1815
+ return { allowed: false, reason: "ROUTE_NOT_PERMITTED" };
1816
+ }
1817
+ return { allowed: true };
1818
+ }
1819
+ function evaluateActionPermission(action, config, currentUrl, target) {
1820
+ const explicitRule = config.rules.find((rule) => rule.action === action && rule.enabled !== false);
1821
+ if (explicitRule && typeof explicitRule.allowed === "boolean") {
1822
+ if (!explicitRule.allowed) {
1823
+ return { allowed: false, reason: "ACTION_NOT_PERMITTED" };
1824
+ }
1825
+ }
1826
+ if (!config.allowedActions.includes(action)) {
1827
+ return { allowed: false, reason: "ACTION_NOT_PERMITTED" };
1828
+ }
1829
+ if (action === "navigate") {
1830
+ if (!target) {
1831
+ return { allowed: true };
1832
+ }
1833
+ return canNavigateTo(config, target, currentUrl);
1834
+ }
1835
+ const allowedDomains = config.allowedDomains;
1836
+ if (allowedDomains.length > 0) {
1837
+ const domainCheck = validateWebsiteDomain(config, currentUrl);
1838
+ if (!domainCheck.allowed) {
1839
+ return domainCheck;
1840
+ }
1841
+ }
1842
+ return { allowed: true };
1843
+ }
1844
+ function isActionAllowed(action, config, source, currentUrl, target) {
1631
1845
  if (source === "developer") {
1632
1846
  return true;
1633
1847
  }
1634
- return config.allowedActions.includes(action);
1848
+ if (currentUrl) {
1849
+ return evaluateActionPermission(action, config, currentUrl, target).allowed;
1850
+ }
1851
+ return config.allowedActions.includes(action) && !config.rules.some((rule) => rule.action === action && rule.allowed === false);
1635
1852
  }
1636
- function assertActionAllowed(action, config, source) {
1637
- if (!isActionAllowed(action, config, source)) {
1853
+ function assertActionAllowed(action, config, source, currentUrl, target) {
1854
+ const allowed = isActionAllowed(action, config, source, currentUrl, target);
1855
+ if (!allowed) {
1638
1856
  throw new ActionNotAllowedError(`Action "${action}" is not permitted for this project`);
1639
1857
  }
1640
1858
  }
@@ -1664,7 +1882,12 @@ var InteractionEngine = class {
1664
1882
  }
1665
1883
  async execute(step, source) {
1666
1884
  const config = this.getConfig();
1667
- assertActionAllowed(step.action, config, source);
1885
+ const currentUrl = this.win.location.href;
1886
+ const target = step.url ?? (typeof step.target === "string" ? step.target : void 0);
1887
+ const allowed = this.evaluatePermission(step.action, config, currentUrl, target, source);
1888
+ if (!allowed.ok) {
1889
+ return { ok: false, waitedForUser: false, allowed: false, reason: allowed.reason ?? "ACTION_NOT_PERMITTED" };
1890
+ }
1668
1891
  const wait = shouldWaitForUser(step.action, config.mode, step.waitForUser);
1669
1892
  switch (step.action) {
1670
1893
  case "explain":
@@ -1695,6 +1918,14 @@ var InteractionEngine = class {
1695
1918
  throw new TaskExecutionError("Unsupported action");
1696
1919
  }
1697
1920
  }
1921
+ evaluatePermission(action, config, currentUrl, target, source) {
1922
+ try {
1923
+ assertActionAllowed(action, config, source, currentUrl, target);
1924
+ return { ok: true };
1925
+ } catch {
1926
+ return { ok: false, reason: "ACTION_NOT_PERMITTED" };
1927
+ }
1928
+ }
1698
1929
  async highlight(step, source, wait) {
1699
1930
  if (!step.target) {
1700
1931
  throw new TaskExecutionError("Highlight requires a target");
@@ -1878,6 +2109,11 @@ var TaskEngine = class {
1878
2109
  }
1879
2110
  this.move(TaskStatus.ActionStarted);
1880
2111
  const result = await this.interaction.execute(step, source);
2112
+ if (!result.ok) {
2113
+ this.events.emit("asiyst:task:failed", { taskId: task.id, reason: result.reason ?? "ACTION_NOT_PERMITTED" });
2114
+ this.analytics.track("task_failed", { taskId: task.id });
2115
+ return this.finish(TaskStatus.Failed, result.reason ?? "ACTION_NOT_PERMITTED");
2116
+ }
1881
2117
  if (result.waitedForUser && result.elementId) {
1882
2118
  this.move(TaskStatus.WaitingForUser);
1883
2119
  const clicked = await this.interaction.waitForElementClick(result.elementId, step.timeoutMs ?? 3e4);
@@ -2180,6 +2416,15 @@ var AsiystRuntime = class {
2180
2416
  async start() {
2181
2417
  this.events.emit("asiyst:initialized", { projectId: this.options.projectId });
2182
2418
  const cfg = await isolateAsync(() => this.config.refresh(), this.config.get());
2419
+ const domainCheck = validateWebsiteDomain(cfg, window.location.href);
2420
+ if (!domainCheck.allowed) {
2421
+ this.connectionStatus = "disconnected";
2422
+ this.events.emit("asiyst:error", {
2423
+ code: "domain_not_authorized",
2424
+ message: "This website is not authorized for the current Asiyst project."
2425
+ });
2426
+ return;
2427
+ }
2183
2428
  this.avatar.applyConfig(cfg);
2184
2429
  this.avatar.show();
2185
2430
  try {
@@ -2386,12 +2631,16 @@ exports.TargetNotFoundError = TargetNotFoundError;
2386
2631
  exports.TaskExecutionError = TaskExecutionError;
2387
2632
  exports.TaskStatus = TaskStatus;
2388
2633
  exports.anchorToViewport = anchorToViewport;
2634
+ exports.canNavigateTo = canNavigateTo;
2389
2635
  exports.canTransition = canTransition;
2390
2636
  exports.computeAvatarDestination = computeAvatarDestination;
2637
+ exports.evaluateActionPermission = evaluateActionPermission;
2391
2638
  exports.fallbackConfig = fallbackConfig;
2639
+ exports.isActionAllowed = isActionAllowed;
2392
2640
  exports.isSafeSelector = isSafeSelector;
2393
2641
  exports.normalizeProjectConfig = normalizeProjectConfig;
2394
2642
  exports.sanitizeText = sanitizeText;
2395
2643
  exports.validateInitOptions = validateInitOptions;
2644
+ exports.validateWebsiteDomain = validateWebsiteDomain;
2396
2645
  //# sourceMappingURL=index.cjs.map
2397
2646
  //# sourceMappingURL=index.cjs.map