@foir/sdk 0.30.0 → 0.31.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
@@ -943,7 +943,7 @@ async function fetchFoirManifest(config, options = {}) {
943
943
 
944
944
  // src/auth/ops.ts
945
945
  var AUTH_OPS = {
946
- // Pre-credential reads a login screen renders itself from.
946
+ // Pre-credential reads a sign-in screen renders itself from.
947
947
  authConfig: "authConfig",
948
948
  authProviders: "authProviders",
949
949
  defaultAuthProvider: "defaultAuthProvider",
@@ -955,17 +955,15 @@ var AUTH_OPS = {
955
955
  // they hold. `./access` reads it.
956
956
  whoami: "whoami",
957
957
  // Methods.
958
- login: "login",
958
+ signIn: "signIn",
959
959
  register: "register",
960
960
  requestOTP: "requestOTP",
961
- loginWithOTP: "loginWithOTP",
962
961
  // Session lifecycle.
963
962
  refreshToken: "refreshToken",
964
- logout: "logout",
963
+ signOut: "signOut",
965
964
  joinWaitlist: "joinWaitlist",
966
965
  // Password management and account lifecycle.
967
966
  requestPasswordReset: "requestPasswordReset",
968
- resetPassword: "resetPassword",
969
967
  updatePassword: "updatePassword",
970
968
  deleteMyAccount: "deleteMyAccount",
971
969
  // Ending their own membership where the request stands.
@@ -979,10 +977,10 @@ var AUTH_OPS = {
979
977
  mySessions: "mySessions",
980
978
  endMySession: "endMySession",
981
979
  signOutEverywhere: "signOutEverywhere",
982
- // Their email addresses: one added is kept once the code it was sent proves it.
980
+ // Their email addresses: one added is kept once the code it was sent comes
981
+ // back on the step addMyAddress answers, on its own route (proveMyAddress).
983
982
  myAddresses: "myAddresses",
984
983
  addMyAddress: "addMyAddress",
985
- proveMyAddress: "proveMyAddress",
986
984
  makeMyAddressPrimary: "makeMyAddressPrimary",
987
985
  removeMyAddress: "removeMyAddress",
988
986
  // A copy of their own data.
@@ -993,10 +991,8 @@ var AUTH_OPS = {
993
991
  disconnectSignInMethod: "disconnectSignInMethod",
994
992
  // Their second factors. A change needs one proven first, by a step-up.
995
993
  myFactors: "myFactors",
996
- beginStepUp: "beginStepUp",
997
- completeStepUp: "completeStepUp",
994
+ stepUp: "stepUp",
998
995
  beginFactorEnrollment: "beginFactorEnrollment",
999
- completeFactorEnrollment: "completeFactorEnrollment",
1000
996
  removeFactor: "removeFactor",
1001
997
  renameFactor: "renameFactor",
1002
998
  makeRecoveryCodes: "makeRecoveryCodes",
@@ -1010,13 +1006,7 @@ var AUTH_OPS = {
1010
1006
  // Founding one. Present in a manifest only when the project opted in.
1011
1007
  registerOrganization: "registerOrganization",
1012
1008
  // External identity providers.
1013
- loginWithProvider: "loginWithProvider",
1014
- providerCallback: "providerCallback",
1015
- providerVerifyOTP: "providerVerifyOTP",
1016
- // A provider sign-in that names an address somebody already holds, waiting
1017
- // for the code sent to it.
1018
- confirmProviderLink: "confirmProviderLink",
1019
- resendProviderLinkCode: "resendProviderLinkCode"
1009
+ signInWithProvider: "signInWithProvider"
1020
1010
  };
1021
1011
  function authClientConfig(env, options) {
1022
1012
  const apiKey = resolveFoirApiKey(env);
@@ -1076,35 +1066,101 @@ function stripUndefined(args) {
1076
1066
  return out;
1077
1067
  }
1078
1068
 
1069
+ // src/auth/steps.ts
1070
+ async function postSignInStep(env, path, body, stepToken) {
1071
+ const apiUrl = env.PLATFORM_API_URL;
1072
+ if (!apiUrl) {
1073
+ throw new FoirRequestError("[foir] no api url configured: set PLATFORM_API_URL");
1074
+ }
1075
+ const headers = { "Content-Type": "application/json" };
1076
+ if (stepToken) headers["X-Foir-Token"] = stepToken;
1077
+ const res = await fetch(foirApiEndpoint(apiUrl, path), {
1078
+ method: "POST",
1079
+ headers,
1080
+ body: JSON.stringify(body)
1081
+ });
1082
+ const data = await res.json().catch(() => ({}));
1083
+ if (!res.ok) {
1084
+ const message = typeof data.error === "string" ? data.error : `[foir] POST ${path} failed: HTTP ${res.status}`;
1085
+ const code = typeof data.code === "string" ? data.code : void 0;
1086
+ const extensions = {};
1087
+ if (code) extensions.code = code;
1088
+ if (data.signIn && typeof data.signIn === "object") extensions.signIn = data.signIn;
1089
+ if (data.providerSignIn && typeof data.providerSignIn === "object") extensions.providerSignIn = data.providerSignIn;
1090
+ throw new FoirRequestError(message, {
1091
+ status: res.status,
1092
+ graphQLErrors: Object.keys(extensions).length > 0 ? [{ message, extensions }] : void 0
1093
+ });
1094
+ }
1095
+ return data;
1096
+ }
1097
+
1079
1098
  // src/auth/auth.ts
1080
1099
  var TOKEN_KEY = "foir:token";
1081
1100
  var REFRESH_KEY = "foir:refreshToken";
1082
1101
  var USER_ID_KEY = "foir:userId";
1083
1102
  var EMAIL_KEY = "foir:email";
1084
- async function login(env, vars) {
1085
- return authOp(env, AUTH_OPS.login, vars);
1086
- }
1087
- async function register(env, vars) {
1088
- return authOp(env, AUTH_OPS.register, vars);
1089
- }
1090
1103
  async function requestOTP(env, email) {
1091
1104
  return authOp(env, AUTH_OPS.requestOTP, { email });
1092
1105
  }
1093
- async function loginWithOTP(env, vars) {
1094
- return authOp(env, AUTH_OPS.loginWithOTP, vars);
1106
+ async function codeSignIn(env, vars) {
1107
+ const signedIn = await postSignInStep(env, `/v1/sign-ins/${encodeURIComponent(vars.step.id)}/code`, { code: vars.otp }, vars.step.token);
1108
+ return {
1109
+ token: signedIn.accessToken ?? "",
1110
+ refreshToken: signedIn.refreshToken ?? "",
1111
+ userId: signedIn.user?.id ?? "",
1112
+ email: signedIn.user?.email ?? ""
1113
+ };
1095
1114
  }
1096
- async function refreshTokens(env, refreshToken) {
1115
+ async function rotateTokens(env, refreshToken) {
1097
1116
  return authOp(env, AUTH_OPS.refreshToken, { refreshToken });
1098
1117
  }
1099
- async function loginWithProvider(env, input) {
1100
- return authOp(env, AUTH_OPS.loginWithProvider, { input });
1118
+ async function signInWithProvider(adapter, env, input) {
1119
+ const started = await authOp(
1120
+ env,
1121
+ AUTH_OPS.signInWithProvider,
1122
+ { input }
1123
+ );
1124
+ if (started.step) await adapter.set(PROVIDER_STEP_KEY, JSON.stringify(started.step));
1125
+ else await adapter.unset(PROVIDER_STEP_KEY);
1126
+ return { redirectUrl: started.redirectUrl ?? null };
1127
+ }
1128
+ async function finishProviderSignIn(adapter, env, input) {
1129
+ const step = await keptProviderStep(adapter, "no provider sign-in was started here: start one before finishing it");
1130
+ return signedInOnStep(adapter, env, step, "provider", {
1131
+ providerKey: input.providerKey,
1132
+ code: input.code,
1133
+ state: input.state
1134
+ });
1101
1135
  }
1102
- async function providerCallback(env, input) {
1103
- return authOp(env, AUTH_OPS.providerCallback, { input });
1136
+ var PROVIDER_STEP_KEY = "foir:providerStep";
1137
+ async function keptProviderStep(adapter, missing) {
1138
+ const raw = await adapter.get(PROVIDER_STEP_KEY);
1139
+ if (raw) {
1140
+ try {
1141
+ const step = JSON.parse(raw);
1142
+ if (typeof step.id === "string" && typeof step.token === "string") return step;
1143
+ } catch {
1144
+ }
1145
+ }
1146
+ throw new Error(`[foir] ${missing}`);
1147
+ }
1148
+ async function signedInOnStep(adapter, env, step, route, body) {
1149
+ const signedIn = await postSignInStep(env, `/v1/sign-ins/${encodeURIComponent(step.id)}/${route}`, body, step.token);
1150
+ await adapter.unset(PROVIDER_STEP_KEY);
1151
+ return saveSession(adapter, {
1152
+ token: signedIn.accessToken ?? "",
1153
+ refreshToken: signedIn.refreshToken ?? "",
1154
+ userId: signedIn.userId ?? "",
1155
+ email: signedIn.email ?? ""
1156
+ });
1104
1157
  }
1105
1158
  async function getAuthProviders(env) {
1106
1159
  return authOp(env, AUTH_OPS.authProviders);
1107
1160
  }
1161
+ async function endSignIn(env, options) {
1162
+ return authOp(env, AUTH_OPS.signOut, void 0, options);
1163
+ }
1108
1164
  async function saveSession(adapter, result) {
1109
1165
  await adapter.set(TOKEN_KEY, result.token);
1110
1166
  await adapter.set(REFRESH_KEY, result.refreshToken);
@@ -1131,19 +1187,29 @@ async function getSessionUser(adapter) {
1131
1187
  return { id, email: email ?? "" };
1132
1188
  }
1133
1189
  async function signInWithPassword(adapter, env, vars) {
1134
- return saveSession(adapter, await login(env, vars));
1190
+ return saveSession(adapter, await authOp(env, AUTH_OPS.signIn, vars));
1135
1191
  }
1136
1192
  async function signUpWithPassword(env, vars) {
1137
- await register(env, vars);
1193
+ return authOp(env, AUTH_OPS.register, vars);
1138
1194
  }
1139
1195
  async function signInWithOTP(adapter, env, vars) {
1140
- return saveSession(adapter, await loginWithOTP(env, vars));
1196
+ return saveSession(adapter, await codeSignIn(env, vars));
1197
+ }
1198
+ async function signOut(adapter, env) {
1199
+ const token = await getSessionToken(adapter);
1200
+ if (token) {
1201
+ try {
1202
+ await endSignIn(env, { bearerToken: token });
1203
+ } catch {
1204
+ }
1205
+ }
1206
+ await clearSession(adapter);
1141
1207
  }
1142
1208
  async function refreshSession(adapter, env) {
1143
1209
  const refreshToken = await adapter.get(REFRESH_KEY) ?? null;
1144
1210
  if (!refreshToken) return null;
1145
1211
  try {
1146
- const result = await refreshTokens(env, refreshToken);
1212
+ const result = await rotateTokens(env, refreshToken);
1147
1213
  await saveSession(adapter, result);
1148
1214
  return { token: result.token, refreshToken: result.refreshToken };
1149
1215
  } catch {
@@ -1447,14 +1513,14 @@ function hasScope(access, scope) {
1447
1513
 
1448
1514
  // src/auth/session-client.ts
1449
1515
  var DEFAULT_SESSION_PATH = "/session/token";
1450
- var DEFAULT_LOGOUT_PATH = "/session/logout";
1516
+ var DEFAULT_SIGN_OUT_PATH = "/session/logout";
1451
1517
  var DEFAULT_REFRESH_SKEW_SECONDS = 60;
1452
1518
  var MIN_REFRESH_DELAY_MS = 5e3;
1453
1519
  var CSRF_HEADER = { "X-Foir-Session": "1" };
1454
1520
  function createFoirSessionClient(config) {
1455
1521
  const forApp = config.clientId ? `?client_id=${encodeURIComponent(config.clientId)}` : "";
1456
1522
  const sessionUrl = `${trimSlash(config.loginOrigin)}${config.sessionPath ?? DEFAULT_SESSION_PATH}${forApp}`;
1457
- const logoutUrl = `${trimSlash(config.loginOrigin)}${config.logoutPath ?? DEFAULT_LOGOUT_PATH}${forApp}`;
1523
+ const signOutUrl = `${trimSlash(config.loginOrigin)}${config.signOutPath ?? DEFAULT_SIGN_OUT_PATH}${forApp}`;
1458
1524
  const skewMs = (config.refreshSkewSeconds ?? DEFAULT_REFRESH_SKEW_SECONDS) * 1e3;
1459
1525
  let cached = null;
1460
1526
  let inflight = null;
@@ -1550,7 +1616,7 @@ function createFoirSessionClient(config) {
1550
1616
  },
1551
1617
  async signOut() {
1552
1618
  try {
1553
- await fetch(logoutUrl, {
1619
+ await fetch(signOutUrl, {
1554
1620
  method: "POST",
1555
1621
  credentials: "include",
1556
1622
  headers: { ...CSRF_HEADER }
@@ -1635,18 +1701,18 @@ function FoirProvider({
1635
1701
  }, []);
1636
1702
  const loginOrigin = hostedSession?.loginOrigin;
1637
1703
  const sessionPath = hostedSession?.sessionPath;
1638
- const logoutPath = hostedSession?.logoutPath;
1704
+ const signOutPath = hostedSession?.signOutPath;
1639
1705
  const refreshSkewSeconds = hostedSession?.refreshSkewSeconds;
1640
1706
  const hosted = useMemo(() => {
1641
1707
  if (!loginOrigin) return null;
1642
1708
  return createFoirSessionClient({
1643
1709
  loginOrigin,
1644
1710
  sessionPath,
1645
- logoutPath,
1711
+ signOutPath,
1646
1712
  refreshSkewSeconds,
1647
1713
  onSignOut: () => apply(null)
1648
1714
  });
1649
- }, [loginOrigin, sessionPath, logoutPath, refreshSkewSeconds, apply]);
1715
+ }, [loginOrigin, sessionPath, signOutPath, refreshSkewSeconds, apply]);
1650
1716
  useEffect(() => {
1651
1717
  let cancelled = false;
1652
1718
  void (async () => {
@@ -1685,33 +1751,41 @@ function FoirProvider({
1685
1751
  },
1686
1752
  [apply, assertTokenMode]
1687
1753
  );
1688
- const register2 = useCallback(
1689
- (vars) => {
1690
- assertTokenMode("register");
1691
- return signUpWithPassword(envRef.current, vars);
1754
+ const stepsRef = useRef(/* @__PURE__ */ new Map());
1755
+ const stepKey = (email) => email.trim().toLowerCase();
1756
+ const signUp = useCallback(
1757
+ async (vars) => {
1758
+ assertTokenMode("signUp");
1759
+ stepsRef.current.set(stepKey(vars.email), await signUpWithPassword(envRef.current, vars));
1692
1760
  },
1693
1761
  [assertTokenMode]
1694
1762
  );
1695
1763
  const requestOtp = useCallback(
1696
- (email) => {
1764
+ async (email) => {
1697
1765
  assertTokenMode("requestOtp");
1698
- return requestOTP(envRef.current, email);
1766
+ stepsRef.current.set(stepKey(email), await requestOTP(envRef.current, email));
1767
+ return true;
1699
1768
  },
1700
1769
  [assertTokenMode]
1701
1770
  );
1702
1771
  const signInWithOtp = useCallback(
1703
1772
  async (vars) => {
1704
1773
  assertTokenMode("signInWithOtp");
1705
- const c = await signInWithOTP(adapterRef.current, envRef.current, vars);
1774
+ const step = stepsRef.current.get(stepKey(vars.email));
1775
+ if (!step) {
1776
+ throw new Error("[foir] no code was sent to this address here: call requestOtp or signUp first");
1777
+ }
1778
+ const c = await signInWithOTP(adapterRef.current, envRef.current, { step, otp: vars.otp });
1779
+ stepsRef.current.delete(stepKey(vars.email));
1706
1780
  apply(c);
1707
1781
  return c;
1708
1782
  },
1709
1783
  [apply, assertTokenMode]
1710
1784
  );
1711
- const signInWithProvider = useCallback(
1785
+ const signInWithProvider2 = useCallback(
1712
1786
  async (providerKey, opts) => {
1713
1787
  assertTokenMode("signInWithProvider");
1714
- const res = await loginWithProvider(envRef.current, {
1788
+ const res = await signInWithProvider(adapterRef.current, envRef.current, {
1715
1789
  providerKey,
1716
1790
  ...opts
1717
1791
  });
@@ -1721,11 +1795,10 @@ function FoirProvider({
1721
1795
  },
1722
1796
  [assertTokenMode]
1723
1797
  );
1724
- const completeProviderLogin = useCallback(
1798
+ const finishProviderSignIn2 = useCallback(
1725
1799
  async (input) => {
1726
- assertTokenMode("completeProviderLogin");
1727
- const result = await providerCallback(envRef.current, input);
1728
- const c = await saveSession(adapterRef.current, result);
1800
+ assertTokenMode("finishProviderSignIn");
1801
+ const c = await finishProviderSignIn(adapterRef.current, envRef.current, input);
1729
1802
  apply(c);
1730
1803
  return c;
1731
1804
  },
@@ -1736,7 +1809,7 @@ function FoirProvider({
1736
1809
  await hosted.signOut();
1737
1810
  return;
1738
1811
  }
1739
- await clearSession(adapterRef.current);
1812
+ await signOut(adapterRef.current, envRef.current);
1740
1813
  apply(null);
1741
1814
  }, [apply, hosted]);
1742
1815
  const refresh = useCallback(async () => {
@@ -1788,7 +1861,7 @@ function FoirProvider({
1788
1861
  return ok;
1789
1862
  }, [refresh, loadAccess, accessWasRead]);
1790
1863
  const getProviders = useCallback(() => getAuthProviders(envRef.current), []);
1791
- const hostedSignInUrl = useMemo(
1864
+ const hostedSignInUrl2 = useMemo(
1792
1865
  () => hosted ? (returnTo, options) => hosted.signInUrl(returnTo, options) : null,
1793
1866
  [hosted]
1794
1867
  );
@@ -1803,16 +1876,16 @@ function FoirProvider({
1803
1876
  env,
1804
1877
  adapter: adapterRef.current,
1805
1878
  signIn,
1806
- register: register2,
1879
+ signUp,
1807
1880
  requestOtp,
1808
1881
  signInWithOtp,
1809
- signInWithProvider,
1810
- completeProviderLogin,
1882
+ signInWithProvider: signInWithProvider2,
1883
+ finishProviderSignIn: finishProviderSignIn2,
1811
1884
  signOut: signOut2,
1812
1885
  refresh: refreshWithAccess,
1813
1886
  getToken,
1814
1887
  getProviders,
1815
- hostedSignInUrl
1888
+ hostedSignInUrl: hostedSignInUrl2
1816
1889
  }),
1817
1890
  [
1818
1891
  status,
@@ -1823,16 +1896,16 @@ function FoirProvider({
1823
1896
  ensureAccess,
1824
1897
  env,
1825
1898
  signIn,
1826
- register2,
1899
+ signUp,
1827
1900
  requestOtp,
1828
1901
  signInWithOtp,
1829
- signInWithProvider,
1830
- completeProviderLogin,
1902
+ signInWithProvider2,
1903
+ finishProviderSignIn2,
1831
1904
  signOut2,
1832
1905
  refreshWithAccess,
1833
1906
  getToken,
1834
1907
  getProviders,
1835
- hostedSignInUrl
1908
+ hostedSignInUrl2
1836
1909
  ]
1837
1910
  );
1838
1911
  return /* @__PURE__ */ jsx(FoirContext.Provider, { value, children });
@@ -1877,29 +1950,29 @@ function useFoirAccess() {
1877
1950
  function useFoirAuth() {
1878
1951
  const {
1879
1952
  signIn,
1880
- register: register2,
1953
+ signUp,
1881
1954
  requestOtp,
1882
1955
  signInWithOtp,
1883
- signInWithProvider,
1884
- completeProviderLogin,
1956
+ signInWithProvider: signInWithProvider2,
1957
+ finishProviderSignIn: finishProviderSignIn2,
1885
1958
  signOut: signOut2,
1886
1959
  refresh,
1887
1960
  getToken,
1888
1961
  getProviders,
1889
- hostedSignInUrl
1962
+ hostedSignInUrl: hostedSignInUrl2
1890
1963
  } = useFoirContext();
1891
1964
  return {
1892
1965
  signIn,
1893
- register: register2,
1966
+ signUp,
1894
1967
  requestOtp,
1895
1968
  signInWithOtp,
1896
- signInWithProvider,
1897
- completeProviderLogin,
1969
+ signInWithProvider: signInWithProvider2,
1970
+ finishProviderSignIn: finishProviderSignIn2,
1898
1971
  signOut: signOut2,
1899
1972
  refresh,
1900
1973
  getToken,
1901
1974
  getProviders,
1902
- hostedSignInUrl
1975
+ hostedSignInUrl: hostedSignInUrl2
1903
1976
  };
1904
1977
  }
1905
1978
 
@@ -1927,7 +2000,7 @@ function OAuthButtons({
1927
2000
  className,
1928
2001
  onError
1929
2002
  }) {
1930
- const { getProviders, signInWithProvider } = useFoirAuth();
2003
+ const { getProviders, signInWithProvider: signInWithProvider2 } = useFoirAuth();
1931
2004
  const [providers, setProviders] = useState2([]);
1932
2005
  useEffect2(() => {
1933
2006
  let cancelled = false;
@@ -1943,7 +2016,7 @@ function OAuthButtons({
1943
2016
  );
1944
2017
  if (shown.length === 0) return null;
1945
2018
  const start = (p) => {
1946
- void signInWithProvider(p.key, { returnTo, redirectUri }).catch(
2019
+ void signInWithProvider2(p.key, { returnTo, redirectUri }).catch(
1947
2020
  (err) => onError?.(err)
1948
2021
  );
1949
2022
  };
@@ -1996,7 +2069,7 @@ function SignIn({
1996
2069
  const [error, setError] = useState3(null);
1997
2070
  const styled = !className;
1998
2071
  if (hostedUrl) {
1999
- return /* @__PURE__ */ jsx3("div", { className, "data-foir-signin": true, children: /* @__PURE__ */ jsx3("a", { href: hostedUrl, "data-foir-hosted-login": true, style: styled ? submitStyle : void 0, children: "Sign in" }) });
2072
+ return /* @__PURE__ */ jsx3("div", { className, "data-foir-signin": true, children: /* @__PURE__ */ jsx3("a", { href: hostedUrl, "data-foir-hosted-sign-in": true, style: styled ? submitStyle : void 0, children: "Sign in" }) });
2000
2073
  }
2001
2074
  const finish = (user) => {
2002
2075
  onSignIn?.(user);
@@ -2306,7 +2379,6 @@ function noteArgs(subject, body, anchorField) {
2306
2379
  const args = {
2307
2380
  subjectType: "record",
2308
2381
  subjectId: subject.recordId,
2309
- subjectModelKey: subject.modelKey,
2310
2382
  body
2311
2383
  };
2312
2384
  if (anchorField) args.anchorField = anchorField;
@@ -2320,7 +2392,7 @@ async function uploadFeedbackAttachment(client, screenshot, fetchImpl = globalTh
2320
2392
  );
2321
2393
  }
2322
2394
  const ticket = readUploadTicket(
2323
- await files.createFileUpload({
2395
+ await files.uploadFile({
2324
2396
  filename: screenshotFilename(screenshot.mimeType),
2325
2397
  mimeType: screenshot.mimeType,
2326
2398
  // The blob's own size, never a number computed alongside it: this is what
@@ -2339,24 +2411,35 @@ async function uploadFeedbackAttachment(client, screenshot, fetchImpl = globalTh
2339
2411
  `[foir] uploading the screenshot failed: HTTP ${res.status} ${res.statusText}. The note was not written.`
2340
2412
  );
2341
2413
  }
2342
- const confirmed = await files.confirmFileUpload({ uploadId: ticket.uploadId });
2343
- const id = confirmed?.id;
2414
+ const landed = await fetchImpl(ticket.landedEndpoint, {
2415
+ method: "POST",
2416
+ headers: { "X-Foir-Token": ticket.landedToken }
2417
+ });
2418
+ if (!landed.ok) {
2419
+ throw new Error(
2420
+ `[foir] the screenshot's upload was refused when it landed: HTTP ${landed.status} ${landed.statusText}. The note was not written.`
2421
+ );
2422
+ }
2423
+ const settled = await landed.json().catch(() => null);
2424
+ const id = settled?.id;
2344
2425
  if (typeof id !== "string" || id === "") {
2345
2426
  throw new Error(
2346
- "[foir] confirmFileUpload answered without a file id, so there is nothing to attach the note to. The note was not written."
2427
+ "[foir] the upload landed and was answered without a file id, so there is nothing to attach the note to. The note was not written."
2347
2428
  );
2348
2429
  }
2349
2430
  return id;
2350
2431
  }
2351
2432
  function readUploadTicket(result) {
2352
- const uploadId = result?.uploadId;
2353
- const uploadUrl = result?.uploadUrl;
2354
- if (typeof uploadId !== "string" || typeof uploadUrl !== "string" || !uploadId || !uploadUrl) {
2433
+ const r = result;
2434
+ const uploadUrl = r?.uploadUrl;
2435
+ const landedEndpoint = r?.landed?.endpoint;
2436
+ const landedToken = r?.landed?.token;
2437
+ if (typeof uploadUrl !== "string" || !uploadUrl || typeof landedEndpoint !== "string" || !landedEndpoint || typeof landedToken !== "string" || !landedToken) {
2355
2438
  throw new Error(
2356
- "[foir] createFileUpload answered without an uploadId and uploadUrl, so the screenshot has nowhere to go. The note was not written."
2439
+ "[foir] uploadFile answered without an uploadUrl and a way to say the bytes landed, so the screenshot has nowhere to go. The note was not written."
2357
2440
  );
2358
2441
  }
2359
- return { uploadId, uploadUrl };
2442
+ return { uploadUrl, landedEndpoint, landedToken };
2360
2443
  }
2361
2444
  function readCreatedNote(result) {
2362
2445
  const note = result;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * A step's reply to its run.
3
+ *
4
+ * A step that answers later replies with the token its run handed it alone,
5
+ * signed with the key that came with it, at the address that came with it:
6
+ * an operation's endpoint, a local development session forwarding a
7
+ * dispatch, or the page of a run's own caller answering a call to one of its
8
+ * tools. Nobody else holds such a token, so a reply reaches the one step it
9
+ * was minted for.
10
+ *
11
+ * Each reply is POSTed to the step's own reply route as the contract's reply
12
+ * itself (`{"result": ...}` and the rest), and carries:
13
+ * - `Authorization: Bearer <reply.token>`
14
+ * - `X-Foir-Signature: sha256=<hex>`, the HMAC-SHA256 of the raw request
15
+ * body under `reply.signingKey`.
16
+ *
17
+ * A reply that gets no answer, or a 5xx or a 429, is tried again: every reply
18
+ * is safe to repeat, and a step whose reply is lost waits until its time is up.
19
+ */
20
+ import { type FoirRetryPolicy } from '../transport/retry';
21
+ /** How a step replies to its run, as the platform hands it to whoever runs the step. */
22
+ export interface StepReply {
23
+ token: string;
24
+ signingKey: string;
25
+ /** The step's own reply route at the data API. */
26
+ endpoint: string;
27
+ /** ISO-8601. After it the token is refused. */
28
+ expiresAt?: string | null;
29
+ }
30
+ /** A step's reply, in the runs contract's own shape: exactly one of these. */
31
+ export type StepReplyBody = {
32
+ result: unknown;
33
+ } | {
34
+ progress: {
35
+ percent?: number;
36
+ message?: string;
37
+ metadata?: Record<string, unknown>;
38
+ };
39
+ } | {
40
+ failure: {
41
+ code: string;
42
+ message: string;
43
+ retryable?: boolean;
44
+ details?: Record<string, unknown>;
45
+ };
46
+ } | {
47
+ continuation: Record<string, unknown>;
48
+ }
49
+ /** The step stops its own run. */
50
+ | {
51
+ cancel: Record<string, never>;
52
+ };
53
+ /** What the platform made of a reply. */
54
+ export interface StepReplyResult {
55
+ /** The step's status after the reply: running, held, completed, failed, cancelled, timed_out or pending. */
56
+ status: string;
57
+ /** True when the step had been cancelled, so the reply was not taken. */
58
+ cancelled: boolean;
59
+ /** False when the reply changed nothing: the step had ended, or progress came out of order. */
60
+ applied: boolean;
61
+ }
62
+ /** Reply to a run from one of its steps, with the reply its run handed it. */
63
+ export declare function replyToStep(reply: StepReply, body: StepReplyBody, opts?: {
64
+ fetch?: typeof fetch;
65
+ retry?: FoirRetryPolicy;
66
+ signal?: AbortSignal;
67
+ }): Promise<StepReplyResult>;
68
+ /**
69
+ * Send one reply to a step's reply route, signed with `signingKey`. Shared by
70
+ * `replyToStep` and the async endpoint's callback client.
71
+ */
72
+ export declare function sendReply(endpoint: string, token: string, signingKey: string, body: StepReplyBody, opts?: {
73
+ fetch?: typeof fetch;
74
+ retry?: FoirRetryPolicy;
75
+ signal?: AbortSignal;
76
+ }): Promise<StepReplyResult>;
77
+ /**
78
+ * The lowercase hex HMAC-SHA256 of a body, through Web Crypto, so it runs in
79
+ * a browser, a worker and Node alike.
80
+ */
81
+ export declare function hmacSha256Hex(secret: string, body: string): Promise<string>;
@@ -4,7 +4,7 @@
4
4
  * The platform dispatches with `mode='async'` and includes a `callback`
5
5
  * block in the operation payload. The extension acks 202, kicks the work
6
6
  * off in its own durable runtime (CF Queue, BullMQ, etc.), and later
7
- * calls back with one of:
7
+ * replies at the run's own reply route (`callback.endpoint`) with one of:
8
8
  *
9
9
  * - `complete(result)` — operation succeeded; result conforms to the
10
10
  * operation's `output_schema` (per-op typed via codegen).
@@ -27,18 +27,15 @@
27
27
  * with a 5xx or a 429, is tried again: a report that is lost leaves the run to
28
28
  * be closed by the platform's watchdog as timed out, and every one of these
29
29
  * calls is safe to repeat.
30
- *
31
- * Both response body shape and content-type follow the GraphQL spec —
32
- * extensions importing this client receive typed result data.
33
30
  */
34
- import { type FoirRetryPolicy } from '../transport/retry';
31
+ import type { FoirRetryPolicy } from '../transport/retry';
35
32
  import type { OperationCallback } from './operation-types.js';
36
- /** Shape of a successful response from any callback mutation. */
33
+ /** What the platform made of a reply. */
37
34
  export interface CallbackResult {
38
35
  /**
39
- * Status as seen by the platform AFTER the call. One of:
40
- * pending|dispatched|running|completed|failed|cancelled|timed_out|progress_stale.
41
- * Always set even on idempotent / cancelled calls.
36
+ * The run's status as the platform sees it AFTER the call: pending,
37
+ * running, completed, failed, cancelled or timed_out. Always set, even on
38
+ * a repeated or a cancelled call.
42
39
  */
43
40
  status: string;
44
41
  /**
@@ -48,16 +45,14 @@ export interface CallbackResult {
48
45
  */
49
46
  cancelled: boolean;
50
47
  /**
51
- * Progress mutations only: false when the monotonic guard rejected an
52
- * out-of-order update. Extensions can use this to detect packet
53
- * reordering without erroring; missing on complete/fail/cancel.
48
+ * False when the reply changed nothing: the run had already ended, or a
49
+ * progress report came out of order (the monotonic guard dropped it).
54
50
  */
55
51
  applied?: boolean;
56
52
  }
57
53
  /**
58
54
  * Per-operation callback client. `TResult` is the operation's typed result
59
- * shape — supply the type generated by graphql-codegen against the
60
- * api-public schema (`Complete{Op}ExecutionMutationVariables['result']`).
55
+ * shape, the operation's output type.
61
56
  */
62
57
  export interface CallbackClient<TResult = unknown> {
63
58
  complete(result: TResult): Promise<CallbackResult>;
@@ -97,9 +92,9 @@ export interface CallbackClient<TResult = unknown> {
97
92
  * told otherwise; `{ attempts: 1 }` turns it off).
98
93
  *
99
94
  * `apiUrl` is the Foir API origin the endpoint is configured with. The
100
- * dispatch names the address to report to and that address is the one used; a
95
+ * dispatch names the address to reply at and that address is the one used; a
101
96
  * platform with no public address of its own configured names none, and the
102
- * run then reports to the data API at `apiUrl`.
97
+ * run then replies at its route under `apiUrl`.
103
98
  */
104
99
  export declare function createCallbackClient<TResult = unknown>(callback: OperationCallback, opts: {
105
100
  executionId: string;
@@ -9,6 +9,8 @@ export type { OperationPayload, OperationTrigger, OperationRecordContext, Operat
9
9
  export { continueRun, CONTINUATION_MEDIA_TYPE, MAX_CONTINUATION_CURSOR_BYTES, MAX_CONTINUATION_DELAY_MS, type Continuation, } from './continue-run';
10
10
  export { createCallbackClient } from './callback-client';
11
11
  export type { CallbackClient, CallbackResult } from './callback-client';
12
+ export { replyToStep } from '../runs/step-reply';
13
+ export type { StepReply, StepReplyBody, StepReplyResult } from '../runs/step-reply';
12
14
  export { createEventsClient, FoirEventCursorExpiredError, isFoirEventCursorExpired, FOIR_EVENTS_PATH, FOIR_EVENTS_SCOPE, FOIR_EVENTS_RETENTION_DAYS, } from './events-client';
13
15
  export type { FoirEvent, FoirEventActor, FoirEventPage, FoirEventRetention, FoirEventsClient, FoirEventsClientConfig, FoirEventsListParams, FoirEventsStreamParams, FoirEventsSubscribeParams, } from './events-client';
14
16
  export type { FoirSubscription, FoirSubscriptionHandlers } from '../data/ws-transport';