@oddington/sdk 0.1.8 → 0.1.9

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
@@ -54,9 +54,10 @@ an OS-launched IPv4 loopback origin. It checks `/_os/app/context` on that exact
54
54
  origin for every protected operation, without reading hosted access credentials.
55
55
  It accepts no workspace, endpoint or token option. Missing, forged, expired or
56
56
  changed local context fails closed, as do sign-out and initialization replacement.
57
- Local Data and Files currently return `context_unavailable` and
58
- `unsupported_context` respectively after checking context; they do not return
59
- empty data, upload locally or fall back to hosted services. Local app routine
57
+ Local Data selects from the Host's exact published index; a warm selection
58
+ neither initiates nor waits for renewal or synchronous hosted authority checks.
59
+ Local Files returns `unsupported_context` after checking context. Neither path
60
+ falls back to hosted services. Local app routine
60
61
  execution uses the installed routine's immediate-publication authority.
61
62
 
62
63
  Apps can assess a declared routine without starting it:
@@ -104,14 +105,26 @@ Only event reads reconnect, with bounded backoff and duplicate suppression;
104
105
  launch and cancellation requests are never automatically retried. Failed
105
106
  cancellation requests do not acknowledge cancellation. Handles expose no IDs,
106
107
  cursors, raw output or process controls. Local sign-out, expiry, context
107
- replacement and origin/credential changes invalidate pending work.
108
+ replacement, origin changes and unconfirmed credential changes invalidate pending work.
108
109
 
109
110
  The exact-origin browser credential survives reload and tab closure in local
110
- storage. Host expiry and current membership remain authoritative; stored state
111
- cannot revive a stopped host. Recovery covers only the operation's current
112
- session and bounded terminal grace, without relaunching or durable history.
111
+ storage. Each active initialization proactively renews before expiry, normally
112
+ sixty seconds early with a shorter lead for shorter lifetimes. Only the exact
113
+ Local Host silently refreshes the secure CLI login and confirms unchanged
114
+ complete access. Successful renewal rotates only the bounded browser credential;
115
+ it exposes no CLI credential or login prompt. Other tabs and SDK instances
116
+ retain only their unchanged deadline during a temporary renewal outage and
117
+ retry proactively without starting or delaying Data reads. They
118
+ confirm the stored winner against the same Host and logical session before
119
+ silently adopting it. Rotation preserves admitted Data/results and owned runs;
120
+ old credentials cannot admit new requests and failed writes, launches and
121
+ cancellations are never replayed. Sign-out, replacement and expiry cancel
122
+ renewal, including pending response bodies. Host expiry and current membership
123
+ remain authoritative; stored state cannot revive a stopped host. Recovery
124
+ covers only the operation's current logical session and bounded terminal grace,
125
+ without relaunching or durable history.
113
126
  Supplied File inputs currently reject `execution_unavailable` before launch;
114
- File-use transfer and local Data/Files adapters are separate work.
127
+ File-use transfer and a local browser Files adapter are separate work.
115
128
 
116
129
  The fixed child imports `appRun` from the same package and shares the app's
117
130
  customer-owned input/result/step types:
package/dist/browser.js CHANGED
@@ -1500,7 +1500,9 @@ var files = {
1500
1500
  read
1501
1501
  };
1502
1502
 
1503
- var sessionKey = "_os.app.session", AppResponseReadError = class extends Error {
1503
+ var sessionKey = "_os.app.session", responseChecks = /* @__PURE__ */ new WeakMap(), AppResponseReadError = class extends Error {
1504
+ }, AppContextDenied = class extends Error {
1505
+ }, AppRenewalUnavailable = class extends Error {
1504
1506
  };
1505
1507
  async function appResponse(response, maximum, signal) {
1506
1508
  if (signal.aborted || !response.body || response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() !== "application/json") throw new Error();
@@ -1519,7 +1521,11 @@ async function appResponse(response, maximum, signal) {
1519
1521
  throw new AppResponseReadError();
1520
1522
  }
1521
1523
  if (signal.aborted) throw new Error();
1522
- if (chunk.done) return JSON.parse(text3 + decoder.decode());
1524
+ if (chunk.done) {
1525
+ let value = JSON.parse(text3 + decoder.decode());
1526
+ if (await responseChecks.get(response)?.(value), signal.aborted) throw new Error();
1527
+ return value;
1528
+ }
1523
1529
  if (bytes += chunk.value.byteLength, bytes > maximum)
1524
1530
  throw abort(), new Error();
1525
1531
  text3 += decoder.decode(chunk.value, { stream: !0 });
@@ -1537,27 +1543,180 @@ function isAppOrigin(value) {
1537
1543
  }
1538
1544
  }
1539
1545
  function appContext(origin, active2) {
1540
- let binding, credential, denied = !1, controller = new AbortController(), expiry, invalidate = () => {
1541
- denied = !0, controller.abort(), clearTimeout(expiry), clearInterval(monitor);
1546
+ let binding, credential, denied = !1, controller = new AbortController(), expiry, startup, renewalTimer, renewal, observation, resolution2, disposed = !1, stored = () => globalThis.localStorage.getItem(sessionKey), alive = () => {
1547
+ try {
1548
+ return !denied && !disposed && active2() && globalThis.location.origin === origin && (!binding || binding.expiresAt > Date.now());
1549
+ } catch {
1550
+ return !1;
1551
+ }
1552
+ }, cancel = () => {
1553
+ controller.abort(), clearTimeout(expiry), clearTimeout(startup), clearTimeout(renewalTimer), clearInterval(monitor), globalThis.removeEventListener?.("storage", storageChanged);
1554
+ }, invalidate = () => {
1555
+ denied = !0, cancel();
1542
1556
  try {
1543
- active2() && globalThis.location.origin === origin && (!credential || globalThis.localStorage.getItem(sessionKey) === credential) && globalThis.localStorage.removeItem(sessionKey);
1557
+ !disposed && active2() && globalThis.location.origin === origin && (!credential || globalThis.localStorage.getItem(sessionKey) === credential) && globalThis.localStorage.removeItem(sessionKey);
1544
1558
  } catch {
1545
1559
  }
1546
1560
  }, current2 = () => {
1547
1561
  try {
1548
- return !denied && active2() && globalThis.location.origin === origin && globalThis.localStorage.getItem(sessionKey) === credential && (!binding || binding.expiresAt > Date.now());
1562
+ return alive() && stored() === credential;
1549
1563
  } catch {
1550
1564
  return !1;
1551
1565
  }
1566
+ }, sameSession = (value) => !binding || value.appId === binding.appId && value.memberId === binding.memberId && value.workspace === binding.workspace && value.sessionId === binding.sessionId, bounded = (value) => validBinding(value) && value.expiresAt > Date.now() && value.expiresAt <= Date.now() + 6e5 && sameSession(value);
1567
+ async function proof(candidate, signal = controller.signal) {
1568
+ if (!alive() || !/^[A-Za-z0-9_-]{43}$/.test(candidate)) throw new Error();
1569
+ let url = `${origin}/_os/app/context`, response = await fetch(url, {
1570
+ credentials: "omit",
1571
+ redirect: "error",
1572
+ cache: "no-store",
1573
+ referrerPolicy: "no-referrer",
1574
+ headers: { authorization: `Bearer ${candidate}` },
1575
+ signal
1576
+ });
1577
+ if (response.redirected || response.url && response.url !== url) throw new Error();
1578
+ if (response.status === 401 || response.status === 403) throw new AppContextDenied();
1579
+ if (!response.ok) throw new Error();
1580
+ let value = await appResponse(response, 4096, signal);
1581
+ if (!alive() || !bounded(value)) throw new Error();
1582
+ return value;
1583
+ }
1584
+ function install(value, candidate) {
1585
+ binding = { ...value }, credential = candidate, clearTimeout(expiry), clearTimeout(renewalTimer);
1586
+ let remaining = binding.expiresAt - Date.now();
1587
+ expiry = setTimeout(invalidate, remaining), renewalTimer = setTimeout(() => {
1588
+ renew();
1589
+ }, remaining - Math.min(6e4, remaining / 5));
1590
+ }
1591
+ async function synchronize() {
1592
+ if (!alive()) throw new Error();
1593
+ if (credential && stored() !== credential) {
1594
+ if (!binding) throw new Error();
1595
+ if (observation) return observation;
1596
+ observation = (async () => {
1597
+ for (; alive(); ) {
1598
+ let candidate = stored();
1599
+ if (!candidate) throw new Error();
1600
+ let value;
1601
+ try {
1602
+ value = await proof(candidate);
1603
+ } catch {
1604
+ if (stored() !== candidate) continue;
1605
+ throw new Error();
1606
+ }
1607
+ if (stored() === candidate) {
1608
+ install(value, candidate);
1609
+ return;
1610
+ }
1611
+ }
1612
+ throw new Error();
1613
+ })();
1614
+ try {
1615
+ await observation;
1616
+ } catch {
1617
+ throw invalidate(), new Error("Local app context unavailable.");
1618
+ } finally {
1619
+ observation = void 0;
1620
+ }
1621
+ }
1622
+ }
1623
+ async function renew() {
1624
+ if (!(renewal || !binding || !alive())) {
1625
+ renewal = (async () => {
1626
+ let deadline = binding.expiresAt, requestController = new AbortController(), abort = () => requestController.abort();
1627
+ controller.signal.addEventListener("abort", abort, { once: !0 });
1628
+ let timeout = setTimeout(abort, Math.min(2e4, deadline - Date.now())), previous = credential;
1629
+ try {
1630
+ if (await synchronize(), credential !== previous) return;
1631
+ let url = `${origin}/_os/app/renew`, response;
1632
+ try {
1633
+ response = await fetch(url, {
1634
+ method: "POST",
1635
+ credentials: "omit",
1636
+ redirect: "error",
1637
+ cache: "no-store",
1638
+ referrerPolicy: "no-referrer",
1639
+ signal: requestController.signal,
1640
+ headers: { authorization: `Bearer ${previous}` }
1641
+ });
1642
+ } catch {
1643
+ throw new AppRenewalUnavailable();
1644
+ }
1645
+ if (response.redirected || response.url && response.url !== url) throw new Error();
1646
+ let value = await appResponse(response, 4096, requestController.signal);
1647
+ if (response.status === 503 && value && typeof value == "object" && !Array.isArray(value) && Object.keys(value).join(",") === "error" && "error" in value && value.error === "renewal_unavailable")
1648
+ throw new AppRenewalUnavailable();
1649
+ if (response.status === 403 && value && typeof value == "object" && !Array.isArray(value) && Object.keys(value).join(",") === "error" && "error" in value && value.error === "renewal_conflict") {
1650
+ for (; alive() && stored() === previous && !requestController.signal.aborted; )
1651
+ await new Promise((resolve2, reject) => {
1652
+ let abortWait = () => {
1653
+ clearTimeout(timer), reject(new Error());
1654
+ }, timer = setTimeout(() => {
1655
+ requestController.signal.removeEventListener("abort", abortWait), resolve2();
1656
+ }, 100);
1657
+ requestController.signal.addEventListener("abort", abortWait, { once: !0 });
1658
+ });
1659
+ if (requestController.signal.aborted) throw new Error();
1660
+ await synchronize();
1661
+ return;
1662
+ }
1663
+ if (!response.ok) throw new Error();
1664
+ if (!alive() || Date.now() >= deadline || !value || typeof value != "object" || Array.isArray(value) || Object.keys(value).sort().join(",") !== "context,credential") throw new Error();
1665
+ let result2 = value;
1666
+ if (!bounded(result2.context) || typeof result2.credential != "string" || !/^[A-Za-z0-9_-]{43}$/.test(result2.credential) || result2.credential === previous) throw new Error();
1667
+ if (stored() !== previous) {
1668
+ await synchronize();
1669
+ return;
1670
+ }
1671
+ globalThis.localStorage.setItem(sessionKey, result2.credential), install(result2.context, result2.credential);
1672
+ } catch (error) {
1673
+ try {
1674
+ if (alive() && stored() && stored() !== previous) {
1675
+ await synchronize();
1676
+ return;
1677
+ }
1678
+ } catch {
1679
+ }
1680
+ try {
1681
+ if (alive() && stored() && stored() !== previous) {
1682
+ await synchronize();
1683
+ return;
1684
+ }
1685
+ } catch {
1686
+ }
1687
+ (error instanceof AppRenewalUnavailable || error instanceof AppResponseReadError || requestController.signal.aborted) && current2() && Date.now() < deadline ? renewalTimer = setTimeout(() => {
1688
+ renew();
1689
+ }, Math.min(5e3, (deadline - Date.now()) / 5)) : invalidate();
1690
+ } finally {
1691
+ clearTimeout(timeout), controller.signal.removeEventListener("abort", abort);
1692
+ }
1693
+ })();
1694
+ try {
1695
+ await renewal;
1696
+ } finally {
1697
+ renewal = void 0;
1698
+ }
1699
+ }
1700
+ }
1701
+ let storageChanged = () => {
1702
+ if (credential) {
1703
+ if (!alive()) {
1704
+ invalidate();
1705
+ return;
1706
+ }
1707
+ synchronize().catch(invalidate);
1708
+ }
1552
1709
  }, monitor = setInterval(() => {
1553
- credential && !current2() && invalidate();
1554
- }, 100), dispose = () => {
1555
- controller.abort(), clearTimeout(expiry), clearInterval(monitor);
1710
+ storageChanged();
1711
+ }, 100);
1712
+ globalThis.addEventListener?.("storage", storageChanged);
1713
+ let dispose = () => {
1714
+ disposed = !0, cancel();
1556
1715
  };
1557
1716
  async function requestData(operation2, input) {
1558
1717
  try {
1559
1718
  if (operation2 !== "data/query" || !current2()) throw new Error();
1560
- let url = `${origin}/_os/app/data`, response = await fetch(url, {
1719
+ let url = `${origin}/_os/app/data`, admitted = credential, response = await fetch(url, {
1561
1720
  method: "POST",
1562
1721
  credentials: "omit",
1563
1722
  redirect: "error",
@@ -1567,10 +1726,12 @@ function appContext(origin, active2) {
1567
1726
  headers: { "content-type": "application/json", authorization: `Bearer ${credential}` },
1568
1727
  body: JSON.stringify(input)
1569
1728
  });
1729
+ for (await synchronize(); alive() && stored() !== credential; ) await synchronize();
1570
1730
  if (!current2() || response.redirected || response.url && response.url !== url) throw new Error();
1571
1731
  if (response.status === 401 || response.status === 403)
1572
- throw invalidate(), new DataOperationError("access_denied");
1732
+ throw admitted === credential && invalidate(), new DataOperationError("access_denied");
1573
1733
  let result2 = await appResponse(response, Number.MAX_SAFE_INTEGER, controller.signal);
1734
+ for (await synchronize(); alive() && stored() !== credential; ) await synchronize();
1574
1735
  if (!current2()) throw new Error();
1575
1736
  if (!response.ok) throw dataTransportError(response.status, result2);
1576
1737
  return result2;
@@ -1581,7 +1742,7 @@ function appContext(origin, active2) {
1581
1742
  async function requestRoutine(key, suffix, init2 = {}) {
1582
1743
  if (!current2())
1583
1744
  throw invalidate(), new Error("Local app context unavailable.");
1584
- let url = `${origin}/_os/app/routines/${encodeURIComponent(key)}/${suffix}`, response = await fetch(url, {
1745
+ let url = `${origin}/_os/app/routines/${encodeURIComponent(key)}/${suffix}`, admitted = credential, response = await fetch(url, {
1585
1746
  ...init2,
1586
1747
  credentials: "omit",
1587
1748
  redirect: "error",
@@ -1590,45 +1751,69 @@ function appContext(origin, active2) {
1590
1751
  signal: controller.signal,
1591
1752
  headers: { ...init2.body === void 0 ? {} : { "content-type": "application/json" }, authorization: `Bearer ${credential}` }
1592
1753
  });
1754
+ for (await synchronize(); alive() && stored() !== credential; ) await synchronize();
1593
1755
  if (!current2() || response.redirected || response.url && response.url !== url)
1594
1756
  throw invalidate(), new Error("Local app context unavailable.");
1595
- return (response.status === 401 || response.status === 403) && invalidate(), response;
1757
+ return (response.status === 401 || response.status === 403) && admitted === credential && invalidate(), responseChecks.set(response, async (value) => {
1758
+ for (await synchronize(); alive() && stored() !== credential; ) await synchronize();
1759
+ admitted === credential && value && typeof value == "object" && "error" in value && value.error === "authority_unavailable" && invalidate();
1760
+ }), response;
1761
+ }
1762
+ function context2() {
1763
+ let issued = credential;
1764
+ return {
1765
+ kind: "local",
1766
+ ...binding,
1767
+ get expiresAt() {
1768
+ return binding.expiresAt;
1769
+ },
1770
+ isCurrent: current2,
1771
+ signal: controller.signal,
1772
+ invalidate: () => {
1773
+ issued === credential && invalidate();
1774
+ },
1775
+ requestRoutine,
1776
+ requestData,
1777
+ requestAvailability: (key) => requestRoutine(key, "availability", { method: "GET" })
1778
+ };
1596
1779
  }
1597
- return { resolve: async () => {
1780
+ let resolveOnce = async () => {
1781
+ let attempted;
1598
1782
  try {
1599
1783
  if (denied || !active2() || globalThis.location.origin !== origin) throw new Error();
1600
- let stored = globalThis.localStorage.getItem(sessionKey);
1601
- if (!stored || !/^[A-Za-z0-9_-]{43}$/.test(stored) || credential && stored !== credential) throw new Error();
1602
- if (credential = stored, !current2()) throw new Error();
1603
- let response = await fetch(`${origin}/_os/app/context`, {
1604
- credentials: "omit",
1605
- redirect: "error",
1606
- referrerPolicy: "no-referrer",
1607
- headers: { authorization: `Bearer ${credential}` },
1608
- signal: controller.signal
1609
- });
1610
- if (!response.ok || response.redirected || response.url && response.url !== `${origin}/_os/app/context` || !response.headers.get("content-type")?.startsWith("application/json")) throw new Error();
1611
- let value = await appResponse(response, 4096, controller.signal);
1612
- if (!validBinding(value) || !current2() || value.expiresAt <= Date.now() || value.expiresAt > Date.now() + 6e5 || binding && (value.appId !== binding.appId || value.memberId !== binding.memberId || value.workspace !== binding.workspace || value.expiresAt > binding.expiresAt)) throw new Error();
1613
- return binding = { ...value }, clearTimeout(expiry), expiry = setTimeout(invalidate, binding.expiresAt - Date.now()), {
1614
- kind: "local",
1615
- ...binding,
1616
- isCurrent: current2,
1617
- signal: controller.signal,
1618
- invalidate,
1619
- requestRoutine,
1620
- requestData,
1621
- requestAvailability: (key) => requestRoutine(key, "availability", { method: "GET" })
1622
- };
1623
- } catch {
1624
- throw invalidate(), new Error("Local app context unavailable.");
1784
+ credential && await synchronize();
1785
+ let candidate = stored();
1786
+ if (!candidate || !/^[A-Za-z0-9_-]{43}$/.test(candidate)) throw new Error();
1787
+ if (credential = candidate, attempted = candidate, !current2()) throw new Error();
1788
+ let value = await proof(candidate);
1789
+ if (stored() !== candidate || credential !== candidate)
1790
+ return binding || (binding = { ...value }), await synchronize(), context2();
1791
+ if (!current2()) throw new Error();
1792
+ if (binding && value.expiresAt > binding.expiresAt) throw new Error();
1793
+ return (!binding || value.expiresAt !== binding.expiresAt) && install(value, candidate), context2();
1794
+ } catch (error) {
1795
+ try {
1796
+ if (binding && current2() && attempted && attempted !== credential) return context2();
1797
+ if (binding && alive() && stored() && stored() !== credential)
1798
+ return await synchronize(), context2();
1799
+ } catch {
1800
+ }
1801
+ throw error instanceof AppContextDenied && binding && alive() && renewal ? new Error("Local app context unavailable.") : (invalidate(), new Error("Local app context unavailable."));
1625
1802
  }
1803
+ }, resolve = () => resolution2 || (resolution2 = resolveOnce().finally(() => {
1804
+ resolution2 = void 0;
1805
+ }), resolution2);
1806
+ return { resolve, start: () => {
1807
+ startup = setTimeout(() => {
1808
+ alive() && !resolution2 && !binding && resolve().catch(() => {
1809
+ });
1810
+ }, 0);
1626
1811
  }, invalidate, dispose };
1627
1812
  }
1628
1813
  function validBinding(value) {
1629
- if (!value || typeof value != "object" || Array.isArray(value) || Object.keys(value).sort().join(",") !== "appId,expiresAt,memberId,workspace") return !1;
1814
+ if (!value || typeof value != "object" || Array.isArray(value) || Object.keys(value).sort().join(",") !== "appId,expiresAt,memberId,sessionId,workspace") return !1;
1630
1815
  let binding = value;
1631
- return typeof binding.appId == "string" && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(binding.appId) && typeof binding.workspace == "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(binding.workspace) && typeof binding.memberId == "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(binding.memberId) && typeof binding.expiresAt == "number" && Number.isFinite(binding.expiresAt);
1816
+ return typeof binding.appId == "string" && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(binding.appId) && typeof binding.workspace == "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(binding.workspace) && typeof binding.memberId == "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(binding.memberId) && typeof binding.sessionId == "string" && /^[A-Za-z0-9_-]{43}$/.test(binding.sessionId) && typeof binding.expiresAt == "number" && Number.isFinite(binding.expiresAt);
1632
1817
  }
1633
1818
 
1634
1819
  var installedDispose;
@@ -1664,7 +1849,7 @@ function init({ auth }) {
1664
1849
  }), dispose = () => {
1665
1850
  active2 && (active2 = !1, local?.dispose(), revision++, subscription.unsubscribe(), uninstall(), installedDispose === dispose && (installedDispose = void 0));
1666
1851
  };
1667
- installedDispose = dispose;
1852
+ installedDispose = dispose, local?.start();
1668
1853
  }
1669
1854
  var os = { init };
1670
1855
 
package/dist/index.js CHANGED
@@ -22030,7 +22030,7 @@ function privateRoutineRequest(environment = typeof __ODDINGTON_BROWSER__ !== "u
22030
22030
  const tcp = /^tcp:\/\/127\.0\.0\.1:([1-9][0-9]{0,4})$/.exec(socket);
22031
22031
  const endpoint = tcp ? { hostname: "127.0.0.1", port: Number(tcp[1]) } : { socketPath: socket };
22032
22032
  const id2 = crypto.randomUUID();
22033
- if (init.body instanceof ReadableStream) {
22033
+ if (init.body instanceof ReadableStream || suffix === "" && init.method === "POST") {
22034
22034
  await new Promise((resolve, reject) => {
22035
22035
  const admission = httpRequest({
22036
22036
  ...endpoint,
@@ -23114,9 +23114,16 @@ var files = {
23114
23114
  };
23115
23115
  // src/app-context.ts
23116
23116
  var sessionKey = "_os.app.session";
23117
+ var responseChecks = new WeakMap;
23117
23118
 
23118
23119
  class AppResponseReadError extends Error {
23119
23120
  }
23121
+
23122
+ class AppContextDenied extends Error {
23123
+ }
23124
+
23125
+ class AppRenewalUnavailable extends Error {
23126
+ }
23120
23127
  async function appResponse(response, maximum, signal) {
23121
23128
  if (signal.aborted || !response.body || response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() !== "application/json")
23122
23129
  throw new Error;
@@ -23137,8 +23144,13 @@ async function appResponse(response, maximum, signal) {
23137
23144
  }
23138
23145
  if (signal.aborted)
23139
23146
  throw new Error;
23140
- if (chunk.done)
23141
- return JSON.parse(text3 + decoder.decode());
23147
+ if (chunk.done) {
23148
+ const value = JSON.parse(text3 + decoder.decode());
23149
+ await responseChecks.get(response)?.(value);
23150
+ if (signal.aborted)
23151
+ throw new Error;
23152
+ return value;
23153
+ }
23142
23154
  bytes += chunk.value.byteLength;
23143
23155
  if (bytes > maximum) {
23144
23156
  abort();
@@ -23165,38 +23177,239 @@ function appContext(origin, active2) {
23165
23177
  let denied = false;
23166
23178
  const controller = new AbortController;
23167
23179
  let expiry;
23168
- const invalidate = () => {
23169
- denied = true;
23180
+ let startup;
23181
+ let renewalTimer;
23182
+ let renewal;
23183
+ let observation;
23184
+ let resolution2;
23185
+ let disposed = false;
23186
+ const stored = () => globalThis.localStorage.getItem(sessionKey);
23187
+ const alive = () => {
23188
+ try {
23189
+ return !denied && !disposed && active2() && globalThis.location.origin === origin && (!binding || binding.expiresAt > Date.now());
23190
+ } catch {
23191
+ return false;
23192
+ }
23193
+ };
23194
+ const cancel = () => {
23170
23195
  controller.abort();
23171
23196
  clearTimeout(expiry);
23197
+ clearTimeout(startup);
23198
+ clearTimeout(renewalTimer);
23172
23199
  clearInterval(monitor);
23200
+ globalThis.removeEventListener?.("storage", storageChanged);
23201
+ };
23202
+ const invalidate = () => {
23203
+ denied = true;
23204
+ cancel();
23173
23205
  try {
23174
- if (active2() && globalThis.location.origin === origin && (!credential || globalThis.localStorage.getItem(sessionKey) === credential)) {
23206
+ if (!disposed && active2() && globalThis.location.origin === origin && (!credential || globalThis.localStorage.getItem(sessionKey) === credential)) {
23175
23207
  globalThis.localStorage.removeItem(sessionKey);
23176
23208
  }
23177
23209
  } catch {}
23178
23210
  };
23179
23211
  const current = () => {
23180
23212
  try {
23181
- return !denied && active2() && globalThis.location.origin === origin && globalThis.localStorage.getItem(sessionKey) === credential && (!binding || binding.expiresAt > Date.now());
23213
+ return alive() && stored() === credential;
23182
23214
  } catch {
23183
23215
  return false;
23184
23216
  }
23185
23217
  };
23186
- const monitor = setInterval(() => {
23187
- if (credential && !current())
23218
+ const sameSession = (value) => !binding || value.appId === binding.appId && value.memberId === binding.memberId && value.workspace === binding.workspace && value.sessionId === binding.sessionId;
23219
+ const bounded = (value) => validBinding(value) && value.expiresAt > Date.now() && value.expiresAt <= Date.now() + 600000 && sameSession(value);
23220
+ async function proof(candidate, signal = controller.signal) {
23221
+ if (!alive() || !/^[A-Za-z0-9_-]{43}$/.test(candidate))
23222
+ throw new Error;
23223
+ const url = `${origin}/_os/app/context`;
23224
+ const response = await fetch(url, {
23225
+ credentials: "omit",
23226
+ redirect: "error",
23227
+ cache: "no-store",
23228
+ referrerPolicy: "no-referrer",
23229
+ headers: { authorization: `Bearer ${candidate}` },
23230
+ signal
23231
+ });
23232
+ if (response.redirected || response.url && response.url !== url)
23233
+ throw new Error;
23234
+ if (response.status === 401 || response.status === 403)
23235
+ throw new AppContextDenied;
23236
+ if (!response.ok)
23237
+ throw new Error;
23238
+ const value = await appResponse(response, 4096, signal);
23239
+ if (!alive() || !bounded(value))
23240
+ throw new Error;
23241
+ return value;
23242
+ }
23243
+ function install(value, candidate) {
23244
+ binding = { ...value };
23245
+ credential = candidate;
23246
+ clearTimeout(expiry);
23247
+ clearTimeout(renewalTimer);
23248
+ const remaining = binding.expiresAt - Date.now();
23249
+ expiry = setTimeout(invalidate, remaining);
23250
+ renewalTimer = setTimeout(() => {
23251
+ renew();
23252
+ }, remaining - Math.min(60000, remaining / 5));
23253
+ }
23254
+ async function synchronize() {
23255
+ if (!alive())
23256
+ throw new Error;
23257
+ if (!credential)
23258
+ return;
23259
+ if (stored() === credential)
23260
+ return;
23261
+ if (!binding)
23262
+ throw new Error;
23263
+ if (observation)
23264
+ return observation;
23265
+ observation = (async () => {
23266
+ while (alive()) {
23267
+ const candidate = stored();
23268
+ if (!candidate)
23269
+ throw new Error;
23270
+ let value;
23271
+ try {
23272
+ value = await proof(candidate);
23273
+ } catch {
23274
+ if (stored() !== candidate)
23275
+ continue;
23276
+ throw new Error;
23277
+ }
23278
+ if (stored() !== candidate)
23279
+ continue;
23280
+ install(value, candidate);
23281
+ return;
23282
+ }
23283
+ throw new Error;
23284
+ })();
23285
+ try {
23286
+ await observation;
23287
+ } catch {
23288
+ invalidate();
23289
+ throw new Error("Local app context unavailable.");
23290
+ } finally {
23291
+ observation = undefined;
23292
+ }
23293
+ }
23294
+ async function renew() {
23295
+ if (renewal || !binding || !alive())
23296
+ return;
23297
+ renewal = (async () => {
23298
+ const deadline = binding.expiresAt;
23299
+ const requestController = new AbortController;
23300
+ const abort = () => requestController.abort();
23301
+ controller.signal.addEventListener("abort", abort, { once: true });
23302
+ const timeout = setTimeout(abort, Math.min(20000, deadline - Date.now()));
23303
+ const previous = credential;
23304
+ try {
23305
+ await synchronize();
23306
+ if (credential !== previous)
23307
+ return;
23308
+ const url = `${origin}/_os/app/renew`;
23309
+ let response;
23310
+ try {
23311
+ response = await fetch(url, {
23312
+ method: "POST",
23313
+ credentials: "omit",
23314
+ redirect: "error",
23315
+ cache: "no-store",
23316
+ referrerPolicy: "no-referrer",
23317
+ signal: requestController.signal,
23318
+ headers: { authorization: `Bearer ${previous}` }
23319
+ });
23320
+ } catch {
23321
+ throw new AppRenewalUnavailable;
23322
+ }
23323
+ if (response.redirected || response.url && response.url !== url)
23324
+ throw new Error;
23325
+ const value = await appResponse(response, 4096, requestController.signal);
23326
+ if (response.status === 503 && value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).join(",") === "error" && "error" in value && value.error === "renewal_unavailable")
23327
+ throw new AppRenewalUnavailable;
23328
+ if (response.status === 403 && value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).join(",") === "error" && "error" in value && value.error === "renewal_conflict") {
23329
+ while (alive() && stored() === previous && !requestController.signal.aborted) {
23330
+ await new Promise((resolve2, reject) => {
23331
+ const abortWait = () => {
23332
+ clearTimeout(timer);
23333
+ reject(new Error);
23334
+ };
23335
+ const timer = setTimeout(() => {
23336
+ requestController.signal.removeEventListener("abort", abortWait);
23337
+ resolve2();
23338
+ }, 100);
23339
+ requestController.signal.addEventListener("abort", abortWait, { once: true });
23340
+ });
23341
+ }
23342
+ if (requestController.signal.aborted)
23343
+ throw new Error;
23344
+ await synchronize();
23345
+ return;
23346
+ }
23347
+ if (!response.ok)
23348
+ throw new Error;
23349
+ if (!alive() || Date.now() >= deadline || !value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).sort().join(",") !== "context,credential")
23350
+ throw new Error;
23351
+ const result2 = value;
23352
+ if (!bounded(result2.context) || typeof result2.credential !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(result2.credential) || result2.credential === previous)
23353
+ throw new Error;
23354
+ if (stored() !== previous) {
23355
+ await synchronize();
23356
+ return;
23357
+ }
23358
+ globalThis.localStorage.setItem(sessionKey, result2.credential);
23359
+ install(result2.context, result2.credential);
23360
+ } catch (error) {
23361
+ try {
23362
+ if (alive() && stored() && stored() !== previous) {
23363
+ await synchronize();
23364
+ return;
23365
+ }
23366
+ } catch {}
23367
+ try {
23368
+ if (alive() && stored() && stored() !== previous) {
23369
+ await synchronize();
23370
+ return;
23371
+ }
23372
+ } catch {}
23373
+ if ((error instanceof AppRenewalUnavailable || error instanceof AppResponseReadError || requestController.signal.aborted) && current() && Date.now() < deadline) {
23374
+ renewalTimer = setTimeout(() => {
23375
+ renew();
23376
+ }, Math.min(5000, (deadline - Date.now()) / 5));
23377
+ } else
23378
+ invalidate();
23379
+ } finally {
23380
+ clearTimeout(timeout);
23381
+ controller.signal.removeEventListener("abort", abort);
23382
+ }
23383
+ })();
23384
+ try {
23385
+ await renewal;
23386
+ } finally {
23387
+ renewal = undefined;
23388
+ }
23389
+ }
23390
+ const storageChanged = () => {
23391
+ if (!credential)
23392
+ return;
23393
+ if (!alive()) {
23188
23394
  invalidate();
23395
+ return;
23396
+ }
23397
+ synchronize().catch(invalidate);
23398
+ };
23399
+ const monitor = setInterval(() => {
23400
+ storageChanged();
23189
23401
  }, 100);
23402
+ globalThis.addEventListener?.("storage", storageChanged);
23190
23403
  const dispose = () => {
23191
- controller.abort();
23192
- clearTimeout(expiry);
23193
- clearInterval(monitor);
23404
+ disposed = true;
23405
+ cancel();
23194
23406
  };
23195
23407
  async function requestData(operation2, input) {
23196
23408
  try {
23197
23409
  if (operation2 !== "data/query" || !current())
23198
23410
  throw new Error;
23199
23411
  const url = `${origin}/_os/app/data`;
23412
+ const admitted = credential;
23200
23413
  const response = await fetch(url, {
23201
23414
  method: "POST",
23202
23415
  credentials: "omit",
@@ -23207,13 +23420,20 @@ function appContext(origin, active2) {
23207
23420
  headers: { "content-type": "application/json", authorization: `Bearer ${credential}` },
23208
23421
  body: JSON.stringify(input)
23209
23422
  });
23423
+ await synchronize();
23424
+ while (alive() && stored() !== credential)
23425
+ await synchronize();
23210
23426
  if (!current() || response.redirected || response.url && response.url !== url)
23211
23427
  throw new Error;
23212
23428
  if (response.status === 401 || response.status === 403) {
23213
- invalidate();
23429
+ if (admitted === credential)
23430
+ invalidate();
23214
23431
  throw new DataOperationError("access_denied");
23215
23432
  }
23216
23433
  const result2 = await appResponse(response, Number.MAX_SAFE_INTEGER, controller.signal);
23434
+ await synchronize();
23435
+ while (alive() && stored() !== credential)
23436
+ await synchronize();
23217
23437
  if (!current())
23218
23438
  throw new Error;
23219
23439
  if (!response.ok)
@@ -23233,6 +23453,7 @@ function appContext(origin, active2) {
23233
23453
  throw new Error("Local app context unavailable.");
23234
23454
  }
23235
23455
  const url = `${origin}/_os/app/routines/${encodeURIComponent(key)}/${suffix}`;
23456
+ const admitted = credential;
23236
23457
  const response = await fetch(url, {
23237
23458
  ...init,
23238
23459
  credentials: "omit",
@@ -23242,61 +23463,107 @@ function appContext(origin, active2) {
23242
23463
  signal: controller.signal,
23243
23464
  headers: { ...init.body === undefined ? {} : { "content-type": "application/json" }, authorization: `Bearer ${credential}` }
23244
23465
  });
23466
+ await synchronize();
23467
+ while (alive() && stored() !== credential)
23468
+ await synchronize();
23245
23469
  if (!current() || response.redirected || response.url && response.url !== url) {
23246
23470
  invalidate();
23247
23471
  throw new Error("Local app context unavailable.");
23248
23472
  }
23249
- if (response.status === 401 || response.status === 403)
23473
+ if ((response.status === 401 || response.status === 403) && admitted === credential)
23250
23474
  invalidate();
23475
+ responseChecks.set(response, async (value) => {
23476
+ await synchronize();
23477
+ while (alive() && stored() !== credential)
23478
+ await synchronize();
23479
+ if (admitted === credential && value && typeof value === "object" && "error" in value && value.error === "authority_unavailable")
23480
+ invalidate();
23481
+ });
23251
23482
  return response;
23252
23483
  }
23253
- const resolve = async () => {
23484
+ function context() {
23485
+ const issued = credential;
23486
+ return {
23487
+ kind: "local",
23488
+ ...binding,
23489
+ get expiresAt() {
23490
+ return binding.expiresAt;
23491
+ },
23492
+ isCurrent: current,
23493
+ signal: controller.signal,
23494
+ invalidate: () => {
23495
+ if (issued === credential)
23496
+ invalidate();
23497
+ },
23498
+ requestRoutine,
23499
+ requestData,
23500
+ requestAvailability: (key) => requestRoutine(key, "availability", { method: "GET" })
23501
+ };
23502
+ }
23503
+ const resolveOnce = async () => {
23504
+ let attempted;
23254
23505
  try {
23255
23506
  if (denied || !active2() || globalThis.location.origin !== origin)
23256
23507
  throw new Error;
23257
- const stored = globalThis.localStorage.getItem(sessionKey);
23258
- if (!stored || !/^[A-Za-z0-9_-]{43}$/.test(stored) || credential && stored !== credential)
23508
+ if (credential)
23509
+ await synchronize();
23510
+ const candidate = stored();
23511
+ if (!candidate || !/^[A-Za-z0-9_-]{43}$/.test(candidate))
23259
23512
  throw new Error;
23260
- credential = stored;
23513
+ credential = candidate;
23514
+ attempted = candidate;
23261
23515
  if (!current())
23262
23516
  throw new Error;
23263
- const response = await fetch(`${origin}/_os/app/context`, {
23264
- credentials: "omit",
23265
- redirect: "error",
23266
- referrerPolicy: "no-referrer",
23267
- headers: { authorization: `Bearer ${credential}` },
23268
- signal: controller.signal
23269
- });
23270
- if (!response.ok || response.redirected || response.url && response.url !== `${origin}/_os/app/context` || !response.headers.get("content-type")?.startsWith("application/json"))
23517
+ const value = await proof(candidate);
23518
+ if (stored() !== candidate || credential !== candidate) {
23519
+ if (!binding)
23520
+ binding = { ...value };
23521
+ await synchronize();
23522
+ return context();
23523
+ }
23524
+ if (!current())
23271
23525
  throw new Error;
23272
- const value = await appResponse(response, 4096, controller.signal);
23273
- if (!validBinding(value) || !current() || value.expiresAt <= Date.now() || value.expiresAt > Date.now() + 600000 || binding && (value.appId !== binding.appId || value.memberId !== binding.memberId || value.workspace !== binding.workspace || value.expiresAt > binding.expiresAt))
23526
+ if (binding && value.expiresAt > binding.expiresAt)
23274
23527
  throw new Error;
23275
- binding = { ...value };
23276
- clearTimeout(expiry);
23277
- expiry = setTimeout(invalidate, binding.expiresAt - Date.now());
23278
- return {
23279
- kind: "local",
23280
- ...binding,
23281
- isCurrent: current,
23282
- signal: controller.signal,
23283
- invalidate,
23284
- requestRoutine,
23285
- requestData,
23286
- requestAvailability: (key) => requestRoutine(key, "availability", { method: "GET" })
23287
- };
23288
- } catch {
23528
+ if (!binding || value.expiresAt !== binding.expiresAt)
23529
+ install(value, candidate);
23530
+ return context();
23531
+ } catch (error) {
23532
+ try {
23533
+ if (binding && current() && attempted && attempted !== credential)
23534
+ return context();
23535
+ if (binding && alive() && stored() && stored() !== credential) {
23536
+ await synchronize();
23537
+ return context();
23538
+ }
23539
+ } catch {}
23540
+ if (error instanceof AppContextDenied && binding && alive() && renewal)
23541
+ throw new Error("Local app context unavailable.");
23289
23542
  invalidate();
23290
23543
  throw new Error("Local app context unavailable.");
23291
23544
  }
23292
23545
  };
23293
- return { resolve, invalidate, dispose };
23546
+ const resolve = () => {
23547
+ if (resolution2)
23548
+ return resolution2;
23549
+ resolution2 = resolveOnce().finally(() => {
23550
+ resolution2 = undefined;
23551
+ });
23552
+ return resolution2;
23553
+ };
23554
+ const start = () => {
23555
+ startup = setTimeout(() => {
23556
+ if (alive() && !resolution2 && !binding)
23557
+ resolve().catch(() => {});
23558
+ }, 0);
23559
+ };
23560
+ return { resolve, start, invalidate, dispose };
23294
23561
  }
23295
23562
  function validBinding(value) {
23296
- if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).sort().join(",") !== "appId,expiresAt,memberId,workspace")
23563
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).sort().join(",") !== "appId,expiresAt,memberId,sessionId,workspace")
23297
23564
  return false;
23298
23565
  const binding = value;
23299
- return typeof binding.appId === "string" && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(binding.appId) && typeof binding.workspace === "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(binding.workspace) && typeof binding.memberId === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(binding.memberId) && typeof binding.expiresAt === "number" && Number.isFinite(binding.expiresAt);
23566
+ return typeof binding.appId === "string" && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(binding.appId) && typeof binding.workspace === "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(binding.workspace) && typeof binding.memberId === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(binding.memberId) && typeof binding.sessionId === "string" && /^[A-Za-z0-9_-]{43}$/.test(binding.sessionId) && typeof binding.expiresAt === "number" && Number.isFinite(binding.expiresAt);
23300
23567
  }
23301
23568
 
23302
23569
  // src/os.ts
@@ -23364,6 +23631,7 @@ function init({ auth }) {
23364
23631
  installedDispose = undefined;
23365
23632
  };
23366
23633
  installedDispose = dispose;
23634
+ local?.start();
23367
23635
  }
23368
23636
  var os = { init };
23369
23637
  // src/app-routine-browser.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oddington/sdk",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Oddington routine and response-handler SDK",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",