@foir/sdk 0.28.0 → 0.30.0

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/react.js CHANGED
@@ -1133,8 +1133,8 @@ async function getSessionUser(adapter) {
1133
1133
  async function signInWithPassword(adapter, env, vars) {
1134
1134
  return saveSession(adapter, await login(env, vars));
1135
1135
  }
1136
- async function signUpWithPassword(adapter, env, vars) {
1137
- return saveSession(adapter, await register(env, vars));
1136
+ async function signUpWithPassword(env, vars) {
1137
+ await register(env, vars);
1138
1138
  }
1139
1139
  async function signInWithOTP(adapter, env, vars) {
1140
1140
  return saveSession(adapter, await loginWithOTP(env, vars));
@@ -1242,6 +1242,12 @@ var GRANT_TABLE = [
1242
1242
  ["operations:write", "none", [], []],
1243
1243
  ["operations:complete", "none", [], []],
1244
1244
  ["status:write", "none", [], []],
1245
+ ["agents:read", "none", [], []],
1246
+ ["agents:execute", "agent", [], []],
1247
+ ["agents:write", "none", [], []],
1248
+ ["agenttools:execute", "agent_tool", [], []],
1249
+ ["brief:read", "none", [], []],
1250
+ ["brief:write", "none", [], []],
1245
1251
  ["appconfig:read", "none", [], []],
1246
1252
  ["schedules:read", "none", [], []],
1247
1253
  ["schedules:write", "none", [], []],
@@ -1280,6 +1286,7 @@ var GRANT_TABLE = [
1280
1286
  ["apikeys:write", "none", [], []],
1281
1287
  ["organizations:read", "none", [], []],
1282
1288
  ["organizations:write", "none", [], []],
1289
+ ["orgsettings:write", "none", [], []],
1283
1290
  ["organizations:create", "none", [], []],
1284
1291
  ["organizations:delete", "none", [], []],
1285
1292
  ["organizations:reach", "none", [], []],
@@ -1290,7 +1297,7 @@ var GRANT_TABLE = [
1290
1297
  ["projects:read", "none", [], []],
1291
1298
  ["projects:write", "none", [], []],
1292
1299
  ["project:stand", "none", [], []],
1293
- ["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", "auth:login_with_foir", "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", "organizations:reach", "members:read", "members:write", "roles:read", "roles:write", "projects:read", "projects:write", "audit:read"]],
1300
+ ["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"]],
1294
1301
  ["projects:create", "none", [], []],
1295
1302
  ["projects:delete", "none", [], []],
1296
1303
  ["tenants:read", "none", [], []],
@@ -1321,7 +1328,6 @@ var GRANT_TABLE = [
1321
1328
  ["usersecrets:list", "none", [], []],
1322
1329
  ["usersecrets:put", "none", [], []],
1323
1330
  ["usersecrets:delete", "none", [], []],
1324
- ["auth:login_with_foir", "none", [], []],
1325
1331
  ["signinproviders:read", "none", [], []],
1326
1332
  ["signinproviders:write", "none", [], []]
1327
1333
  ];
@@ -1680,13 +1686,11 @@ function FoirProvider({
1680
1686
  [apply, assertTokenMode]
1681
1687
  );
1682
1688
  const register2 = useCallback(
1683
- async (vars) => {
1689
+ (vars) => {
1684
1690
  assertTokenMode("register");
1685
- const c = await signUpWithPassword(adapterRef.current, envRef.current, vars);
1686
- apply(c);
1687
- return c;
1691
+ return signUpWithPassword(envRef.current, vars);
1688
1692
  },
1689
- [apply, assertTokenMode]
1693
+ [assertTokenMode]
1690
1694
  );
1691
1695
  const requestOtp = useCallback(
1692
1696
  (email) => {
@@ -2265,7 +2269,7 @@ async function submitFeedbackNote(client, submission) {
2265
2269
  await uploadFeedbackAttachment(client, submission.screenshot, submission.fetch)
2266
2270
  );
2267
2271
  }
2268
- const args = noteArgs(submission.subject, submission.body);
2272
+ const args = noteArgs(submission.subject, submission.body, submission.anchorField);
2269
2273
  if (attachmentFileIds.length > 0) args.attachmentFileIds = attachmentFileIds;
2270
2274
  const { notes } = client.ns;
2271
2275
  return readCreatedNote(
@@ -2279,7 +2283,11 @@ async function submitVisitorNote(client, submission) {
2279
2283
  "[foir] this client has no `notes.writeNote`, which is the only create that takes a note from a visitor who is not signed in. Re-run `foir types`."
2280
2284
  );
2281
2285
  }
2282
- const args = noteArgs(submission.subject, submission.body);
2286
+ const args = noteArgs(
2287
+ submission.subject,
2288
+ submission.body,
2289
+ submission.anchorField
2290
+ );
2283
2291
  const contactEmail = submission.contactEmail?.trim();
2284
2292
  if (contactEmail) args.contactEmail = contactEmail;
2285
2293
  args.pick = ["id", "createdAt"];
@@ -2294,13 +2302,15 @@ async function submitVisitorNote(client, submission) {
2294
2302
  createdAt: typeof result.createdAt === "string" ? result.createdAt : ""
2295
2303
  };
2296
2304
  }
2297
- function noteArgs(subject, body) {
2298
- return {
2305
+ function noteArgs(subject, body, anchorField) {
2306
+ const args = {
2299
2307
  subjectType: "record",
2300
2308
  subjectId: subject.recordId,
2301
2309
  subjectModelKey: subject.modelKey,
2302
2310
  body
2303
2311
  };
2312
+ if (anchorField) args.anchorField = anchorField;
2313
+ return args;
2304
2314
  }
2305
2315
  async function uploadFeedbackAttachment(client, screenshot, fetchImpl = globalThis.fetch) {
2306
2316
  const files = client.ns.files;
@@ -2424,7 +2434,11 @@ var COPY = {
2424
2434
  captureFailed: "The screenshot could not be taken. You can still send your feedback.",
2425
2435
  screenshotAlt: "The screenshot that will be sent with your feedback",
2426
2436
  removeScreenshot: "Remove screenshot",
2427
- about: "About"
2437
+ about: "About",
2438
+ point: "Point at what you mean",
2439
+ pointAgain: "Point at something else",
2440
+ pointHint: "Click the part of the page you mean, or use Tab and Enter. Escape cancels.",
2441
+ pointCancel: "Cancel"
2428
2442
  };
2429
2443
  var border = "1px solid var(--foir-feedback-border, currentColor)";
2430
2444
  var radius = "var(--foir-feedback-radius, 8px)";
@@ -2531,6 +2545,8 @@ function FoirFeedback({
2531
2545
  onSignIn,
2532
2546
  signIn = true,
2533
2547
  layout = "inline",
2548
+ launcher,
2549
+ pointing = false,
2534
2550
  appearance,
2535
2551
  onSubmitted,
2536
2552
  onVisitorSubmitted,
@@ -2546,10 +2562,32 @@ function FoirFeedback({
2546
2562
  const [busy, setBusy] = useState4(false);
2547
2563
  const [failure, setFailure] = useState4(null);
2548
2564
  const [sent, setSent] = useState4(false);
2565
+ const [pointed, setPointed] = useState4(null);
2566
+ const [pointingNow, setPointingNow] = useState4(false);
2567
+ const [canPoint, setCanPoint] = useState4(false);
2549
2568
  const onErrorRef = useRef2(onError);
2550
2569
  onErrorRef.current = onError;
2551
- const resolved = useResolvedSubject(subjects, subject);
2570
+ const fetched = useResolvedSubject(subjects, subject);
2571
+ const offersPointing = pointing && subjects !== void 0 && subject !== null;
2572
+ const resolved = offersPointing && pointed ? pointed.subject : fetched;
2552
2573
  const styled = !className;
2574
+ useEffect3(() => {
2575
+ if (!open || !offersPointing || !subjects) return;
2576
+ setCanPoint(markedFields(subjects).length > 0);
2577
+ }, [open, offersPointing, subjects]);
2578
+ const highlight = usePointing(pointingNow ? subjects : void 0, {
2579
+ onPick: (mark) => {
2580
+ setPointed(mark);
2581
+ setPointingNow(false);
2582
+ setSent(false);
2583
+ setFailure(null);
2584
+ setOpen(true);
2585
+ },
2586
+ onCancel: () => {
2587
+ setPointingNow(false);
2588
+ setOpen(true);
2589
+ }
2590
+ });
2553
2591
  useNoSubjectNotice(open, resolved, subjects !== void 0, subject !== void 0);
2554
2592
  useEffect3(() => {
2555
2593
  if (!open) return;
@@ -2615,6 +2653,7 @@ function FoirFeedback({
2615
2653
  const note = await submitFeedbackNote(client, {
2616
2654
  subject: resolved,
2617
2655
  body: prose,
2656
+ anchorField: pointed?.field,
2618
2657
  screenshot
2619
2658
  });
2620
2659
  setBody("");
@@ -2625,6 +2664,7 @@ function FoirFeedback({
2625
2664
  const written = await submitVisitorNote(client, {
2626
2665
  subject: resolved,
2627
2666
  body: prose,
2667
+ anchorField: pointed?.field,
2628
2668
  contactEmail
2629
2669
  });
2630
2670
  setBody("");
@@ -2656,9 +2696,11 @@ function FoirFeedback({
2656
2696
  }
2657
2697
  setSent(false);
2658
2698
  setFailure(null);
2699
+ setPointed(null);
2659
2700
  setOpen(true);
2660
2701
  };
2661
- const subjectName = resolved ? label ?? resolved.label : void 0;
2702
+ const recordName = resolved ? pointed ? resolved.label : label ?? resolved.label : void 0;
2703
+ const subjectName = [recordName, pointed?.label].filter(Boolean).join(" \xB7 ") || void 0;
2662
2704
  const dialog = layout === "dialog";
2663
2705
  const heading = /* @__PURE__ */ jsx5("h2", { "data-foir-feedback-title": true, style: styled ? headingStyle : void 0, children: title });
2664
2706
  const panel = open && /* @__PURE__ */ jsxs4(
@@ -2686,6 +2728,19 @@ function FoirFeedback({
2686
2728
  ": ",
2687
2729
  subjectName
2688
2730
  ] }),
2731
+ offersPointing && canPoint && probe.status === "ready" && /* @__PURE__ */ jsx5(
2732
+ "button",
2733
+ {
2734
+ type: "button",
2735
+ "data-foir-feedback-point": true,
2736
+ onClick: () => {
2737
+ setOpen(false);
2738
+ setPointingNow(true);
2739
+ },
2740
+ style: styled ? buttonStyle : void 0,
2741
+ children: pointed ? COPY.pointAgain : COPY.point
2742
+ }
2743
+ ),
2689
2744
  probe.status === "loading" && /* @__PURE__ */ jsx5(
2690
2745
  "p",
2691
2746
  {
@@ -2800,12 +2855,19 @@ function FoirFeedback({
2800
2855
  ]
2801
2856
  }
2802
2857
  );
2858
+ const launcherPlace = launcher ? {
2859
+ position: "fixed",
2860
+ bottom: 16,
2861
+ [launcher === "bottom-left" ? "left" : "right"]: 16,
2862
+ zIndex: LAUNCHER_LAYER
2863
+ } : void 0;
2803
2864
  return /* @__PURE__ */ jsxs4(
2804
2865
  "div",
2805
2866
  {
2806
2867
  className,
2807
2868
  "data-foir-feedback": true,
2808
2869
  "data-foir-feedback-layout": layout,
2870
+ "data-foir-feedback-launcher": launcher,
2809
2871
  style: styled ? wrapperStyle(appearance) : void 0,
2810
2872
  children: [
2811
2873
  /* @__PURE__ */ jsx5(
@@ -2816,10 +2878,25 @@ function FoirFeedback({
2816
2878
  "aria-expanded": open,
2817
2879
  "aria-haspopup": dialog ? "dialog" : void 0,
2818
2880
  onClick: toggle,
2819
- style: styled ? buttonStyle : void 0,
2881
+ style: {
2882
+ ...styled ? buttonStyle : void 0,
2883
+ ...styled && launcher ? launcherStyle : void 0,
2884
+ ...launcherPlace
2885
+ },
2820
2886
  children: triggerLabel
2821
2887
  }
2822
2888
  ),
2889
+ pointingNow && /* @__PURE__ */ jsx5(
2890
+ PointingChrome,
2891
+ {
2892
+ highlight,
2893
+ styled,
2894
+ onCancel: () => {
2895
+ setPointingNow(false);
2896
+ setOpen(true);
2897
+ }
2898
+ }
2899
+ ),
2823
2900
  dialog ? /* @__PURE__ */ jsx5(
2824
2901
  "dialog",
2825
2902
  {
@@ -2833,11 +2910,184 @@ function FoirFeedback({
2833
2910
  style: styled ? dialogStyle : void 0,
2834
2911
  children: panel
2835
2912
  }
2913
+ ) : launcher && panel ? /* @__PURE__ */ jsx5(
2914
+ "div",
2915
+ {
2916
+ "data-foir-feedback-launcher-panel": true,
2917
+ style: {
2918
+ position: "fixed",
2919
+ bottom: 64,
2920
+ [launcher === "bottom-left" ? "left" : "right"]: 16,
2921
+ zIndex: LAUNCHER_LAYER,
2922
+ ...styled ? { background: "var(--foir-feedback-background, Canvas)" } : void 0
2923
+ },
2924
+ children: panel
2925
+ }
2836
2926
  ) : panel
2837
2927
  ]
2838
2928
  }
2839
2929
  );
2840
2930
  }
2931
+ var LAUNCHER_LAYER = 2147483e3;
2932
+ var POINTING_LAYER = 2147483600;
2933
+ var launcherStyle = {
2934
+ background: "var(--foir-feedback-primary, Canvas)",
2935
+ color: "var(--foir-feedback-primary-text, CanvasText)",
2936
+ boxShadow: "0 4px 16px rgb(0 0 0 / 0.18)",
2937
+ padding: "10px 14px",
2938
+ fontWeight: 600
2939
+ };
2940
+ function markedFields(subjects) {
2941
+ if (typeof document === "undefined") return [];
2942
+ return Array.from(document.querySelectorAll("[data-foir-field]")).filter(
2943
+ (el) => subjects.pointedAt(el.getAttribute("data-foir-field") ?? "") !== void 0 && el.getClientRects().length > 0
2944
+ );
2945
+ }
2946
+ function markedFieldAt(target, subjects) {
2947
+ if (!(target instanceof Element)) return null;
2948
+ const el = target.closest("[data-foir-field]");
2949
+ if (!el) return null;
2950
+ const mark = subjects.pointedAt(el.getAttribute("data-foir-field") ?? "");
2951
+ return mark ? { el, mark } : null;
2952
+ }
2953
+ function usePointing(subjects, handlers) {
2954
+ const [highlight, setHighlight] = useState4(null);
2955
+ const handlersRef = useRef2(handlers);
2956
+ handlersRef.current = handlers;
2957
+ useEffect3(() => {
2958
+ if (!subjects) return;
2959
+ let current = null;
2960
+ let index2 = -1;
2961
+ const show = (el) => {
2962
+ current = el;
2963
+ if (!el) {
2964
+ setHighlight(null);
2965
+ return;
2966
+ }
2967
+ const box = el.getBoundingClientRect();
2968
+ setHighlight({ top: box.top, left: box.left, width: box.width, height: box.height });
2969
+ };
2970
+ const ours = (target) => target instanceof Element && target.closest("[data-foir-feedback-pointing]") !== null;
2971
+ const onMove = (event) => {
2972
+ if (ours(event.target)) return;
2973
+ show(markedFieldAt(event.target, subjects)?.el ?? null);
2974
+ };
2975
+ const onClick = (event) => {
2976
+ if (ours(event.target)) return;
2977
+ event.preventDefault();
2978
+ event.stopPropagation();
2979
+ const hit = markedFieldAt(event.target, subjects);
2980
+ if (hit) handlersRef.current.onPick(hit.mark);
2981
+ };
2982
+ const onKey = (event) => {
2983
+ if (event.key === "Escape") {
2984
+ event.preventDefault();
2985
+ handlersRef.current.onCancel();
2986
+ return;
2987
+ }
2988
+ if (event.key === "Tab") {
2989
+ const fields = markedFields(subjects);
2990
+ if (fields.length === 0) return;
2991
+ event.preventDefault();
2992
+ index2 = (index2 + (event.shiftKey ? -1 : 1) + fields.length) % fields.length;
2993
+ const el = fields[index2];
2994
+ el.scrollIntoView?.({ block: "nearest" });
2995
+ show(el);
2996
+ return;
2997
+ }
2998
+ if (event.key === "Enter" && current) {
2999
+ const mark = subjects.pointedAt(current.getAttribute("data-foir-field") ?? "");
3000
+ if (!mark) return;
3001
+ event.preventDefault();
3002
+ handlersRef.current.onPick(mark);
3003
+ }
3004
+ };
3005
+ const onScroll = () => show(current);
3006
+ const root = document.documentElement;
3007
+ const cursor = root.style.cursor;
3008
+ root.style.cursor = "crosshair";
3009
+ document.addEventListener("pointermove", onMove, true);
3010
+ document.addEventListener("click", onClick, true);
3011
+ document.addEventListener("keydown", onKey, true);
3012
+ window.addEventListener("scroll", onScroll, true);
3013
+ return () => {
3014
+ root.style.cursor = cursor;
3015
+ document.removeEventListener("pointermove", onMove, true);
3016
+ document.removeEventListener("click", onClick, true);
3017
+ document.removeEventListener("keydown", onKey, true);
3018
+ window.removeEventListener("scroll", onScroll, true);
3019
+ setHighlight(null);
3020
+ };
3021
+ }, [subjects]);
3022
+ return highlight;
3023
+ }
3024
+ function PointingChrome({
3025
+ highlight,
3026
+ styled,
3027
+ onCancel
3028
+ }) {
3029
+ return /* @__PURE__ */ jsxs4(Fragment2, { children: [
3030
+ highlight && /* @__PURE__ */ jsx5(
3031
+ "div",
3032
+ {
3033
+ "aria-hidden": "true",
3034
+ "data-foir-feedback-highlight": true,
3035
+ style: {
3036
+ position: "fixed",
3037
+ top: highlight.top - 4,
3038
+ left: highlight.left - 4,
3039
+ width: highlight.width + 8,
3040
+ height: highlight.height + 8,
3041
+ pointerEvents: "none",
3042
+ zIndex: POINTING_LAYER,
3043
+ ...styled ? {
3044
+ outline: "2px solid var(--foir-feedback-primary, Highlight)",
3045
+ borderRadius: "var(--foir-feedback-radius, 8px)"
3046
+ } : void 0
3047
+ }
3048
+ }
3049
+ ),
3050
+ /* @__PURE__ */ jsxs4(
3051
+ "div",
3052
+ {
3053
+ role: "status",
3054
+ "data-foir-feedback-pointing": true,
3055
+ style: {
3056
+ position: "fixed",
3057
+ bottom: 16,
3058
+ left: "50%",
3059
+ transform: "translateX(-50%)",
3060
+ zIndex: POINTING_LAYER,
3061
+ cursor: "auto",
3062
+ ...styled ? {
3063
+ display: "flex",
3064
+ alignItems: "center",
3065
+ gap: 12,
3066
+ padding: "10px 14px",
3067
+ borderRadius: radius,
3068
+ border,
3069
+ background: "var(--foir-feedback-background, Canvas)",
3070
+ color: "var(--foir-feedback-text, CanvasText)",
3071
+ boxShadow: "0 4px 16px rgb(0 0 0 / 0.18)"
3072
+ } : void 0
3073
+ },
3074
+ children: [
3075
+ /* @__PURE__ */ jsx5("span", { children: COPY.pointHint }),
3076
+ /* @__PURE__ */ jsx5(
3077
+ "button",
3078
+ {
3079
+ type: "button",
3080
+ "data-foir-feedback-point-cancel": true,
3081
+ onClick: onCancel,
3082
+ style: styled ? closeStyle : void 0,
3083
+ children: COPY.pointCancel
3084
+ }
3085
+ )
3086
+ ]
3087
+ }
3088
+ )
3089
+ ] });
3090
+ }
2841
3091
  async function probeRefusal(client) {
2842
3092
  try {
2843
3093
  await readNoteConfig(client);
@@ -71,6 +71,12 @@ export interface CreateCollabSessionOptions {
71
71
  room: RoomAddress | string;
72
72
  /** Async token source. Invoked before the first connect and on each refresh tick. */
73
73
  getToken: () => string | Promise<string>;
74
+ /**
75
+ * The organization the signed-in user stands at in the room, as a request
76
+ * names it. Omit for the project. The platform admits the join only where
77
+ * the user is a member.
78
+ */
79
+ orgId?: string;
74
80
  /** Caller-owned `Y.Doc` carrying the app-defined shared types. */
75
81
  doc: Y.Doc;
76
82
  /** Local presence. Sets awareness under the `user` field (contract shape). Omit for no presence. */
package/dist/realtime.js CHANGED
@@ -39,12 +39,13 @@ function createCollabSession(options) {
39
39
  refreshMs = 4 * 60 * 1e3,
40
40
  connect = true,
41
41
  params: extraParams,
42
+ orgId,
42
43
  WebSocketPolyfill
43
44
  } = options;
44
45
  const room = roomPath(options.room);
45
46
  const provider = new WebsocketProvider(url, room, doc, {
46
47
  connect: false,
47
- params: { ...extraParams },
48
+ params: { ...extraParams, ...orgId ? { org: orgId } : {} },
48
49
  ...WebSocketPolyfill ? { WebSocketPolyfill } : {}
49
50
  });
50
51
  const awareness = provider.awareness;
@@ -15,7 +15,9 @@
15
15
  *
16
16
  * The platform signs webhook payloads with HMAC-SHA256 using the
17
17
  * extension's webhook secret. The signature is sent in the
18
- * `x-foir-signature` header as a hex-encoded string.
18
+ * `x-foir-signature` header as a hex-encoded string. A base64 signature of
19
+ * the same HMAC is accepted too, so a header that carries one (Shopify's
20
+ * `X-Shopify-Hmac-Sha256`, for one) verifies as it arrives.
19
21
  *
20
22
  * @example
21
23
  * ```ts
package/dist/server.js CHANGED
@@ -63,14 +63,35 @@ async function verifyWebhookSignature(payload, signature, secret) {
63
63
  key,
64
64
  encoder.encode(payload)
65
65
  );
66
- const computed = Array.from(new Uint8Array(signatureBytes)).map((b) => b.toString(16).padStart(2, "0")).join("");
67
- if (computed.length !== signature.length) return false;
66
+ const computed = new Uint8Array(signatureBytes);
67
+ const presented = signatureBytesOf(signature.trim());
68
+ if (!presented || presented.length !== computed.length) return false;
68
69
  let mismatch = 0;
69
70
  for (let i = 0; i < computed.length; i++) {
70
- mismatch |= computed.charCodeAt(i) ^ signature.charCodeAt(i);
71
+ mismatch |= computed[i] ^ presented[i];
71
72
  }
72
73
  return mismatch === 0;
73
74
  }
75
+ function signatureBytesOf(signature) {
76
+ if (/^[0-9a-fA-F]+$/.test(signature) && signature.length % 2 === 0) {
77
+ const out = new Uint8Array(signature.length / 2);
78
+ for (let i = 0; i < out.length; i++) {
79
+ out[i] = parseInt(signature.slice(i * 2, i * 2 + 2), 16);
80
+ }
81
+ return out;
82
+ }
83
+ if (!/^[A-Za-z0-9+/_-]+={0,2}$/.test(signature)) return null;
84
+ const standard = signature.replace(/-/g, "+").replace(/_/g, "/");
85
+ const padded = standard + "=".repeat((4 - standard.length % 4) % 4);
86
+ try {
87
+ const binary = atob(padded);
88
+ const out = new Uint8Array(binary.length);
89
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
90
+ return out;
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
74
95
 
75
96
  // src/auth/grant-table.generated.ts
76
97
  var GRANT_TABLE = [
@@ -106,6 +127,12 @@ var GRANT_TABLE = [
106
127
  ["operations:write", "none", [], []],
107
128
  ["operations:complete", "none", [], []],
108
129
  ["status:write", "none", [], []],
130
+ ["agents:read", "none", [], []],
131
+ ["agents:execute", "agent", [], []],
132
+ ["agents:write", "none", [], []],
133
+ ["agenttools:execute", "agent_tool", [], []],
134
+ ["brief:read", "none", [], []],
135
+ ["brief:write", "none", [], []],
109
136
  ["appconfig:read", "none", [], []],
110
137
  ["schedules:read", "none", [], []],
111
138
  ["schedules:write", "none", [], []],
@@ -144,6 +171,7 @@ var GRANT_TABLE = [
144
171
  ["apikeys:write", "none", [], []],
145
172
  ["organizations:read", "none", [], []],
146
173
  ["organizations:write", "none", [], []],
174
+ ["orgsettings:write", "none", [], []],
147
175
  ["organizations:create", "none", [], []],
148
176
  ["organizations:delete", "none", [], []],
149
177
  ["organizations:reach", "none", [], []],
@@ -154,7 +182,7 @@ var GRANT_TABLE = [
154
182
  ["projects:read", "none", [], []],
155
183
  ["projects:write", "none", [], []],
156
184
  ["project:stand", "none", [], []],
157
- ["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", "auth:login_with_foir", "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", "organizations:reach", "members:read", "members:write", "roles:read", "roles:write", "projects:read", "projects:write", "audit:read"]],
185
+ ["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"]],
158
186
  ["projects:create", "none", [], []],
159
187
  ["projects:delete", "none", [], []],
160
188
  ["tenants:read", "none", [], []],
@@ -185,7 +213,6 @@ var GRANT_TABLE = [
185
213
  ["usersecrets:list", "none", [], []],
186
214
  ["usersecrets:put", "none", [], []],
187
215
  ["usersecrets:delete", "none", [], []],
188
- ["auth:login_with_foir", "none", [], []],
189
216
  ["signinproviders:read", "none", [], []],
190
217
  ["signinproviders:write", "none", [], []]
191
218
  ];
@@ -1107,8 +1134,18 @@ async function resolveFoirAuth(config) {
1107
1134
  key: { apiKey: config.apiKey, bearerToken }
1108
1135
  };
1109
1136
  }
1137
+ var renewing = /* @__PURE__ */ new WeakMap();
1138
+ function renewBearerOnce(refresh) {
1139
+ const running = renewing.get(refresh);
1140
+ if (running) return running;
1141
+ const next = refresh().finally(() => renewing.delete(refresh));
1142
+ renewing.set(refresh, next);
1143
+ return next;
1144
+ }
1110
1145
 
1111
1146
  // src/data/ws-transport.ts
1147
+ var CLOSE_CREDENTIAL_ENDED = 4402;
1148
+ var CLOSE_FORBIDDEN = 4403;
1112
1149
  var GQL = {
1113
1150
  connectionInit: "connection_init",
1114
1151
  connectionAck: "connection_ack",
@@ -1143,6 +1180,9 @@ var FoirSocketTransport = class {
1143
1180
  disposed = false;
1144
1181
  keepAlive = null;
1145
1182
  retryTimer = null;
1183
+ // A bearer renewed after the socket's lapsed (4402), presented from the next
1184
+ // connect on in place of the client's own.
1185
+ renewedBearer = null;
1146
1186
  constructor(config) {
1147
1187
  if (config.dispatch !== void 0) {
1148
1188
  throw new FoirRequestError(
@@ -1215,12 +1255,14 @@ var FoirSocketTransport = class {
1215
1255
  socket.onmessage = (event) => this.receive(event.data);
1216
1256
  socket.onerror = () => {
1217
1257
  };
1218
- socket.onclose = () => this.handleClose();
1258
+ socket.onclose = (event) => this.handleClose(event);
1219
1259
  }
1220
1260
  async sendConnectionInit(socket) {
1221
1261
  this.connecting = false;
1222
1262
  try {
1223
- const { headers } = await resolveFoirAuth(this.config);
1263
+ const { headers } = await resolveFoirAuth(
1264
+ this.renewedBearer ? { ...this.config, bearerToken: this.renewedBearer } : this.config
1265
+ );
1224
1266
  const { "Content-Type": _contentType, ...params } = headers;
1225
1267
  const extra = await this.config.connectionParams?.() ?? {};
1226
1268
  this.send(socket, {
@@ -1234,19 +1276,53 @@ var FoirSocketTransport = class {
1234
1276
  this.teardownSocket(true);
1235
1277
  }
1236
1278
  }
1237
- handleClose() {
1279
+ handleClose(event) {
1280
+ const code = event?.code;
1238
1281
  const wasAcknowledged = this.acknowledged;
1239
1282
  this.teardownSocket(false);
1240
1283
  if (this.disposed || this.registrations.size === 0) return;
1241
1284
  for (const registration of this.registrations.values()) {
1242
1285
  registration.sent = false;
1243
1286
  }
1287
+ if (code === CLOSE_FORBIDDEN) {
1288
+ this.failAll(
1289
+ new FoirRequestError(
1290
+ "[foir] the subscription socket was refused where it asked to stand: the signed-in user is no member of the organization this client names"
1291
+ )
1292
+ );
1293
+ return;
1294
+ }
1295
+ if (code === CLOSE_CREDENTIAL_ENDED) {
1296
+ void this.reconnectWithARenewedBearer();
1297
+ return;
1298
+ }
1244
1299
  this.scheduleReconnect(
1245
1300
  new FoirRequestError(
1246
1301
  wasAcknowledged ? "[foir] the subscription socket closed and could not be re-established" : "[foir] the subscription socket closed before the credential was accepted"
1247
1302
  )
1248
1303
  );
1249
1304
  }
1305
+ /**
1306
+ * Reconnect after the socket's credential lapsed: with the bearer renewed
1307
+ * through the client's `refreshBearer` where it has one, failing every
1308
+ * subscription with `FoirSessionExpiredError` when the session is over. A
1309
+ * client without one reconnects at once, presenting what its bearer
1310
+ * provider gives now.
1311
+ */
1312
+ async reconnectWithARenewedBearer() {
1313
+ const refresh = this.config.refreshBearer;
1314
+ if (refresh) {
1315
+ const token = await renewBearerOnce(refresh).catch(() => null);
1316
+ if (this.disposed) return;
1317
+ if (!token) {
1318
+ this.failAll(new FoirSessionExpiredError());
1319
+ return;
1320
+ }
1321
+ this.renewedBearer = token;
1322
+ }
1323
+ this.attempt = 0;
1324
+ this.ensureConnected();
1325
+ }
1250
1326
  scheduleReconnect(error) {
1251
1327
  if (this.disposed || this.registrations.size === 0) return;
1252
1328
  if (this.attempt >= this.policy.maxAttempts) {