@foir/sdk 0.29.0 → 0.30.1

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/data.js CHANGED
@@ -1782,7 +1782,8 @@ function describe(payload) {
1782
1782
  var MODEL_SUBSCRIPTION_METHODS = /* @__PURE__ */ new Set(["onChange"]);
1783
1783
  var NAMESPACE_SUBSCRIPTION_METHODS = {
1784
1784
  notifications: /* @__PURE__ */ new Set(["onNotification"]),
1785
- operations: /* @__PURE__ */ new Set(["onJobProgress", "onExecution"])
1785
+ operations: /* @__PURE__ */ new Set(["onJobProgress", "onExecution"]),
1786
+ agents: /* @__PURE__ */ new Set(["onRun"])
1786
1787
  };
1787
1788
  function isModelSubscriptionMethod(name) {
1788
1789
  return MODEL_SUBSCRIPTION_METHODS.has(name);
@@ -1866,6 +1867,25 @@ var FoirSubscriptionRouter = class {
1866
1867
  { ...handlers, onData: (event) => handlers.onData(executionEventWithEnumStatus(event)) }
1867
1868
  );
1868
1869
  }
1870
+ if (namespace === "agents" && method === "onRun") {
1871
+ const id = input.id;
1872
+ if (typeof id !== "string" || id === "") {
1873
+ throw new FoirRequestError("[foir] onRun requires a run `id`");
1874
+ }
1875
+ const args = [{ name: "id", type: "String!" }];
1876
+ const variables = { id };
1877
+ for (const name of ["after", "visitorToken"]) {
1878
+ const v = input[name];
1879
+ if (typeof v === "string" && v !== "") {
1880
+ args.push({ name, type: "String" });
1881
+ variables[name] = v;
1882
+ }
1883
+ }
1884
+ return this.open(
1885
+ { field: "agentRun", operationName: "FoirAgentRun", args, variables },
1886
+ handlers
1887
+ );
1888
+ }
1869
1889
  throw new FoirRequestError(`[foir] ns.${namespace}.${method} is not a subscription`);
1870
1890
  }
1871
1891
  /**
@@ -1949,12 +1969,24 @@ function normalise(value) {
1949
1969
  }
1950
1970
 
1951
1971
  // src/data/subjects.ts
1972
+ function markToken(modelKey, recordId, field) {
1973
+ const text = `${modelKey}\0${recordId}\0${field}`;
1974
+ let fnv = 2166136261;
1975
+ let djb = 5381;
1976
+ for (let i = 0; i < text.length; i++) {
1977
+ const code = text.charCodeAt(i);
1978
+ fnv = Math.imul(fnv ^ code, 16777619) >>> 0;
1979
+ djb = (Math.imul(djb, 33) ^ code) >>> 0;
1980
+ }
1981
+ return `f${fnv.toString(36)}${djb.toString(36)}`;
1982
+ }
1952
1983
  var keyOf = (modelKey, recordId) => `${modelKey}\0${recordId}`;
1953
1984
  function createFoirSubjectRegistry(options = {}) {
1954
1985
  const held = /* @__PURE__ */ new Map();
1955
1986
  const listeners = /* @__PURE__ */ new Set();
1956
1987
  const pinned = { subject: null };
1957
1988
  const latest = { key: null };
1989
+ const marks = /* @__PURE__ */ new Map();
1958
1990
  const notify = () => {
1959
1991
  const failures = [];
1960
1992
  for (const listener of [...listeners]) {
@@ -2025,6 +2057,25 @@ function createFoirSubjectRegistry(options = {}) {
2025
2057
  return () => {
2026
2058
  listeners.delete(listener);
2027
2059
  };
2060
+ },
2061
+ field(subject, field, fieldOptions = {}) {
2062
+ if (!subject.modelKey || !subject.recordId || !field) {
2063
+ throw new FoirRequestError(
2064
+ "[foir] a field mark needs a modelKey, a recordId and a field \u2014 a mark missing one points at nothing a note can be filed against"
2065
+ );
2066
+ }
2067
+ const token = markToken(subject.modelKey, subject.recordId, field);
2068
+ const mark = {
2069
+ subject: { modelKey: subject.modelKey, recordId: subject.recordId },
2070
+ field
2071
+ };
2072
+ if (subject.label) mark.subject.label = subject.label;
2073
+ if (fieldOptions.label) mark.label = fieldOptions.label;
2074
+ marks.set(token, mark);
2075
+ return { "data-foir-field": token };
2076
+ },
2077
+ pointedAt(token) {
2078
+ return marks.get(token);
2028
2079
  }
2029
2080
  };
2030
2081
  }
@@ -2716,6 +2767,119 @@ var FoirDecimal = class _FoirDecimal {
2716
2767
  function decimal(value) {
2717
2768
  return new FoirDecimal(value);
2718
2769
  }
2770
+
2771
+ // src/data/agents.ts
2772
+ async function uploadAgentFile(foir, options) {
2773
+ const agents = foir.ns.agents;
2774
+ if (typeof agents?.createAgentAttachment !== "function") {
2775
+ throw new Error("[foir] uploadAgentFile: this client sends no agent files. Its key runs none of the project's agents, or the project has released none.");
2776
+ }
2777
+ const { blob, mediaType, filename } = fileParts(options.file);
2778
+ const up = await agents.createAgentAttachment({
2779
+ agent: options.agent,
2780
+ filename,
2781
+ mediaType,
2782
+ size: blob.size,
2783
+ ...options.visitorToken ? { visitorToken: options.visitorToken } : {}
2784
+ });
2785
+ const put = await (options.fetch ?? fetch)(up.uploadUrl, { method: "PUT", headers: { "content-type": mediaType }, body: blob });
2786
+ if (!put.ok) throw new Error(`[foir] uploadAgentFile: ${filename} was not uploaded (${put.status}).`);
2787
+ return up.attachment;
2788
+ }
2789
+ function fileParts(file) {
2790
+ if (typeof Blob !== "undefined" && file instanceof Blob) {
2791
+ const name = file.name;
2792
+ return { blob: file, mediaType: file.type, filename: name || "file" };
2793
+ }
2794
+ const { data, mediaType, filename } = file;
2795
+ const blob = data instanceof Blob ? data : new Blob([data], { type: mediaType });
2796
+ return { blob, mediaType, filename: filename || "file" };
2797
+ }
2798
+ var FINAL = /* @__PURE__ */ new Set(["COMPLETED", "FAILED", "SKIPPED", "CANCELLED", "HELD"]);
2799
+ async function askAgent(foir, options) {
2800
+ const agents = foir.ns.agents;
2801
+ if (typeof agents?.startAgentRun !== "function" || typeof agents.onRun !== "function") {
2802
+ throw new Error(
2803
+ "[foir] askAgent: this client offers no agents. Its key runs none of the project's agents (agents:execute), or the project has released none."
2804
+ );
2805
+ }
2806
+ const { onText, onEvent, signal, files, ...args } = options;
2807
+ if (files?.length) {
2808
+ const sent = await Promise.all(files.map((file) => uploadAgentFile(foir, { agent: args.agent, file, visitorToken: args.visitorToken })));
2809
+ args.attachmentIds = [...args.attachmentIds ?? [], ...sent.map((a) => a.id)];
2810
+ }
2811
+ const run = await agents.startAgentRun(args);
2812
+ const reply = {
2813
+ runId: run.id,
2814
+ threadId: run.threadId ?? null,
2815
+ status: run.status,
2816
+ answer: run.answer ?? "",
2817
+ error: run.error ?? null,
2818
+ ...run.output === void 0 || run.output === null ? {} : { output: run.output }
2819
+ };
2820
+ if (FINAL.has(run.status) && run.status !== "HELD") return reply;
2821
+ let written = "";
2822
+ return new Promise((resolve, reject) => {
2823
+ let settled = false;
2824
+ const finish = (sub2, err) => {
2825
+ if (settled) return;
2826
+ settled = true;
2827
+ sub2?.unsubscribe();
2828
+ if (err !== void 0) reject(err);
2829
+ else resolve({ ...reply, answer: reply.answer || written });
2830
+ };
2831
+ let sub = null;
2832
+ sub = agents.onRun({
2833
+ id: run.id,
2834
+ visitorToken: args.visitorToken,
2835
+ onData: (event) => {
2836
+ onEvent?.(event);
2837
+ if (event.kind === "TEXT" && event.text) {
2838
+ written += event.text;
2839
+ onText?.(event.text);
2840
+ }
2841
+ if (event.kind === "STATUS" && event.status) {
2842
+ reply.status = event.status;
2843
+ if (event.answer) reply.answer = event.answer;
2844
+ if (event.error) reply.error = event.error;
2845
+ if (event.output !== void 0 && event.output !== null) reply.output = event.output;
2846
+ if (FINAL.has(event.status)) finish(sub);
2847
+ }
2848
+ },
2849
+ onError: (err) => finish(sub, err),
2850
+ onComplete: () => finish(sub)
2851
+ });
2852
+ signal?.addEventListener("abort", () => finish(sub), { once: true });
2853
+ });
2854
+ }
2855
+ var VISITOR_KEY = "foir:agents:visitor";
2856
+ function visitorToken(storage = browserStorage(), key = VISITOR_KEY) {
2857
+ try {
2858
+ const kept = storage?.getItem(key);
2859
+ if (kept) return kept;
2860
+ } catch {
2861
+ }
2862
+ const made = randomSecret();
2863
+ try {
2864
+ storage?.setItem(key, made);
2865
+ } catch {
2866
+ }
2867
+ return made;
2868
+ }
2869
+ function browserStorage() {
2870
+ try {
2871
+ return typeof localStorage === "undefined" ? null : localStorage;
2872
+ } catch {
2873
+ return null;
2874
+ }
2875
+ }
2876
+ function randomSecret() {
2877
+ const bytes = new Uint8Array(24);
2878
+ globalThis.crypto.getRandomValues(bytes);
2879
+ let out = "";
2880
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
2881
+ return out;
2882
+ }
2719
2883
  export {
2720
2884
  FOIR_VISITOR_IP_HEADER,
2721
2885
  FoirAuthDowngradedError,
@@ -2724,6 +2888,7 @@ export {
2724
2888
  FoirSchemaDriftError,
2725
2889
  FoirSessionExpiredError,
2726
2890
  FoirSocketTransport,
2891
+ askAgent,
2727
2892
  createFoirClient,
2728
2893
  createFoirClientFromEnv,
2729
2894
  createFoirSubjectRegistry,
@@ -2737,5 +2902,7 @@ export {
2737
2902
  planFoirNamespaceCall,
2738
2903
  printSubscriptionDocument,
2739
2904
  recordFoirSubjects,
2740
- stepUpRequired
2905
+ stepUpRequired,
2906
+ uploadAgentFile,
2907
+ visitorToken
2741
2908
  };
package/dist/placement.js CHANGED
@@ -32,6 +32,12 @@ var GRANT_TABLE = [
32
32
  ["operations:write", "none", [], []],
33
33
  ["operations:complete", "none", [], []],
34
34
  ["status:write", "none", [], []],
35
+ ["agents:read", "none", [], []],
36
+ ["agents:execute", "agent", [], []],
37
+ ["agents:write", "none", [], []],
38
+ ["agenttools:execute", "agent_tool", [], []],
39
+ ["brief:read", "none", [], []],
40
+ ["brief:write", "none", [], []],
35
41
  ["appconfig:read", "none", [], []],
36
42
  ["schedules:read", "none", [], []],
37
43
  ["schedules:write", "none", [], []],
@@ -81,7 +87,7 @@ var GRANT_TABLE = [
81
87
  ["projects:read", "none", [], []],
82
88
  ["projects:write", "none", [], []],
83
89
  ["project:stand", "none", [], []],
84
- ["project:manage", "none", [], ["project:stand", "records:read.all", "records:create", "records:write.all", "records:delete.all", "records:publish.all", "records:attribute", "search:read", "embeddings:read", "embeddings:write", "schemas:read", "drafts:read", "files:read.all", "files:write.all", "files:delete.all", "users:read.all", "identities:read", "users:create", "identities:delete", "users:write.all", "users:setup", "users:delete.all", "notifications:read.all", "notifications:write.all", "notifications:consent", "pushcredentials:read", "pushcredentials:write", "notes:read.all", "notes:write.all", "notes:create", "approvals:read", "approvals:write", "rooms:read", "rooms:write", "presence:read", "operations:read", "operations:execute", "operations:write", "appconfig:read", "schedules:read", "schedules:write", "hooks:read", "hooks:write", "authclients:read", "authclients:write", "domains:read", "domains:write", "loginpages:read", "loginpages:write", "secrets:read", "secrets:list", "secrets:put", "secrets:delete", "appsecrets:read", "appsecrets:list", "appsecrets:put", "appsecrets:delete", "usersecrets:read", "usersecrets:list", "usersecrets:put", "usersecrets:delete", "opsecrets:read", "opsecrets:write", "settings:read", "settings:write", "designtokens:read", "designtokens:write", "models:read", "models:write", "configs:read", "configs:write", "signinproviders:read", "signinproviders:write", "apps:read", "apps:write", "events:read", "targeting:debug", "visitors:forward", "apikeys:read", "apikeys:write", "organizations:read", "organizations:create", "organizations:delete", "organizations:write", "orgsettings:write", "organizations:reach", "members:read", "members:write", "roles:read", "roles:write", "projects:read", "projects:write", "audit:read"]],
90
+ ["project:manage", "none", [], ["project:stand", "records:read.all", "records:create", "records:write.all", "records:delete.all", "records:publish.all", "records:attribute", "search:read", "embeddings:read", "embeddings:write", "schemas:read", "drafts:read", "files:read.all", "files:write.all", "files:delete.all", "users:read.all", "identities:read", "users:create", "identities:delete", "users:write.all", "users:setup", "users:delete.all", "notifications:read.all", "notifications:write.all", "notifications:consent", "pushcredentials:read", "pushcredentials:write", "notes:read.all", "notes:write.all", "notes:create", "approvals:read", "approvals:write", "rooms:read", "rooms:write", "presence:read", "operations:read", "operations:execute", "operations:write", "agents:read", "agents:execute", "agents:write", "agenttools:execute", "brief:read", "brief:write", "appconfig:read", "schedules:read", "schedules:write", "hooks:read", "hooks:write", "authclients:read", "authclients:write", "domains:read", "domains:write", "loginpages:read", "loginpages:write", "secrets:read", "secrets:list", "secrets:put", "secrets:delete", "appsecrets:read", "appsecrets:list", "appsecrets:put", "appsecrets:delete", "usersecrets:read", "usersecrets:list", "usersecrets:put", "usersecrets:delete", "opsecrets:read", "opsecrets:write", "settings:read", "settings:write", "designtokens:read", "designtokens:write", "models:read", "models:write", "configs:read", "configs:write", "signinproviders:read", "signinproviders:write", "apps:read", "apps:write", "events:read", "targeting:debug", "visitors:forward", "apikeys:read", "apikeys:write", "organizations:read", "organizations:create", "organizations:delete", "organizations:write", "orgsettings:write", "organizations:reach", "members:read", "members:write", "roles:read", "roles:write", "projects:read", "projects:write", "audit:read"]],
85
91
  ["projects:create", "none", [], []],
86
92
  ["projects:delete", "none", [], []],
87
93
  ["tenants:read", "none", [], []],
@@ -59,6 +59,23 @@ export interface FoirFeedbackProps {
59
59
  * with a backdrop, closed by Escape, the backdrop or its Close button.
60
60
  */
61
61
  layout?: 'inline' | 'dialog';
62
+ /**
63
+ * Show the button fixed in a corner of the viewport, rather than where the
64
+ * component is placed, with the inline panel opening above it. Default: where
65
+ * it is placed.
66
+ */
67
+ launcher?: 'bottom-right' | 'bottom-left';
68
+ /**
69
+ * Let the reader point at the part of the page they mean: a field the host
70
+ * marked with `subjects.field(record, key, { label })`. The note is then filed
71
+ * against that record and names the field, so a page showing two records (a
72
+ * menu and an article) files each comment against the right one.
73
+ *
74
+ * Needs `subjects`, and is offered only while the page shows a marked field.
75
+ * A mouse points by clicking; the keyboard by Tab and Enter; Escape cancels.
76
+ * Default false.
77
+ */
78
+ pointing?: boolean;
62
79
  /**
63
80
  * The built-in styles' colours, radius and type. See
64
81
  * `FoirFeedbackAppearance`: each is also a CSS custom property, so a
@@ -121,4 +138,4 @@ export interface FoirFeedbackAppearance {
121
138
  /** `--foir-feedback-font-size`. Default: inherited. */
122
139
  fontSize?: string;
123
140
  }
124
- export declare function FoirFeedback({ client, subjects, subject, label, triggerLabel, title, placeholder, defaultOpen, onSignIn, signIn, layout, appearance, onSubmitted, onVisitorSubmitted, onError, className, }: FoirFeedbackProps): import("react/jsx-runtime").JSX.Element;
141
+ export declare function FoirFeedback({ client, subjects, subject, label, triggerLabel, title, placeholder, defaultOpen, onSignIn, signIn, layout, launcher, pointing, appearance, onSubmitted, onVisitorSubmitted, onError, className, }: FoirFeedbackProps): import("react/jsx-runtime").JSX.Element;
@@ -2,7 +2,7 @@ import type { FoirUser } from '../auth';
2
2
  export interface SignInProps {
3
3
  /** `password` (default) or `otp` (email one-time code). Ignored when `hostedUrl` is set. */
4
4
  method?: 'password' | 'otp';
5
- /** Render a single button that redirects to a hosted login page instead of an embedded form. */
5
+ /** Render a single button that redirects to the hosted sign-in page instead of an embedded form. */
6
6
  hostedUrl?: string;
7
7
  /** Show OAuth provider buttons below the form. Default true. */
8
8
  oauth?: boolean;
@@ -15,7 +15,7 @@ export interface SignInProps {
15
15
  /**
16
16
  * A drop-in user sign-in. Renders an email/password (or OTP) form and,
17
17
  * unless disabled, OAuth provider buttons. Set `hostedUrl` to instead render a
18
- * button that redirects to a hosted login page. All state is handled through
18
+ * button that redirects to the hosted sign-in page. All state is handled through
19
19
  * {@link FoirProvider}; on success it calls `onSignIn` and optionally navigates
20
20
  * to `afterSignInUrl`.
21
21
  */
@@ -113,6 +113,8 @@ export interface FoirFeedbackSubmission {
113
113
  */
114
114
  subject: FoirSubject;
115
115
  body: string;
116
+ /** The field of the subject the reader pointed at, when they pointed at one. */
117
+ anchorField?: string;
116
118
  /** The still the visitor chose to attach, or null. */
117
119
  screenshot: FoirScreenCapture | null;
118
120
  /** Injectable for the presigned PUT. Defaults to `globalThis.fetch`. */
@@ -132,6 +134,11 @@ export interface FoirVisitorSubmission {
132
134
  /** What the note is about, as for a signed-in user's. */
133
135
  subject: FoirSubject;
134
136
  body: string;
137
+ /**
138
+ * The field of the subject the visitor pointed at. The data API takes a
139
+ * field the visitor can read and refuses any other.
140
+ */
141
+ anchorField?: string;
135
142
  /**
136
143
  * Where the project's team may reply, as the visitor typed it. Sent only
137
144
  * when it is not blank, and never verified by anyone.